Add options to prevent users from changing their profile. (#7096)

This commit is contained in:
Dirk Klimpel 2020-03-27 20:15:23 +01:00 committed by GitHub
parent ae219fb411
commit 8327eb9280
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 449 additions and 1 deletions

1
changelog.d/7096.feature Normal file
View file

@ -0,0 +1 @@
Add options to prevent users from changing their profile or associated 3PIDs.

View file

@ -1086,6 +1086,29 @@ account_threepid_delegates:
#email: https://example.com # Delegate email sending to example.com #email: https://example.com # Delegate email sending to example.com
#msisdn: http://localhost:8090 # Delegate SMS sending to this local process #msisdn: http://localhost:8090 # Delegate SMS sending to this local process
# Whether users are allowed to change their displayname after it has
# been initially set. Useful when provisioning users based on the
# contents of a third-party directory.
#
# Does not apply to server administrators. Defaults to 'true'
#
#enable_set_displayname: false
# Whether users are allowed to change their avatar after it has been
# initially set. Useful when provisioning users based on the contents
# of a third-party directory.
#
# Does not apply to server administrators. Defaults to 'true'
#
#enable_set_avatar_url: false
# Whether users can change the 3PIDs associated with their accounts
# (email address and msisdn).
#
# Defaults to 'true'
#
#enable_3pid_changes: false
# Users who register on this homeserver will automatically be joined # Users who register on this homeserver will automatically be joined
# to these rooms # to these rooms
# #

View file

@ -129,6 +129,10 @@ class RegistrationConfig(Config):
raise ConfigError("Invalid auto_join_rooms entry %s" % (room_alias,)) raise ConfigError("Invalid auto_join_rooms entry %s" % (room_alias,))
self.autocreate_auto_join_rooms = config.get("autocreate_auto_join_rooms", True) self.autocreate_auto_join_rooms = config.get("autocreate_auto_join_rooms", True)
self.enable_set_displayname = config.get("enable_set_displayname", True)
self.enable_set_avatar_url = config.get("enable_set_avatar_url", True)
self.enable_3pid_changes = config.get("enable_3pid_changes", True)
self.disable_msisdn_registration = config.get( self.disable_msisdn_registration = config.get(
"disable_msisdn_registration", False "disable_msisdn_registration", False
) )
@ -330,6 +334,29 @@ class RegistrationConfig(Config):
#email: https://example.com # Delegate email sending to example.com #email: https://example.com # Delegate email sending to example.com
#msisdn: http://localhost:8090 # Delegate SMS sending to this local process #msisdn: http://localhost:8090 # Delegate SMS sending to this local process
# Whether users are allowed to change their displayname after it has
# been initially set. Useful when provisioning users based on the
# contents of a third-party directory.
#
# Does not apply to server administrators. Defaults to 'true'
#
#enable_set_displayname: false
# Whether users are allowed to change their avatar after it has been
# initially set. Useful when provisioning users based on the contents
# of a third-party directory.
#
# Does not apply to server administrators. Defaults to 'true'
#
#enable_set_avatar_url: false
# Whether users can change the 3PIDs associated with their accounts
# (email address and msisdn).
#
# Defaults to 'true'
#
#enable_3pid_changes: false
# Users who register on this homeserver will automatically be joined # Users who register on this homeserver will automatically be joined
# to these rooms # to these rooms
# #

View file

@ -157,6 +157,15 @@ class BaseProfileHandler(BaseHandler):
if not by_admin and target_user != requester.user: if not by_admin and target_user != requester.user:
raise AuthError(400, "Cannot set another user's displayname") raise AuthError(400, "Cannot set another user's displayname")
if not by_admin and not self.hs.config.enable_set_displayname:
profile = yield self.store.get_profileinfo(target_user.localpart)
if profile.display_name:
raise SynapseError(
400,
"Changing display name is disabled on this server",
Codes.FORBIDDEN,
)
if len(new_displayname) > MAX_DISPLAYNAME_LEN: if len(new_displayname) > MAX_DISPLAYNAME_LEN:
raise SynapseError( raise SynapseError(
400, "Displayname is too long (max %i)" % (MAX_DISPLAYNAME_LEN,) 400, "Displayname is too long (max %i)" % (MAX_DISPLAYNAME_LEN,)
@ -218,6 +227,13 @@ class BaseProfileHandler(BaseHandler):
if not by_admin and target_user != requester.user: if not by_admin and target_user != requester.user:
raise AuthError(400, "Cannot set another user's avatar_url") raise AuthError(400, "Cannot set another user's avatar_url")
if not by_admin and not self.hs.config.enable_set_avatar_url:
profile = yield self.store.get_profileinfo(target_user.localpart)
if profile.avatar_url:
raise SynapseError(
400, "Changing avatar is disabled on this server", Codes.FORBIDDEN
)
if len(new_avatar_url) > MAX_AVATAR_URL_LEN: if len(new_avatar_url) > MAX_AVATAR_URL_LEN:
raise SynapseError( raise SynapseError(
400, "Avatar URL is too long (max %i)" % (MAX_AVATAR_URL_LEN,) 400, "Avatar URL is too long (max %i)" % (MAX_AVATAR_URL_LEN,)

View file

@ -605,6 +605,11 @@ class ThreepidRestServlet(RestServlet):
return 200, {"threepids": threepids} return 200, {"threepids": threepids}
async def on_POST(self, request): async def on_POST(self, request):
if not self.hs.config.enable_3pid_changes:
raise SynapseError(
400, "3PID changes are disabled on this server", Codes.FORBIDDEN
)
requester = await self.auth.get_user_by_req(request) requester = await self.auth.get_user_by_req(request)
user_id = requester.user.to_string() user_id = requester.user.to_string()
body = parse_json_object_from_request(request) body = parse_json_object_from_request(request)
@ -649,6 +654,11 @@ class ThreepidAddRestServlet(RestServlet):
@interactive_auth_handler @interactive_auth_handler
async def on_POST(self, request): async def on_POST(self, request):
if not self.hs.config.enable_3pid_changes:
raise SynapseError(
400, "3PID changes are disabled on this server", Codes.FORBIDDEN
)
requester = await self.auth.get_user_by_req(request) requester = await self.auth.get_user_by_req(request)
user_id = requester.user.to_string() user_id = requester.user.to_string()
body = parse_json_object_from_request(request) body = parse_json_object_from_request(request)
@ -744,10 +754,16 @@ class ThreepidDeleteRestServlet(RestServlet):
def __init__(self, hs): def __init__(self, hs):
super(ThreepidDeleteRestServlet, self).__init__() super(ThreepidDeleteRestServlet, self).__init__()
self.hs = hs
self.auth = hs.get_auth() self.auth = hs.get_auth()
self.auth_handler = hs.get_auth_handler() self.auth_handler = hs.get_auth_handler()
async def on_POST(self, request): async def on_POST(self, request):
if not self.hs.config.enable_3pid_changes:
raise SynapseError(
400, "3PID changes are disabled on this server", Codes.FORBIDDEN
)
body = parse_json_object_from_request(request) body = parse_json_object_from_request(request)
assert_params_in_dict(body, ["medium", "address"]) assert_params_in_dict(body, ["medium", "address"])

View file

@ -19,7 +19,7 @@ from mock import Mock, NonCallableMock
from twisted.internet import defer from twisted.internet import defer
import synapse.types import synapse.types
from synapse.api.errors import AuthError from synapse.api.errors import AuthError, SynapseError
from synapse.handlers.profile import MasterProfileHandler from synapse.handlers.profile import MasterProfileHandler
from synapse.types import UserID from synapse.types import UserID
@ -70,6 +70,7 @@ class ProfileTestCase(unittest.TestCase):
yield self.store.create_profile(self.frank.localpart) yield self.store.create_profile(self.frank.localpart)
self.handler = hs.get_profile_handler() self.handler = hs.get_profile_handler()
self.hs = hs
@defer.inlineCallbacks @defer.inlineCallbacks
def test_get_my_name(self): def test_get_my_name(self):
@ -90,6 +91,33 @@ class ProfileTestCase(unittest.TestCase):
"Frank Jr.", "Frank Jr.",
) )
# Set displayname again
yield self.handler.set_displayname(
self.frank, synapse.types.create_requester(self.frank), "Frank"
)
self.assertEquals(
(yield self.store.get_profile_displayname(self.frank.localpart)), "Frank",
)
@defer.inlineCallbacks
def test_set_my_name_if_disabled(self):
self.hs.config.enable_set_displayname = False
# Setting displayname for the first time is allowed
yield self.store.set_profile_displayname(self.frank.localpart, "Frank")
self.assertEquals(
(yield self.store.get_profile_displayname(self.frank.localpart)), "Frank",
)
# Setting displayname a second time is forbidden
d = self.handler.set_displayname(
self.frank, synapse.types.create_requester(self.frank), "Frank Jr."
)
yield self.assertFailure(d, SynapseError)
@defer.inlineCallbacks @defer.inlineCallbacks
def test_set_my_name_noauth(self): def test_set_my_name_noauth(self):
d = self.handler.set_displayname( d = self.handler.set_displayname(
@ -147,3 +175,38 @@ class ProfileTestCase(unittest.TestCase):
(yield self.store.get_profile_avatar_url(self.frank.localpart)), (yield self.store.get_profile_avatar_url(self.frank.localpart)),
"http://my.server/pic.gif", "http://my.server/pic.gif",
) )
# Set avatar again
yield self.handler.set_avatar_url(
self.frank,
synapse.types.create_requester(self.frank),
"http://my.server/me.png",
)
self.assertEquals(
(yield self.store.get_profile_avatar_url(self.frank.localpart)),
"http://my.server/me.png",
)
@defer.inlineCallbacks
def test_set_my_avatar_if_disabled(self):
self.hs.config.enable_set_avatar_url = False
# Setting displayname for the first time is allowed
yield self.store.set_profile_avatar_url(
self.frank.localpart, "http://my.server/me.png"
)
self.assertEquals(
(yield self.store.get_profile_avatar_url(self.frank.localpart)),
"http://my.server/me.png",
)
# Set avatar a second time is forbidden
d = self.handler.set_avatar_url(
self.frank,
synapse.types.create_requester(self.frank),
"http://my.server/pic.gif",
)
yield self.assertFailure(d, SynapseError)

View file

@ -24,6 +24,7 @@ import pkg_resources
import synapse.rest.admin import synapse.rest.admin
from synapse.api.constants import LoginType, Membership from synapse.api.constants import LoginType, Membership
from synapse.api.errors import Codes
from synapse.rest.client.v1 import login, room from synapse.rest.client.v1 import login, room
from synapse.rest.client.v2_alpha import account, register from synapse.rest.client.v2_alpha import account, register
@ -325,3 +326,304 @@ class DeactivateTestCase(unittest.HomeserverTestCase):
) )
self.render(request) self.render(request)
self.assertEqual(request.code, 200) self.assertEqual(request.code, 200)
class ThreepidEmailRestTestCase(unittest.HomeserverTestCase):
servlets = [
account.register_servlets,
login.register_servlets,
synapse.rest.admin.register_servlets_for_client_rest_resource,
]
def make_homeserver(self, reactor, clock):
config = self.default_config()
# Email config.
self.email_attempts = []
def sendmail(smtphost, from_addr, to_addrs, msg, **kwargs):
self.email_attempts.append(msg)
config["email"] = {
"enable_notifs": False,
"template_dir": os.path.abspath(
pkg_resources.resource_filename("synapse", "res/templates")
),
"smtp_host": "127.0.0.1",
"smtp_port": 20,
"require_transport_security": False,
"smtp_user": None,
"smtp_pass": None,
"notif_from": "test@example.com",
}
config["public_baseurl"] = "https://example.com"
self.hs = self.setup_test_homeserver(config=config, sendmail=sendmail)
return self.hs
def prepare(self, reactor, clock, hs):
self.store = hs.get_datastore()
self.user_id = self.register_user("kermit", "test")
self.user_id_tok = self.login("kermit", "test")
self.email = "test@example.com"
self.url_3pid = b"account/3pid"
def test_add_email(self):
"""Test adding an email to profile
"""
client_secret = "foobar"
session_id = self._request_token(self.email, client_secret)
self.assertEquals(len(self.email_attempts), 1)
link = self._get_link_from_email()
self._validate_token(link)
request, channel = self.make_request(
"POST",
b"/_matrix/client/unstable/account/3pid/add",
{
"client_secret": client_secret,
"sid": session_id,
"auth": {
"type": "m.login.password",
"user": self.user_id,
"password": "test",
},
},
access_token=self.user_id_tok,
)
self.render(request)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
# Get user
request, channel = self.make_request(
"GET", self.url_3pid, access_token=self.user_id_tok,
)
self.render(request)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual("email", channel.json_body["threepids"][0]["medium"])
self.assertEqual(self.email, channel.json_body["threepids"][0]["address"])
def test_add_email_if_disabled(self):
"""Test adding email to profile when doing so is disallowed
"""
self.hs.config.enable_3pid_changes = False
client_secret = "foobar"
session_id = self._request_token(self.email, client_secret)
self.assertEquals(len(self.email_attempts), 1)
link = self._get_link_from_email()
self._validate_token(link)
request, channel = self.make_request(
"POST",
b"/_matrix/client/unstable/account/3pid/add",
{
"client_secret": client_secret,
"sid": session_id,
"auth": {
"type": "m.login.password",
"user": self.user_id,
"password": "test",
},
},
access_token=self.user_id_tok,
)
self.render(request)
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
# Get user
request, channel = self.make_request(
"GET", self.url_3pid, access_token=self.user_id_tok,
)
self.render(request)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
self.assertFalse(channel.json_body["threepids"])
def test_delete_email(self):
"""Test deleting an email from profile
"""
# Add a threepid
self.get_success(
self.store.user_add_threepid(
user_id=self.user_id,
medium="email",
address=self.email,
validated_at=0,
added_at=0,
)
)
request, channel = self.make_request(
"POST",
b"account/3pid/delete",
{"medium": "email", "address": self.email},
access_token=self.user_id_tok,
)
self.render(request)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
# Get user
request, channel = self.make_request(
"GET", self.url_3pid, access_token=self.user_id_tok,
)
self.render(request)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
self.assertFalse(channel.json_body["threepids"])
def test_delete_email_if_disabled(self):
"""Test deleting an email from profile when disallowed
"""
self.hs.config.enable_3pid_changes = False
# Add a threepid
self.get_success(
self.store.user_add_threepid(
user_id=self.user_id,
medium="email",
address=self.email,
validated_at=0,
added_at=0,
)
)
request, channel = self.make_request(
"POST",
b"account/3pid/delete",
{"medium": "email", "address": self.email},
access_token=self.user_id_tok,
)
self.render(request)
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
# Get user
request, channel = self.make_request(
"GET", self.url_3pid, access_token=self.user_id_tok,
)
self.render(request)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual("email", channel.json_body["threepids"][0]["medium"])
self.assertEqual(self.email, channel.json_body["threepids"][0]["address"])
def test_cant_add_email_without_clicking_link(self):
"""Test that we do actually need to click the link in the email
"""
client_secret = "foobar"
session_id = self._request_token(self.email, client_secret)
self.assertEquals(len(self.email_attempts), 1)
# Attempt to add email without clicking the link
request, channel = self.make_request(
"POST",
b"/_matrix/client/unstable/account/3pid/add",
{
"client_secret": client_secret,
"sid": session_id,
"auth": {
"type": "m.login.password",
"user": self.user_id,
"password": "test",
},
},
access_token=self.user_id_tok,
)
self.render(request)
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual(Codes.THREEPID_AUTH_FAILED, channel.json_body["errcode"])
# Get user
request, channel = self.make_request(
"GET", self.url_3pid, access_token=self.user_id_tok,
)
self.render(request)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
self.assertFalse(channel.json_body["threepids"])
def test_no_valid_token(self):
"""Test that we do actually need to request a token and can't just
make a session up.
"""
client_secret = "foobar"
session_id = "weasle"
# Attempt to add email without even requesting an email
request, channel = self.make_request(
"POST",
b"/_matrix/client/unstable/account/3pid/add",
{
"client_secret": client_secret,
"sid": session_id,
"auth": {
"type": "m.login.password",
"user": self.user_id,
"password": "test",
},
},
access_token=self.user_id_tok,
)
self.render(request)
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual(Codes.THREEPID_AUTH_FAILED, channel.json_body["errcode"])
# Get user
request, channel = self.make_request(
"GET", self.url_3pid, access_token=self.user_id_tok,
)
self.render(request)
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
self.assertFalse(channel.json_body["threepids"])
def _request_token(self, email, client_secret):
request, channel = self.make_request(
"POST",
b"account/3pid/email/requestToken",
{"client_secret": client_secret, "email": email, "send_attempt": 1},
)
self.render(request)
self.assertEquals(200, channel.code, channel.result)
return channel.json_body["sid"]
def _validate_token(self, link):
# Remove the host
path = link.replace("https://example.com", "")
request, channel = self.make_request("GET", path, shorthand=False)
self.render(request)
self.assertEquals(200, channel.code, channel.result)
def _get_link_from_email(self):
assert self.email_attempts, "No emails have been sent"
raw_msg = self.email_attempts[-1].decode("UTF-8")
mail = Parser().parsestr(raw_msg)
text = None
for part in mail.walk():
if part.get_content_type() == "text/plain":
text = part.get_payload(decode=True).decode("UTF-8")
break
if not text:
self.fail("Could not find text portion of email to parse")
match = re.search(r"https://example.com\S+", text)
assert match, "Could not find link in email"
return match.group(0)