Compare commits
31 Commits
travis/sam
...
modular-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1389c6282 | ||
|
|
6354a27126 | ||
|
|
9655538913 | ||
|
|
33757bad19 | ||
|
|
2ed75d2589 | ||
|
|
a0b8bf023f | ||
|
|
7423fade92 | ||
|
|
321c054366 | ||
|
|
eafa6f3b0d | ||
|
|
75223bb1d3 | ||
|
|
b9d57502da | ||
|
|
a8ac40445c | ||
|
|
23bc5ee14e | ||
|
|
76b25075b0 | ||
|
|
f69f4e7a97 | ||
|
|
8a2e8eaaec | ||
|
|
785cbd3999 | ||
|
|
b18f54c845 | ||
|
|
cbb926f237 | ||
|
|
f7cb88c3bd | ||
|
|
a6fb79cf36 | ||
|
|
6d2557f1e8 | ||
|
|
cec502a595 | ||
|
|
fe8636b79e | ||
|
|
059369dd04 | ||
|
|
de7ba972be | ||
|
|
35e6e69ea9 | ||
|
|
34b826bef8 | ||
|
|
f014ea9b5c | ||
|
|
b0cf4228d2 | ||
|
|
21c037ac46 |
1
changelog.d/5878.feature
Normal file
1
changelog.d/5878.feature
Normal file
@@ -0,0 +1 @@
|
||||
Add admin API endpoint for setting whether or not a user is a server administrator.
|
||||
1
changelog.d/5914.feature
Normal file
1
changelog.d/5914.feature
Normal file
@@ -0,0 +1 @@
|
||||
Add admin API endpoint for getting whether or not a user is a server administrator.
|
||||
1
changelog.d/6015.feature
Normal file
1
changelog.d/6015.feature
Normal file
@@ -0,0 +1 @@
|
||||
Add config option to increase ratelimits for room admins redacting messages.
|
||||
1
changelog.d/6037.feature
Normal file
1
changelog.d/6037.feature
Normal file
@@ -0,0 +1 @@
|
||||
Handle userid clashes when authenticating via SAML by appending an integer suffix.
|
||||
1
changelog.d/6070.feature
Normal file
1
changelog.d/6070.feature
Normal file
@@ -0,0 +1 @@
|
||||
Handle userid clashes when authenticating via SAML by appending an integer suffix.
|
||||
@@ -84,3 +84,42 @@ with a body of:
|
||||
}
|
||||
|
||||
including an ``access_token`` of a server admin.
|
||||
|
||||
|
||||
Get whether a user is a server administrator or not
|
||||
===================================================
|
||||
|
||||
|
||||
The api is::
|
||||
|
||||
GET /_synapse/admin/v1/users/<user_id>/admin
|
||||
|
||||
including an ``access_token`` of a server admin.
|
||||
|
||||
A response body like the following is returned:
|
||||
|
||||
.. code:: json
|
||||
|
||||
{
|
||||
"admin": true
|
||||
}
|
||||
|
||||
|
||||
Change whether a user is a server administrator or not
|
||||
======================================================
|
||||
|
||||
Note that you cannot demote yourself.
|
||||
|
||||
The api is::
|
||||
|
||||
PUT /_synapse/admin/v1/users/<user_id>/admin
|
||||
|
||||
with a body of:
|
||||
|
||||
.. code:: json
|
||||
|
||||
{
|
||||
"admin": true
|
||||
}
|
||||
|
||||
including an ``access_token`` of a server admin.
|
||||
|
||||
@@ -510,6 +510,9 @@ log_config: "CONFDIR/SERVERNAME.log.config"
|
||||
# - one for login that ratelimits login requests based on the account the
|
||||
# client is attempting to log into, based on the amount of failed login
|
||||
# attempts for this account.
|
||||
# - one for ratelimiting redactions by room admins. If this is not explicitly
|
||||
# set then it uses the same ratelimiting as per rc_message. This is useful
|
||||
# to allow room admins to deal with abuse quickly.
|
||||
#
|
||||
# The defaults are as shown below.
|
||||
#
|
||||
@@ -531,6 +534,10 @@ log_config: "CONFDIR/SERVERNAME.log.config"
|
||||
# failed_attempts:
|
||||
# per_second: 0.17
|
||||
# burst_count: 3
|
||||
#
|
||||
#rc_admin_redaction:
|
||||
# per_second: 1
|
||||
# burst_count: 50
|
||||
|
||||
|
||||
# Ratelimiting settings for incoming federation
|
||||
@@ -1031,12 +1038,13 @@ signing_key_path: "CONFDIR/SERVERNAME.signing.key"
|
||||
|
||||
# Enable SAML2 for registration and login. Uses pysaml2.
|
||||
#
|
||||
# `sp_config` is the configuration for the pysaml2 Service Provider.
|
||||
# See pysaml2 docs for format of config.
|
||||
# At least one of `sp_config` or `config_path` must be set in this section to
|
||||
# enable SAML login.
|
||||
#
|
||||
# Default values will be used for the 'entityid' and 'service' settings,
|
||||
# so it is not normally necessary to specify them unless you need to
|
||||
# override them.
|
||||
# (You will probably also want to set the following options to `false` to
|
||||
# disable the regular login/registration flows:
|
||||
# * enable_registration
|
||||
# * password_config.enabled
|
||||
#
|
||||
# Once SAML support is enabled, a metadata file will be exposed at
|
||||
# https://<server>:<port>/_matrix/saml2/metadata.xml, which you may be able to
|
||||
@@ -1044,52 +1052,87 @@ signing_key_path: "CONFDIR/SERVERNAME.signing.key"
|
||||
# the IdP to use an ACS location of
|
||||
# https://<server>:<port>/_matrix/saml2/authn_response.
|
||||
#
|
||||
#saml2_config:
|
||||
# sp_config:
|
||||
# # point this to the IdP's metadata. You can use either a local file or
|
||||
# # (preferably) a URL.
|
||||
# metadata:
|
||||
# #local: ["saml2/idp.xml"]
|
||||
# remote:
|
||||
# - url: https://our_idp/metadata.xml
|
||||
#
|
||||
# # By default, the user has to go to our login page first. If you'd like to
|
||||
# # allow IdP-initiated login, set 'allow_unsolicited: True' in a
|
||||
# # 'service.sp' section:
|
||||
# #
|
||||
# #service:
|
||||
# # sp:
|
||||
# # allow_unsolicited: True
|
||||
#
|
||||
# # The examples below are just used to generate our metadata xml, and you
|
||||
# # may well not need it, depending on your setup. Alternatively you
|
||||
# # may need a whole lot more detail - see the pysaml2 docs!
|
||||
#
|
||||
# description: ["My awesome SP", "en"]
|
||||
# name: ["Test SP", "en"]
|
||||
#
|
||||
# organization:
|
||||
# name: Example com
|
||||
# display_name:
|
||||
# - ["Example co", "en"]
|
||||
# url: "http://example.com"
|
||||
#
|
||||
# contact_person:
|
||||
# - given_name: Bob
|
||||
# sur_name: "the Sysadmin"
|
||||
# email_address": ["admin@example.com"]
|
||||
# contact_type": technical
|
||||
#
|
||||
# # Instead of putting the config inline as above, you can specify a
|
||||
# # separate pysaml2 configuration file:
|
||||
# #
|
||||
# config_path: "CONFDIR/sp_conf.py"
|
||||
#
|
||||
# # the lifetime of a SAML session. This defines how long a user has to
|
||||
# # complete the authentication process, if allow_unsolicited is unset.
|
||||
# # The default is 5 minutes.
|
||||
# #
|
||||
# # saml_session_lifetime: 5m
|
||||
saml2_config:
|
||||
# `sp_config` is the configuration for the pysaml2 Service Provider.
|
||||
# See pysaml2 docs for format of config.
|
||||
#
|
||||
# Default values will be used for the 'entityid' and 'service' settings,
|
||||
# so it is not normally necessary to specify them unless you need to
|
||||
# override them.
|
||||
#
|
||||
#sp_config:
|
||||
# # point this to the IdP's metadata. You can use either a local file or
|
||||
# # (preferably) a URL.
|
||||
# metadata:
|
||||
# #local: ["saml2/idp.xml"]
|
||||
# remote:
|
||||
# - url: https://our_idp/metadata.xml
|
||||
#
|
||||
# # By default, the user has to go to our login page first. If you'd like
|
||||
# # to allow IdP-initiated login, set 'allow_unsolicited: True' in a
|
||||
# # 'service.sp' section:
|
||||
# #
|
||||
# #service:
|
||||
# # sp:
|
||||
# # allow_unsolicited: true
|
||||
#
|
||||
# # The examples below are just used to generate our metadata xml, and you
|
||||
# # may well not need them, depending on your setup. Alternatively you
|
||||
# # may need a whole lot more detail - see the pysaml2 docs!
|
||||
#
|
||||
# description: ["My awesome SP", "en"]
|
||||
# name: ["Test SP", "en"]
|
||||
#
|
||||
# organization:
|
||||
# name: Example com
|
||||
# display_name:
|
||||
# - ["Example co", "en"]
|
||||
# url: "http://example.com"
|
||||
#
|
||||
# contact_person:
|
||||
# - given_name: Bob
|
||||
# sur_name: "the Sysadmin"
|
||||
# email_address": ["admin@example.com"]
|
||||
# contact_type": technical
|
||||
|
||||
# Instead of putting the config inline as above, you can specify a
|
||||
# separate pysaml2 configuration file:
|
||||
#
|
||||
#config_path: "CONFDIR/sp_conf.py"
|
||||
|
||||
# the lifetime of a SAML session. This defines how long a user has to
|
||||
# complete the authentication process, if allow_unsolicited is unset.
|
||||
# The default is 5 minutes.
|
||||
#
|
||||
#saml_session_lifetime: 5m
|
||||
|
||||
# The SAML attribute (after mapping via the attribute maps) to use to derive
|
||||
# the Matrix ID from. 'uid' by default.
|
||||
#
|
||||
#mxid_source_attribute: displayName
|
||||
|
||||
# The mapping system to use for mapping the saml attribute onto a matrix ID.
|
||||
# Options include:
|
||||
# * 'hexencode' (which maps unpermitted characters to '=xx')
|
||||
# * 'dotreplace' (which replaces unpermitted characters with '.').
|
||||
# * 'dotreplace_email_localpart' (truncates at the first '@' and replaces
|
||||
# unpermitted characters with '.')
|
||||
# The default is 'hexencode'.
|
||||
#
|
||||
#mxid_mapping: dotreplace
|
||||
|
||||
# In previous versions of synapse, the mapping from SAML attribute to MXID was
|
||||
# always calculated dynamically rather than stored in a table. For backwards-
|
||||
# compatibility, we will look for user_ids matching such a pattern before
|
||||
# creating a new account.
|
||||
#
|
||||
# This setting controls the SAML attribute which will be used for this
|
||||
# backwards-compatibility lookup. Typically it should be 'uid', but if the
|
||||
# attribute maps are changed, it may be necessary to change it.
|
||||
#
|
||||
# The default is 'uid'.
|
||||
#
|
||||
#grandfathered_mxid_source_attribute: upn
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -35,4 +35,4 @@ try:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
__version__ = "1.3.1"
|
||||
__version__ = "1.3.2-alpha.5+modular"
|
||||
|
||||
@@ -80,6 +80,12 @@ class RatelimitConfig(Config):
|
||||
"federation_rr_transactions_per_room_per_second", 50
|
||||
)
|
||||
|
||||
rc_admin_redaction = config.get("rc_admin_redaction")
|
||||
if rc_admin_redaction:
|
||||
self.rc_admin_redaction = RateLimitConfig(rc_admin_redaction)
|
||||
else:
|
||||
self.rc_admin_redaction = None
|
||||
|
||||
def generate_config_section(self, **kwargs):
|
||||
return """\
|
||||
## Ratelimiting ##
|
||||
@@ -102,6 +108,9 @@ class RatelimitConfig(Config):
|
||||
# - one for login that ratelimits login requests based on the account the
|
||||
# client is attempting to log into, based on the amount of failed login
|
||||
# attempts for this account.
|
||||
# - one for ratelimiting redactions by room admins. If this is not explicitly
|
||||
# set then it uses the same ratelimiting as per rc_message. This is useful
|
||||
# to allow room admins to deal with abuse quickly.
|
||||
#
|
||||
# The defaults are as shown below.
|
||||
#
|
||||
@@ -123,6 +132,10 @@ class RatelimitConfig(Config):
|
||||
# failed_attempts:
|
||||
# per_second: 0.17
|
||||
# burst_count: 3
|
||||
#
|
||||
#rc_admin_redaction:
|
||||
# per_second: 1
|
||||
# burst_count: 50
|
||||
|
||||
|
||||
# Ratelimiting settings for incoming federation
|
||||
|
||||
@@ -12,7 +12,13 @@
|
||||
# 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.
|
||||
import re
|
||||
|
||||
from synapse.python_dependencies import DependencyException, check_requirements
|
||||
from synapse.types import (
|
||||
map_username_to_mxid_localpart,
|
||||
mxid_localpart_allowed_characters,
|
||||
)
|
||||
|
||||
from ._base import Config, ConfigError
|
||||
|
||||
@@ -26,6 +32,9 @@ class SAML2Config(Config):
|
||||
if not saml2_config or not saml2_config.get("enabled", True):
|
||||
return
|
||||
|
||||
if not saml2_config.get("sp_config") and not saml2_config.get("config_path"):
|
||||
return
|
||||
|
||||
try:
|
||||
check_requirements("saml2")
|
||||
except DependencyException as e:
|
||||
@@ -33,6 +42,14 @@ class SAML2Config(Config):
|
||||
|
||||
self.saml2_enabled = True
|
||||
|
||||
self.saml2_mxid_source_attribute = saml2_config.get(
|
||||
"mxid_source_attribute", "uid"
|
||||
)
|
||||
|
||||
self.saml2_grandfathered_mxid_source_attribute = saml2_config.get(
|
||||
"grandfathered_mxid_source_attribute", "uid"
|
||||
)
|
||||
|
||||
import saml2.config
|
||||
|
||||
self.saml2_sp_config = saml2.config.SPConfig()
|
||||
@@ -48,6 +65,12 @@ class SAML2Config(Config):
|
||||
saml2_config.get("saml_session_lifetime", "5m")
|
||||
)
|
||||
|
||||
mapping = saml2_config.get("mxid_mapping", "hexencode")
|
||||
try:
|
||||
self.saml2_mxid_mapper = MXID_MAPPER_MAP[mapping]
|
||||
except KeyError:
|
||||
raise ConfigError("%s is not a known mxid_mapping" % (mapping,))
|
||||
|
||||
def _default_saml_config_dict(self):
|
||||
import saml2
|
||||
|
||||
@@ -55,6 +78,13 @@ class SAML2Config(Config):
|
||||
if public_baseurl is None:
|
||||
raise ConfigError("saml2_config requires a public_baseurl to be set")
|
||||
|
||||
required_attributes = {"uid", self.saml2_mxid_source_attribute}
|
||||
|
||||
optional_attributes = {"displayName"}
|
||||
if self.saml2_grandfathered_mxid_source_attribute:
|
||||
optional_attributes.add(self.saml2_grandfathered_mxid_source_attribute)
|
||||
optional_attributes -= required_attributes
|
||||
|
||||
metadata_url = public_baseurl + "_matrix/saml2/metadata.xml"
|
||||
response_url = public_baseurl + "_matrix/saml2/authn_response"
|
||||
return {
|
||||
@@ -66,8 +96,9 @@ class SAML2Config(Config):
|
||||
(response_url, saml2.BINDING_HTTP_POST)
|
||||
]
|
||||
},
|
||||
"required_attributes": ["uid"],
|
||||
"optional_attributes": ["mail", "surname", "givenname"],
|
||||
"required_attributes": list(required_attributes),
|
||||
"optional_attributes": list(optional_attributes),
|
||||
# "name_id_format": saml2.saml.NAMEID_FORMAT_PERSISTENT,
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -76,12 +107,13 @@ class SAML2Config(Config):
|
||||
return """\
|
||||
# Enable SAML2 for registration and login. Uses pysaml2.
|
||||
#
|
||||
# `sp_config` is the configuration for the pysaml2 Service Provider.
|
||||
# See pysaml2 docs for format of config.
|
||||
# At least one of `sp_config` or `config_path` must be set in this section to
|
||||
# enable SAML login.
|
||||
#
|
||||
# Default values will be used for the 'entityid' and 'service' settings,
|
||||
# so it is not normally necessary to specify them unless you need to
|
||||
# override them.
|
||||
# (You will probably also want to set the following options to `false` to
|
||||
# disable the regular login/registration flows:
|
||||
# * enable_registration
|
||||
# * password_config.enabled
|
||||
#
|
||||
# Once SAML support is enabled, a metadata file will be exposed at
|
||||
# https://<server>:<port>/_matrix/saml2/metadata.xml, which you may be able to
|
||||
@@ -89,52 +121,115 @@ class SAML2Config(Config):
|
||||
# the IdP to use an ACS location of
|
||||
# https://<server>:<port>/_matrix/saml2/authn_response.
|
||||
#
|
||||
#saml2_config:
|
||||
# sp_config:
|
||||
# # point this to the IdP's metadata. You can use either a local file or
|
||||
# # (preferably) a URL.
|
||||
# metadata:
|
||||
# #local: ["saml2/idp.xml"]
|
||||
# remote:
|
||||
# - url: https://our_idp/metadata.xml
|
||||
#
|
||||
# # By default, the user has to go to our login page first. If you'd like to
|
||||
# # allow IdP-initiated login, set 'allow_unsolicited: True' in a
|
||||
# # 'service.sp' section:
|
||||
# #
|
||||
# #service:
|
||||
# # sp:
|
||||
# # allow_unsolicited: True
|
||||
#
|
||||
# # The examples below are just used to generate our metadata xml, and you
|
||||
# # may well not need it, depending on your setup. Alternatively you
|
||||
# # may need a whole lot more detail - see the pysaml2 docs!
|
||||
#
|
||||
# description: ["My awesome SP", "en"]
|
||||
# name: ["Test SP", "en"]
|
||||
#
|
||||
# organization:
|
||||
# name: Example com
|
||||
# display_name:
|
||||
# - ["Example co", "en"]
|
||||
# url: "http://example.com"
|
||||
#
|
||||
# contact_person:
|
||||
# - given_name: Bob
|
||||
# sur_name: "the Sysadmin"
|
||||
# email_address": ["admin@example.com"]
|
||||
# contact_type": technical
|
||||
#
|
||||
# # Instead of putting the config inline as above, you can specify a
|
||||
# # separate pysaml2 configuration file:
|
||||
# #
|
||||
# config_path: "%(config_dir_path)s/sp_conf.py"
|
||||
#
|
||||
# # the lifetime of a SAML session. This defines how long a user has to
|
||||
# # complete the authentication process, if allow_unsolicited is unset.
|
||||
# # The default is 5 minutes.
|
||||
# #
|
||||
# # saml_session_lifetime: 5m
|
||||
saml2_config:
|
||||
# `sp_config` is the configuration for the pysaml2 Service Provider.
|
||||
# See pysaml2 docs for format of config.
|
||||
#
|
||||
# Default values will be used for the 'entityid' and 'service' settings,
|
||||
# so it is not normally necessary to specify them unless you need to
|
||||
# override them.
|
||||
#
|
||||
#sp_config:
|
||||
# # point this to the IdP's metadata. You can use either a local file or
|
||||
# # (preferably) a URL.
|
||||
# metadata:
|
||||
# #local: ["saml2/idp.xml"]
|
||||
# remote:
|
||||
# - url: https://our_idp/metadata.xml
|
||||
#
|
||||
# # By default, the user has to go to our login page first. If you'd like
|
||||
# # to allow IdP-initiated login, set 'allow_unsolicited: True' in a
|
||||
# # 'service.sp' section:
|
||||
# #
|
||||
# #service:
|
||||
# # sp:
|
||||
# # allow_unsolicited: true
|
||||
#
|
||||
# # The examples below are just used to generate our metadata xml, and you
|
||||
# # may well not need them, depending on your setup. Alternatively you
|
||||
# # may need a whole lot more detail - see the pysaml2 docs!
|
||||
#
|
||||
# description: ["My awesome SP", "en"]
|
||||
# name: ["Test SP", "en"]
|
||||
#
|
||||
# organization:
|
||||
# name: Example com
|
||||
# display_name:
|
||||
# - ["Example co", "en"]
|
||||
# url: "http://example.com"
|
||||
#
|
||||
# contact_person:
|
||||
# - given_name: Bob
|
||||
# sur_name: "the Sysadmin"
|
||||
# email_address": ["admin@example.com"]
|
||||
# contact_type": technical
|
||||
|
||||
# Instead of putting the config inline as above, you can specify a
|
||||
# separate pysaml2 configuration file:
|
||||
#
|
||||
#config_path: "%(config_dir_path)s/sp_conf.py"
|
||||
|
||||
# the lifetime of a SAML session. This defines how long a user has to
|
||||
# complete the authentication process, if allow_unsolicited is unset.
|
||||
# The default is 5 minutes.
|
||||
#
|
||||
#saml_session_lifetime: 5m
|
||||
|
||||
# The SAML attribute (after mapping via the attribute maps) to use to derive
|
||||
# the Matrix ID from. 'uid' by default.
|
||||
#
|
||||
#mxid_source_attribute: displayName
|
||||
|
||||
# The mapping system to use for mapping the saml attribute onto a matrix ID.
|
||||
# Options include:
|
||||
# * 'hexencode' (which maps unpermitted characters to '=xx')
|
||||
# * 'dotreplace' (which replaces unpermitted characters with '.').
|
||||
# * 'dotreplace_email_localpart' (truncates at the first '@' and replaces
|
||||
# unpermitted characters with '.')
|
||||
# The default is 'hexencode'.
|
||||
#
|
||||
#mxid_mapping: dotreplace
|
||||
|
||||
# In previous versions of synapse, the mapping from SAML attribute to MXID was
|
||||
# always calculated dynamically rather than stored in a table. For backwards-
|
||||
# compatibility, we will look for user_ids matching such a pattern before
|
||||
# creating a new account.
|
||||
#
|
||||
# This setting controls the SAML attribute which will be used for this
|
||||
# backwards-compatibility lookup. Typically it should be 'uid', but if the
|
||||
# attribute maps are changed, it may be necessary to change it.
|
||||
#
|
||||
# The default is 'uid'.
|
||||
#
|
||||
#grandfathered_mxid_source_attribute: upn
|
||||
""" % {
|
||||
"config_dir_path": config_dir_path
|
||||
}
|
||||
|
||||
|
||||
DOT_REPLACE_PATTERN = re.compile(
|
||||
("[^%s]" % (re.escape("".join(mxid_localpart_allowed_characters)),))
|
||||
)
|
||||
|
||||
|
||||
def dotreplace_for_mxid(username: str) -> str:
|
||||
username = username.lower()
|
||||
username = DOT_REPLACE_PATTERN.sub(".", username)
|
||||
|
||||
# regular mxids aren't allowed to start with an underscore either
|
||||
username = re.sub("^_", "", username)
|
||||
return username
|
||||
|
||||
|
||||
def dotreplace_email_localpart_for_mxid(username: str) -> str:
|
||||
pos = username.find("@")
|
||||
if pos >= 0:
|
||||
username = username[:pos]
|
||||
return dotreplace_for_mxid(username)
|
||||
|
||||
|
||||
MXID_MAPPER_MAP = {
|
||||
"hexencode": map_username_to_mxid_localpart,
|
||||
"dotreplace": dotreplace_for_mxid,
|
||||
"dotreplace_email_localpart": dotreplace_email_localpart_for_mxid,
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ class BaseHandler(object):
|
||||
self.state_handler = hs.get_state_handler()
|
||||
self.distributor = hs.get_distributor()
|
||||
self.ratelimiter = hs.get_ratelimiter()
|
||||
self.admin_redaction_ratelimiter = hs.get_admin_redaction_ratelimiter()
|
||||
self.clock = hs.get_clock()
|
||||
self.hs = hs
|
||||
|
||||
@@ -53,7 +54,7 @@ class BaseHandler(object):
|
||||
self.event_builder_factory = hs.get_event_builder_factory()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def ratelimit(self, requester, update=True):
|
||||
def ratelimit(self, requester, update=True, is_admin_redaction=False):
|
||||
"""Ratelimits requests.
|
||||
|
||||
Args:
|
||||
@@ -62,6 +63,9 @@ class BaseHandler(object):
|
||||
Set to False when doing multiple checks for one request (e.g.
|
||||
to check up front if we would reject the request), and set to
|
||||
True for the last call for a given request.
|
||||
is_admin_redaction (bool): Whether this is a room admin/moderator
|
||||
redacting an event. If so then we may apply different
|
||||
ratelimits depending on config.
|
||||
|
||||
Raises:
|
||||
LimitExceededError if the request should be ratelimited
|
||||
@@ -90,16 +94,33 @@ class BaseHandler(object):
|
||||
messages_per_second = override.messages_per_second
|
||||
burst_count = override.burst_count
|
||||
else:
|
||||
messages_per_second = self.hs.config.rc_message.per_second
|
||||
burst_count = self.hs.config.rc_message.burst_count
|
||||
# We default to different values if this is an admin redaction and
|
||||
# the config is set
|
||||
if is_admin_redaction and self.hs.config.rc_admin_redaction:
|
||||
messages_per_second = self.hs.config.rc_admin_redaction.per_second
|
||||
burst_count = self.hs.config.rc_admin_redaction.burst_count
|
||||
else:
|
||||
messages_per_second = self.hs.config.rc_message.per_second
|
||||
burst_count = self.hs.config.rc_message.burst_count
|
||||
|
||||
allowed, time_allowed = self.ratelimiter.can_do_action(
|
||||
user_id,
|
||||
time_now,
|
||||
rate_hz=messages_per_second,
|
||||
burst_count=burst_count,
|
||||
update=update,
|
||||
)
|
||||
if is_admin_redaction and self.hs.config.rc_admin_redaction:
|
||||
# If we have separate config for admin redactions we use a separate
|
||||
# ratelimiter
|
||||
allowed, time_allowed = self.admin_redaction_ratelimiter.can_do_action(
|
||||
user_id,
|
||||
time_now,
|
||||
rate_hz=messages_per_second,
|
||||
burst_count=burst_count,
|
||||
update=update,
|
||||
)
|
||||
else:
|
||||
allowed, time_allowed = self.ratelimiter.can_do_action(
|
||||
user_id,
|
||||
time_now,
|
||||
rate_hz=messages_per_second,
|
||||
burst_count=burst_count,
|
||||
update=update,
|
||||
)
|
||||
if not allowed:
|
||||
raise LimitExceededError(
|
||||
retry_after_ms=int(1000 * (time_allowed - time_now))
|
||||
|
||||
@@ -94,6 +94,25 @@ class AdminHandler(BaseHandler):
|
||||
|
||||
return ret
|
||||
|
||||
def get_user_server_admin(self, user):
|
||||
"""
|
||||
Get the admin bit on a user.
|
||||
|
||||
Args:
|
||||
user_id (UserID): the (necessarily local) user to manipulate
|
||||
"""
|
||||
return self.store.is_server_admin(user)
|
||||
|
||||
def set_user_server_admin(self, user, admin):
|
||||
"""
|
||||
Set the admin bit on a user.
|
||||
|
||||
Args:
|
||||
user_id (UserID): the (necessarily local) user to manipulate
|
||||
admin (bool): whether or not the user should be an admin of this server
|
||||
"""
|
||||
return self.store.set_server_admin(user, admin)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def export_user_data(self, user_id, writer):
|
||||
"""Write all data we have on the user to the given writer.
|
||||
|
||||
@@ -726,7 +726,27 @@ class EventCreationHandler(object):
|
||||
assert not self.config.worker_app
|
||||
|
||||
if ratelimit:
|
||||
yield self.base_handler.ratelimit(requester)
|
||||
# We check if this is a room admin redacting an event so that we
|
||||
# can apply different ratelimiting. We do this by simply checking
|
||||
# it's not a self-redaction (to avoid having to look up whether the
|
||||
# user is actually admin or not).
|
||||
is_admin_redaction = False
|
||||
if event.type == EventTypes.Redaction:
|
||||
original_event = yield self.store.get_event(
|
||||
event.redacts,
|
||||
check_redacted=False,
|
||||
get_prev_content=False,
|
||||
allow_rejected=False,
|
||||
allow_none=True,
|
||||
)
|
||||
|
||||
is_admin_redaction = (
|
||||
original_event and event.sender != original_event.sender
|
||||
)
|
||||
|
||||
yield self.base_handler.ratelimit(
|
||||
requester, is_admin_redaction=is_admin_redaction
|
||||
)
|
||||
|
||||
yield self.base_handler.maybe_kick_guest_users(event, context)
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ from saml2.client import Saml2Client
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.http.servlet import parse_string
|
||||
from synapse.rest.client.v1.login import SSOAuthHandler
|
||||
from synapse.types import UserID, map_username_to_mxid_localpart
|
||||
from synapse.util.async_helpers import Linearizer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -29,12 +31,26 @@ class SamlHandler:
|
||||
def __init__(self, hs):
|
||||
self._saml_client = Saml2Client(hs.config.saml2_sp_config)
|
||||
self._sso_auth_handler = SSOAuthHandler(hs)
|
||||
self._registration_handler = hs.get_registration_handler()
|
||||
|
||||
self._clock = hs.get_clock()
|
||||
self._datastore = hs.get_datastore()
|
||||
self._hostname = hs.hostname
|
||||
self._saml2_session_lifetime = hs.config.saml2_session_lifetime
|
||||
self._mxid_source_attribute = hs.config.saml2_mxid_source_attribute
|
||||
self._grandfathered_mxid_source_attribute = (
|
||||
hs.config.saml2_grandfathered_mxid_source_attribute
|
||||
)
|
||||
self._mxid_mapper = hs.config.saml2_mxid_mapper
|
||||
|
||||
# identifier for the external_ids table
|
||||
self._auth_provider_id = "saml"
|
||||
|
||||
# a map from saml session id to Saml2SessionData object
|
||||
self._outstanding_requests_dict = {}
|
||||
|
||||
self._clock = hs.get_clock()
|
||||
self._saml2_session_lifetime = hs.config.saml2_session_lifetime
|
||||
# a lock on the mappings
|
||||
self._mapping_lock = Linearizer(name="saml_mapping", clock=self._clock)
|
||||
|
||||
def handle_redirect_request(self, client_redirect_url):
|
||||
"""Handle an incoming request to /login/sso/redirect
|
||||
@@ -60,7 +76,7 @@ class SamlHandler:
|
||||
# this shouldn't happen!
|
||||
raise Exception("prepare_for_authenticate didn't return a Location header")
|
||||
|
||||
def handle_saml_response(self, request):
|
||||
async def handle_saml_response(self, request):
|
||||
"""Handle an incoming request to /_matrix/saml2/authn_response
|
||||
|
||||
Args:
|
||||
@@ -77,6 +93,10 @@ class SamlHandler:
|
||||
# the dict.
|
||||
self.expire_sessions()
|
||||
|
||||
user_id = await self._map_saml_response_to_user(resp_bytes)
|
||||
self._sso_auth_handler.complete_sso_login(user_id, request, relay_state)
|
||||
|
||||
async def _map_saml_response_to_user(self, resp_bytes):
|
||||
try:
|
||||
saml2_auth = self._saml_client.parse_authn_request_response(
|
||||
resp_bytes,
|
||||
@@ -91,18 +111,94 @@ class SamlHandler:
|
||||
logger.warning("SAML2 response was not signed")
|
||||
raise SynapseError(400, "SAML2 response was not signed")
|
||||
|
||||
if "uid" not in saml2_auth.ava:
|
||||
logger.info("SAML2 response: %s", saml2_auth.origxml)
|
||||
logger.info("SAML2 mapped attributes: %s", saml2_auth.ava)
|
||||
|
||||
try:
|
||||
remote_user_id = saml2_auth.ava["uid"][0]
|
||||
except KeyError:
|
||||
logger.warning("SAML2 response lacks a 'uid' attestation")
|
||||
raise SynapseError(400, "uid not in SAML2 response")
|
||||
|
||||
try:
|
||||
mxid_source = saml2_auth.ava[self._mxid_source_attribute][0]
|
||||
except KeyError:
|
||||
logger.warning(
|
||||
"SAML2 response lacks a '%s' attestation", self._mxid_source_attribute
|
||||
)
|
||||
raise SynapseError(
|
||||
400, "%s not in SAML2 response" % (self._mxid_source_attribute,)
|
||||
)
|
||||
|
||||
self._outstanding_requests_dict.pop(saml2_auth.in_response_to, None)
|
||||
|
||||
username = saml2_auth.ava["uid"][0]
|
||||
displayName = saml2_auth.ava.get("displayName", [None])[0]
|
||||
|
||||
return self._sso_auth_handler.on_successful_auth(
|
||||
username, request, relay_state, user_display_name=displayName
|
||||
)
|
||||
# mozilla-specific hack: truncate at @
|
||||
if displayName:
|
||||
pos = displayName.find("@")
|
||||
if pos >= 0:
|
||||
displayName = displayName[:pos]
|
||||
|
||||
with (await self._mapping_lock.queue(self._auth_provider_id)):
|
||||
# first of all, check if we already have a mapping for this user
|
||||
logger.info(
|
||||
"Looking for existing mapping for user %s:%s",
|
||||
self._auth_provider_id,
|
||||
remote_user_id,
|
||||
)
|
||||
registered_user_id = await self._datastore.get_user_by_external_id(
|
||||
self._auth_provider_id, remote_user_id
|
||||
)
|
||||
if registered_user_id is not None:
|
||||
logger.info("Found existing mapping %s", registered_user_id)
|
||||
return registered_user_id
|
||||
|
||||
# backwards-compatibility hack: see if there is an existing user with a
|
||||
# suitable mapping from the uid
|
||||
if (
|
||||
self._grandfathered_mxid_source_attribute
|
||||
and self._grandfathered_mxid_source_attribute in saml2_auth.ava
|
||||
):
|
||||
attrval = saml2_auth.ava[self._grandfathered_mxid_source_attribute][0]
|
||||
user_id = UserID(
|
||||
map_username_to_mxid_localpart(attrval), self._hostname
|
||||
).to_string()
|
||||
logger.info(
|
||||
"Looking for existing account based on mapped %s %s",
|
||||
self._grandfathered_mxid_source_attribute,
|
||||
user_id,
|
||||
)
|
||||
|
||||
users = await self._datastore.get_users_by_id_case_insensitive(user_id)
|
||||
if users:
|
||||
registered_user_id = list(users.keys())[0]
|
||||
logger.info("Grandfathering mapping to %s", registered_user_id)
|
||||
await self._datastore.record_user_external_id(
|
||||
self._auth_provider_id, remote_user_id, registered_user_id
|
||||
)
|
||||
return registered_user_id
|
||||
|
||||
# figure out a new mxid for this user
|
||||
base_mxid_localpart = self._mxid_mapper(mxid_source)
|
||||
|
||||
suffix = 0
|
||||
while True:
|
||||
localpart = base_mxid_localpart + (str(suffix) if suffix else "")
|
||||
if not await self._datastore.get_users_by_id_case_insensitive(
|
||||
UserID(localpart, self._hostname).to_string()
|
||||
):
|
||||
break
|
||||
suffix += 1
|
||||
logger.info("Allocating mxid for new user with localpart %s", localpart)
|
||||
|
||||
registered_user_id = await self._registration_handler.register_user(
|
||||
localpart=localpart, default_display_name=displayName
|
||||
)
|
||||
await self._datastore.record_user_external_id(
|
||||
self._auth_provider_id, remote_user_id, registered_user_id
|
||||
)
|
||||
return registered_user_id
|
||||
|
||||
def expire_sessions(self):
|
||||
expire_before = self._clock.time_msec() - self._saml2_session_lifetime
|
||||
|
||||
@@ -43,6 +43,7 @@ from synapse.rest.admin._base import (
|
||||
)
|
||||
from synapse.rest.admin.media import register_servlets_for_media_repo
|
||||
from synapse.rest.admin.server_notice_servlet import SendServerNoticeServlet
|
||||
from synapse.rest.admin.users import UserAdminServlet
|
||||
from synapse.types import UserID, create_requester
|
||||
from synapse.util.versionstring import get_version_string
|
||||
|
||||
@@ -50,7 +51,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UsersRestServlet(RestServlet):
|
||||
PATTERNS = historical_admin_path_patterns("/users/(?P<user_id>[^/]*)")
|
||||
PATTERNS = historical_admin_path_patterns("/users/(?P<user_id>[^/]*)$")
|
||||
|
||||
def __init__(self, hs):
|
||||
self.hs = hs
|
||||
@@ -740,6 +741,7 @@ def register_servlets(hs, http_server):
|
||||
register_servlets_for_client_rest_resource(hs, http_server)
|
||||
SendServerNoticeServlet(hs).register(http_server)
|
||||
VersionServlet(hs).register(http_server)
|
||||
UserAdminServlet(hs).register(http_server)
|
||||
|
||||
|
||||
def register_servlets_for_client_rest_resource(hs, http_server):
|
||||
|
||||
100
synapse/rest/admin/users.py
Normal file
100
synapse/rest/admin/users.py
Normal file
@@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
#
|
||||
# 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.
|
||||
import re
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.http.servlet import (
|
||||
RestServlet,
|
||||
assert_params_in_dict,
|
||||
parse_json_object_from_request,
|
||||
)
|
||||
from synapse.rest.admin import assert_requester_is_admin, assert_user_is_admin
|
||||
from synapse.types import UserID
|
||||
|
||||
|
||||
class UserAdminServlet(RestServlet):
|
||||
"""
|
||||
Get or set whether or not a user is a server administrator.
|
||||
|
||||
Note that only local users can be server administrators, and that an
|
||||
administrator may not demote themselves.
|
||||
|
||||
Only server administrators can use this API.
|
||||
|
||||
Examples:
|
||||
* Get
|
||||
GET /_synapse/admin/v1/users/@nonadmin:example.com/admin
|
||||
response on success:
|
||||
{
|
||||
"admin": false
|
||||
}
|
||||
* Set
|
||||
PUT /_synapse/admin/v1/users/@reivilibre:librepush.net/admin
|
||||
request body:
|
||||
{
|
||||
"admin": true
|
||||
}
|
||||
response on success:
|
||||
{}
|
||||
"""
|
||||
|
||||
PATTERNS = (re.compile("^/_synapse/admin/v1/users/(?P<user_id>@[^/]*)/admin$"),)
|
||||
|
||||
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):
|
||||
yield assert_requester_is_admin(self.auth, request)
|
||||
|
||||
target_user = UserID.from_string(user_id)
|
||||
|
||||
if not self.hs.is_mine(target_user):
|
||||
raise SynapseError(400, "Only local users can be admins of this homeserver")
|
||||
|
||||
is_admin = yield self.handlers.admin_handler.get_user_server_admin(target_user)
|
||||
is_admin = bool(is_admin)
|
||||
|
||||
return (200, {"admin": is_admin})
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_PUT(self, request, user_id):
|
||||
requester = yield self.auth.get_user_by_req(request)
|
||||
yield assert_user_is_admin(self.auth, requester.user)
|
||||
auth_user = requester.user
|
||||
|
||||
target_user = UserID.from_string(user_id)
|
||||
|
||||
body = parse_json_object_from_request(request)
|
||||
|
||||
assert_params_in_dict(body, ["admin"])
|
||||
|
||||
if not self.hs.is_mine(target_user):
|
||||
raise SynapseError(400, "Only local users can be admins of this homeserver")
|
||||
|
||||
set_admin_to = bool(body["admin"])
|
||||
|
||||
if target_user == auth_user and not set_admin_to:
|
||||
raise SynapseError(400, "You may not demote yourself.")
|
||||
|
||||
yield self.handlers.admin_handler.set_user_server_admin(
|
||||
target_user, set_admin_to
|
||||
)
|
||||
|
||||
return (200, {})
|
||||
@@ -29,6 +29,7 @@ from synapse.http.servlet import (
|
||||
parse_json_object_from_request,
|
||||
parse_string,
|
||||
)
|
||||
from synapse.http.site import SynapseRequest
|
||||
from synapse.rest.client.v2_alpha._base import client_patterns
|
||||
from synapse.rest.well_known import WellKnownBuilder
|
||||
from synapse.types import UserID, map_username_to_mxid_localpart
|
||||
@@ -507,6 +508,19 @@ class SSOAuthHandler(object):
|
||||
localpart=localpart, default_display_name=user_display_name
|
||||
)
|
||||
|
||||
self.complete_sso_login(registered_user_id, request, client_redirect_url)
|
||||
|
||||
def complete_sso_login(
|
||||
self, registered_user_id: str, request: SynapseRequest, client_redirect_url: str
|
||||
):
|
||||
"""Having figured out a mxid for this user, complete the HTTP request
|
||||
|
||||
Args:
|
||||
registered_user_id:
|
||||
request:
|
||||
client_redirect_url:
|
||||
"""
|
||||
|
||||
login_token = self._macaroon_gen.generate_short_term_login_token(
|
||||
registered_user_id
|
||||
)
|
||||
|
||||
@@ -221,6 +221,7 @@ class HomeServer(object):
|
||||
self.clock = Clock(reactor)
|
||||
self.distributor = Distributor()
|
||||
self.ratelimiter = Ratelimiter()
|
||||
self.admin_redaction_ratelimiter = Ratelimiter()
|
||||
self.registration_ratelimiter = Ratelimiter()
|
||||
|
||||
self.datastore = None
|
||||
@@ -279,6 +280,9 @@ class HomeServer(object):
|
||||
def get_registration_ratelimiter(self):
|
||||
return self.registration_ratelimiter
|
||||
|
||||
def get_admin_redaction_ratelimiter(self):
|
||||
return self.admin_redaction_ratelimiter
|
||||
|
||||
def build_federation_client(self):
|
||||
return FederationClient(self)
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ var show_login = function() {
|
||||
$("#sso_flow").show();
|
||||
}
|
||||
|
||||
if (!matrixLogin.serverAcceptsPassword && !matrixLogin.serverAcceptsCas) {
|
||||
if (!matrixLogin.serverAcceptsPassword && !matrixLogin.serverAcceptsCas && !matrixLogin.serverAcceptsSso) {
|
||||
$("#no_login_types").show();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ from six import iterkeys
|
||||
from six.moves import range
|
||||
|
||||
from twisted.internet import defer
|
||||
from twisted.internet.defer import Deferred
|
||||
|
||||
from synapse.api.constants import UserTypes
|
||||
from synapse.api.errors import Codes, StoreError, ThreepidValidationError
|
||||
@@ -272,6 +273,14 @@ class RegistrationWorkerStore(SQLBaseStore):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def is_server_admin(self, user):
|
||||
"""Determines if a user is an admin of this homeserver.
|
||||
|
||||
Args:
|
||||
user (UserID): user ID of the user to test
|
||||
|
||||
Returns (bool):
|
||||
true iff the user is a server admin, false otherwise.
|
||||
"""
|
||||
res = yield self._simple_select_one_onecol(
|
||||
table="users",
|
||||
keyvalues={"name": user.to_string()},
|
||||
@@ -282,6 +291,21 @@ class RegistrationWorkerStore(SQLBaseStore):
|
||||
|
||||
return res if res else False
|
||||
|
||||
def set_server_admin(self, user, admin):
|
||||
"""Sets whether a user is an admin of this homeserver.
|
||||
|
||||
Args:
|
||||
user (UserID): user ID of the user to test
|
||||
admin (bool): true iff the user is to be a server admin,
|
||||
false otherwise.
|
||||
"""
|
||||
return self._simple_update_one(
|
||||
table="users",
|
||||
keyvalues={"name": user.to_string()},
|
||||
updatevalues={"admin": 1 if admin else 0},
|
||||
desc="set_server_admin",
|
||||
)
|
||||
|
||||
def _query_for_auth(self, txn, token):
|
||||
sql = (
|
||||
"SELECT users.name, users.is_guest, access_tokens.id as token_id,"
|
||||
@@ -337,6 +361,26 @@ class RegistrationWorkerStore(SQLBaseStore):
|
||||
|
||||
return self.runInteraction("get_users_by_id_case_insensitive", f)
|
||||
|
||||
async def get_user_by_external_id(
|
||||
self, auth_provider: str, external_id: str
|
||||
) -> str:
|
||||
"""Look up a user by their external auth id
|
||||
|
||||
Args:
|
||||
auth_provider: identifier for the remote auth provider
|
||||
external_id: id on that system
|
||||
|
||||
Returns:
|
||||
str|None: the mxid of the user, or None if they are not known
|
||||
"""
|
||||
return await self._simple_select_one_onecol(
|
||||
table="user_external_ids",
|
||||
keyvalues={"auth_provider": auth_provider, "external_id": external_id},
|
||||
retcol="user_id",
|
||||
allow_none=True,
|
||||
desc="get_user_by_external_id",
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def count_all_users(self):
|
||||
"""Counts all users registered on the homeserver."""
|
||||
@@ -848,6 +892,26 @@ class RegistrationStore(
|
||||
self._invalidate_cache_and_stream(txn, self.get_user_by_id, (user_id,))
|
||||
txn.call_after(self.is_guest.invalidate, (user_id,))
|
||||
|
||||
def record_user_external_id(
|
||||
self, auth_provider: str, external_id: str, user_id: str
|
||||
) -> Deferred:
|
||||
"""Record a mapping from an external user id to a mxid
|
||||
|
||||
Args:
|
||||
auth_provider: identifier for the remote auth provider
|
||||
external_id: id on that system
|
||||
user_id: complete mxid that it is mapped to
|
||||
"""
|
||||
return self._simple_insert(
|
||||
table="user_external_ids",
|
||||
values={
|
||||
"auth_provider": auth_provider,
|
||||
"external_id": external_id,
|
||||
"user_id": user_id,
|
||||
},
|
||||
desc="record_user_external_id",
|
||||
)
|
||||
|
||||
def user_set_password_hash(self, user_id, password_hash):
|
||||
"""
|
||||
NB. This does *not* evict any cache because the one use for this
|
||||
|
||||
24
synapse/storage/schema/delta/56/user_external_ids.sql
Normal file
24
synapse/storage/schema/delta/56/user_external_ids.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
/* Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* a table which records mappings from external auth providers to mxids
|
||||
*/
|
||||
CREATE TABLE IF NOT EXISTS user_external_ids (
|
||||
auth_provider TEXT NOT NULL,
|
||||
external_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
UNIQUE (auth_provider, external_id)
|
||||
);
|
||||
@@ -30,6 +30,14 @@ class RedactionsTestCase(HomeserverTestCase):
|
||||
sync.register_servlets,
|
||||
]
|
||||
|
||||
def make_homeserver(self, reactor, clock):
|
||||
config = self.default_config()
|
||||
|
||||
config["rc_message"] = {"per_second": 0.2, "burst_count": 10}
|
||||
config["rc_admin_redaction"] = {"per_second": 1, "burst_count": 100}
|
||||
|
||||
return self.setup_test_homeserver(config=config)
|
||||
|
||||
def prepare(self, reactor, clock, hs):
|
||||
# register a couple of users
|
||||
self.mod_user_id = self.register_user("user1", "pass")
|
||||
@@ -177,3 +185,20 @@ class RedactionsTestCase(HomeserverTestCase):
|
||||
self._redact_event(
|
||||
self.other_access_token, self.room_id, create_event_id, expect_code=403
|
||||
)
|
||||
|
||||
def test_redact_event_as_moderator_ratelimit(self):
|
||||
"""Tests that the correct ratelimiting is applied to redactions
|
||||
"""
|
||||
|
||||
message_ids = []
|
||||
# as a regular user, send messages to redact
|
||||
for _ in range(20):
|
||||
b = self.helper.send(room_id=self.room_id, tok=self.other_access_token)
|
||||
message_ids.append(b["event_id"])
|
||||
self.reactor.advance(10) # To get around ratelimits
|
||||
|
||||
# as the moderator, send a bunch of redactions
|
||||
for msg_id in message_ids:
|
||||
# These should all succeed, even though this would be denied by
|
||||
# the standard message ratelimiter
|
||||
self._redact_event(self.mod_access_token, self.room_id, msg_id)
|
||||
|
||||
Reference in New Issue
Block a user