mirror of
https://mau.dev/maunium/synapse.git
synced 2024-10-01 01:36:05 -04:00
Merge pull request #5214 from matrix-org/babolivier/password-policy
Allow server admins to define and enforce a password policy (MSC2000)
This commit is contained in:
commit
64365fcbdd
1
changelog.d/5214.feature
Normal file
1
changelog.d/5214.feature
Normal file
@ -0,0 +1 @@
|
||||
Allow server admins to define and enforce a password policy (MSC2000).
|
@ -1056,6 +1056,36 @@ password_config:
|
||||
#
|
||||
#pepper: "EVEN_MORE_SECRET"
|
||||
|
||||
# Define and enforce a password policy. Each parameter is optional, boolean
|
||||
# parameters default to 'false' and integer parameters default to 0.
|
||||
# This is an early implementation of MSC2000.
|
||||
#
|
||||
#policy:
|
||||
# Whether to enforce the password policy.
|
||||
#
|
||||
#enabled: true
|
||||
|
||||
# Minimum accepted length for a password.
|
||||
#
|
||||
#minimum_length: 15
|
||||
|
||||
# Whether a password must contain at least one digit.
|
||||
#
|
||||
#require_digit: true
|
||||
|
||||
# Whether a password must contain at least one symbol.
|
||||
# A symbol is any character that's not a number or a letter.
|
||||
#
|
||||
#require_symbol: true
|
||||
|
||||
# Whether a password must contain at least one lowercase letter.
|
||||
#
|
||||
#require_lowercase: true
|
||||
|
||||
# Whether a password must contain at least one lowercase letter.
|
||||
#
|
||||
#require_uppercase: true
|
||||
|
||||
|
||||
|
||||
# Enable sending emails for notification events or expiry notices
|
||||
|
@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# Copyright 2018 New Vector Ltd
|
||||
# Copyright 2017-2018 New Vector Ltd
|
||||
# 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.
|
||||
@ -61,6 +62,13 @@ class Codes(object):
|
||||
INCOMPATIBLE_ROOM_VERSION = "M_INCOMPATIBLE_ROOM_VERSION"
|
||||
WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
|
||||
EXPIRED_ACCOUNT = "ORG_MATRIX_EXPIRED_ACCOUNT"
|
||||
PASSWORD_TOO_SHORT = "M_PASSWORD_TOO_SHORT"
|
||||
PASSWORD_NO_DIGIT = "M_PASSWORD_NO_DIGIT"
|
||||
PASSWORD_NO_UPPERCASE = "M_PASSWORD_NO_UPPERCASE"
|
||||
PASSWORD_NO_LOWERCASE = "M_PASSWORD_NO_LOWERCASE"
|
||||
PASSWORD_NO_SYMBOL = "M_PASSWORD_NO_SYMBOL"
|
||||
PASSWORD_IN_DICTIONARY = "M_PASSWORD_IN_DICTIONARY"
|
||||
WEAK_PASSWORD = "M_WEAK_PASSWORD"
|
||||
|
||||
|
||||
class CodeMessageException(RuntimeError):
|
||||
@ -349,6 +357,22 @@ class IncompatibleRoomVersionError(SynapseError):
|
||||
)
|
||||
|
||||
|
||||
class PasswordRefusedError(SynapseError):
|
||||
"""A password has been refused, either during password reset/change or registration.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
msg="This password doesn't comply with the server's policy",
|
||||
errcode=Codes.WEAK_PASSWORD,
|
||||
):
|
||||
super(PasswordRefusedError, self).__init__(
|
||||
code=400,
|
||||
msg=msg,
|
||||
errcode=errcode,
|
||||
)
|
||||
|
||||
|
||||
class RequestSendFailed(RuntimeError):
|
||||
"""Sending a HTTP request over federation failed due to not being able to
|
||||
talk to the remote server for some reason.
|
||||
|
@ -1,5 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2015, 2016 OpenMarket Ltd
|
||||
# Copyright 2015-2016 OpenMarket Ltd
|
||||
# Copyright 2017-2018 New Vector Ltd
|
||||
# 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.
|
||||
@ -28,6 +30,10 @@ class PasswordConfig(Config):
|
||||
self.password_enabled = password_config.get("enabled", True)
|
||||
self.password_pepper = password_config.get("pepper", "")
|
||||
|
||||
# Password policy
|
||||
self.password_policy = password_config.get("policy", {})
|
||||
self.password_policy_enabled = self.password_policy.pop("enabled", False)
|
||||
|
||||
def default_config(self, config_dir_path, server_name, **kwargs):
|
||||
return """\
|
||||
password_config:
|
||||
@ -39,4 +45,34 @@ class PasswordConfig(Config):
|
||||
# DO NOT CHANGE THIS AFTER INITIAL SETUP!
|
||||
#
|
||||
#pepper: "EVEN_MORE_SECRET"
|
||||
|
||||
# Define and enforce a password policy. Each parameter is optional, boolean
|
||||
# parameters default to 'false' and integer parameters default to 0.
|
||||
# This is an early implementation of MSC2000.
|
||||
#
|
||||
#policy:
|
||||
# Whether to enforce the password policy.
|
||||
#
|
||||
#enabled: true
|
||||
|
||||
# Minimum accepted length for a password.
|
||||
#
|
||||
#minimum_length: 15
|
||||
|
||||
# Whether a password must contain at least one digit.
|
||||
#
|
||||
#require_digit: true
|
||||
|
||||
# Whether a password must contain at least one symbol.
|
||||
# A symbol is any character that's not a number or a letter.
|
||||
#
|
||||
#require_symbol: true
|
||||
|
||||
# Whether a password must contain at least one lowercase letter.
|
||||
#
|
||||
#require_lowercase: true
|
||||
|
||||
# Whether a password must contain at least one lowercase letter.
|
||||
#
|
||||
#require_uppercase: true
|
||||
"""
|
||||
|
93
synapse/handlers/password_policy.py
Normal file
93
synapse/handlers/password_policy.py
Normal file
@ -0,0 +1,93 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2019 New Vector Ltd
|
||||
# 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 logging
|
||||
import re
|
||||
|
||||
from synapse.api.errors import Codes, PasswordRefusedError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PasswordPolicyHandler(object):
|
||||
def __init__(self, hs):
|
||||
self.policy = hs.config.password_policy
|
||||
self.enabled = hs.config.password_policy_enabled
|
||||
|
||||
# Regexps for the spec'd policy parameters.
|
||||
self.regexp_digit = re.compile("[0-9]")
|
||||
self.regexp_symbol = re.compile("[^a-zA-Z0-9]")
|
||||
self.regexp_uppercase = re.compile("[A-Z]")
|
||||
self.regexp_lowercase = re.compile("[a-z]")
|
||||
|
||||
def validate_password(self, password):
|
||||
"""Checks whether a given password complies with the server's policy.
|
||||
|
||||
Args:
|
||||
password (str): The password to check against the server's policy.
|
||||
|
||||
Raises:
|
||||
PasswordRefusedError: The password doesn't comply with the server's policy.
|
||||
"""
|
||||
|
||||
if not self.enabled:
|
||||
return
|
||||
|
||||
minimum_accepted_length = self.policy.get("minimum_length", 0)
|
||||
if len(password) < minimum_accepted_length:
|
||||
raise PasswordRefusedError(
|
||||
msg=(
|
||||
"The password must be at least %d characters long"
|
||||
% minimum_accepted_length
|
||||
),
|
||||
errcode=Codes.PASSWORD_TOO_SHORT,
|
||||
)
|
||||
|
||||
if (
|
||||
self.policy.get("require_digit", False) and
|
||||
self.regexp_digit.search(password) is None
|
||||
):
|
||||
raise PasswordRefusedError(
|
||||
msg="The password must include at least one digit",
|
||||
errcode=Codes.PASSWORD_NO_DIGIT,
|
||||
)
|
||||
|
||||
if (
|
||||
self.policy.get("require_symbol", False) and
|
||||
self.regexp_symbol.search(password) is None
|
||||
):
|
||||
raise PasswordRefusedError(
|
||||
msg="The password must include at least one symbol",
|
||||
errcode=Codes.PASSWORD_NO_SYMBOL,
|
||||
)
|
||||
|
||||
if (
|
||||
self.policy.get("require_uppercase", False) and
|
||||
self.regexp_uppercase.search(password) is None
|
||||
):
|
||||
raise PasswordRefusedError(
|
||||
msg="The password must include at least one uppercase letter",
|
||||
errcode=Codes.PASSWORD_NO_UPPERCASE,
|
||||
)
|
||||
|
||||
if (
|
||||
self.policy.get("require_lowercase", False) and
|
||||
self.regexp_lowercase.search(password) is None
|
||||
):
|
||||
raise PasswordRefusedError(
|
||||
msg="The password must include at least one lowercase letter",
|
||||
errcode=Codes.PASSWORD_NO_LOWERCASE,
|
||||
)
|
@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2017 New Vector Ltd
|
||||
# Copyright 2017-2018 New Vector Ltd
|
||||
# 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.
|
||||
@ -29,9 +30,12 @@ class SetPasswordHandler(BaseHandler):
|
||||
super(SetPasswordHandler, self).__init__(hs)
|
||||
self._auth_handler = hs.get_auth_handler()
|
||||
self._device_handler = hs.get_device_handler()
|
||||
self._password_policy_handler = hs.get_password_policy_handler()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def set_password(self, user_id, newpassword, requester=None):
|
||||
self._password_policy_handler.validate_password(newpassword)
|
||||
|
||||
password_hash = yield self._auth_handler.hash(newpassword)
|
||||
|
||||
except_device_id = requester.device_id if requester else None
|
||||
|
@ -41,6 +41,7 @@ from synapse.rest.client.v2_alpha import (
|
||||
keys,
|
||||
notifications,
|
||||
openid,
|
||||
password_policy,
|
||||
read_marker,
|
||||
receipts,
|
||||
register,
|
||||
@ -115,6 +116,7 @@ class ClientRestResource(JsonResource):
|
||||
room_upgrade_rest_servlet.register_servlets(hs, client_resource)
|
||||
capabilities.register_servlets(hs, client_resource)
|
||||
account_validity.register_servlets(hs, client_resource)
|
||||
password_policy.register_servlets(hs, client_resource)
|
||||
|
||||
# moving to /_synapse/admin
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource(
|
||||
|
58
synapse/rest/client/v2_alpha/password_policy.py
Normal file
58
synapse/rest/client/v2_alpha/password_policy.py
Normal file
@ -0,0 +1,58 @@
|
||||
# -*- 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 logging
|
||||
|
||||
from synapse.http.servlet import RestServlet
|
||||
|
||||
from ._base import client_v2_patterns
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PasswordPolicyServlet(RestServlet):
|
||||
PATTERNS = client_v2_patterns("/password_policy$")
|
||||
|
||||
def __init__(self, hs):
|
||||
"""
|
||||
Args:
|
||||
hs (synapse.server.HomeServer): server
|
||||
"""
|
||||
super(PasswordPolicyServlet, self).__init__()
|
||||
|
||||
self.policy = hs.config.password_policy
|
||||
self.enabled = hs.config.password_policy_enabled
|
||||
|
||||
def on_GET(self, request):
|
||||
if not self.enabled or not self.policy:
|
||||
return (200, {})
|
||||
|
||||
policy = {}
|
||||
|
||||
for param in [
|
||||
"minimum_length",
|
||||
"require_digit",
|
||||
"require_symbol",
|
||||
"require_lowercase",
|
||||
"require_uppercase",
|
||||
]:
|
||||
if param in self.policy:
|
||||
policy["m.%s" % param] = self.policy[param]
|
||||
|
||||
return (200, policy)
|
||||
|
||||
|
||||
def register_servlets(hs, http_server):
|
||||
PasswordPolicyServlet(hs).register(http_server)
|
@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2015 - 2016 OpenMarket Ltd
|
||||
# Copyright 2017 Vector Creations Ltd
|
||||
# Copyright 2015-2016 OpenMarket Ltd
|
||||
# Copyright 2017-2018 New Vector Ltd
|
||||
# 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.
|
||||
@ -202,6 +203,7 @@ class RegisterRestServlet(RestServlet):
|
||||
self.room_member_handler = hs.get_room_member_handler()
|
||||
self.macaroon_gen = hs.get_macaroon_generator()
|
||||
self.ratelimiter = hs.get_registration_ratelimiter()
|
||||
self.password_policy_handler = hs.get_password_policy_handler()
|
||||
self.clock = hs.get_clock()
|
||||
|
||||
@interactive_auth_handler
|
||||
@ -245,6 +247,7 @@ class RegisterRestServlet(RestServlet):
|
||||
if (not isinstance(body['password'], string_types) or
|
||||
len(body['password']) > 512):
|
||||
raise SynapseError(400, "Invalid password")
|
||||
self.password_policy_handler.validate_password(body['password'])
|
||||
desired_password = body["password"]
|
||||
|
||||
desired_username = None
|
||||
|
@ -1,5 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014-2016 OpenMarket Ltd
|
||||
# Copyright 2017-2018 New Vector Ltd
|
||||
# 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.
|
||||
@ -62,6 +64,7 @@ from synapse.handlers.groups_local import GroupsLocalHandler
|
||||
from synapse.handlers.initial_sync import InitialSyncHandler
|
||||
from synapse.handlers.message import EventCreationHandler, MessageHandler
|
||||
from synapse.handlers.pagination import PaginationHandler
|
||||
from synapse.handlers.password_policy import PasswordPolicyHandler
|
||||
from synapse.handlers.presence import PresenceHandler
|
||||
from synapse.handlers.profile import BaseProfileHandler, MasterProfileHandler
|
||||
from synapse.handlers.read_marker import ReadMarkerHandler
|
||||
@ -187,6 +190,7 @@ class HomeServer(object):
|
||||
'registration_handler',
|
||||
'account_validity_handler',
|
||||
'event_client_serializer',
|
||||
'password_policy_handler',
|
||||
]
|
||||
|
||||
REQUIRED_ON_MASTER_STARTUP = [
|
||||
@ -516,6 +520,9 @@ class HomeServer(object):
|
||||
def build_event_client_serializer(self):
|
||||
return EventClientSerializer(self)
|
||||
|
||||
def build_password_policy_handler(self):
|
||||
return PasswordPolicyHandler(self)
|
||||
|
||||
def remove_pusher(self, app_id, push_key, user_id):
|
||||
return self.get_pusherpool().remove_pusher(app_id, push_key, user_id)
|
||||
|
||||
|
181
tests/rest/client/v2_alpha/test_password_policy.py
Normal file
181
tests/rest/client/v2_alpha/test_password_policy.py
Normal file
@ -0,0 +1,181 @@
|
||||
# -*- 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 json
|
||||
|
||||
from synapse.api.constants import LoginType
|
||||
from synapse.api.errors import Codes
|
||||
from synapse.rest import admin
|
||||
from synapse.rest.client.v1 import login
|
||||
from synapse.rest.client.v2_alpha import account, password_policy, register
|
||||
|
||||
from tests import unittest
|
||||
|
||||
|
||||
class PasswordPolicyTestCase(unittest.HomeserverTestCase):
|
||||
"""Tests the password policy feature and its compliance with MSC2000.
|
||||
|
||||
When validating a password, Synapse does the necessary checks in this order:
|
||||
|
||||
1. Password is long enough
|
||||
2. Password contains digit(s)
|
||||
3. Password contains symbol(s)
|
||||
4. Password contains uppercase letter(s)
|
||||
5. Password contains lowercase letter(s)
|
||||
|
||||
Therefore, each test in this test case that tests whether a password triggers the
|
||||
right error code to be returned provides a password good enough to pass the previous
|
||||
steps but not the one it's testing (nor any step that comes after).
|
||||
"""
|
||||
|
||||
servlets = [
|
||||
admin.register_servlets_for_client_rest_resource,
|
||||
login.register_servlets,
|
||||
register.register_servlets,
|
||||
password_policy.register_servlets,
|
||||
account.register_servlets,
|
||||
]
|
||||
|
||||
def make_homeserver(self, reactor, clock):
|
||||
self.register_url = "/_matrix/client/r0/register"
|
||||
self.policy = {
|
||||
"enabled": True,
|
||||
"minimum_length": 10,
|
||||
"require_digit": True,
|
||||
"require_symbol": True,
|
||||
"require_lowercase": True,
|
||||
"require_uppercase": True,
|
||||
}
|
||||
|
||||
config = self.default_config()
|
||||
config["password_config"] = {
|
||||
"policy": self.policy,
|
||||
}
|
||||
|
||||
hs = self.setup_test_homeserver(config=config)
|
||||
return hs
|
||||
|
||||
def test_get_policy(self):
|
||||
"""Tests if the /password_policy endpoint returns the configured policy."""
|
||||
|
||||
request, channel = self.make_request("GET", "/_matrix/client/r0/password_policy")
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(channel.code, 200, channel.result)
|
||||
self.assertEqual(channel.json_body, {
|
||||
"m.minimum_length": 10,
|
||||
"m.require_digit": True,
|
||||
"m.require_symbol": True,
|
||||
"m.require_lowercase": True,
|
||||
"m.require_uppercase": True,
|
||||
}, channel.result)
|
||||
|
||||
def test_password_too_short(self):
|
||||
request_data = json.dumps({"username": "kermit", "password": "shorty"})
|
||||
request, channel = self.make_request("POST", self.register_url, request_data)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(channel.code, 400, channel.result)
|
||||
self.assertEqual(
|
||||
channel.json_body["errcode"],
|
||||
Codes.PASSWORD_TOO_SHORT,
|
||||
channel.result,
|
||||
)
|
||||
|
||||
def test_password_no_digit(self):
|
||||
request_data = json.dumps({"username": "kermit", "password": "longerpassword"})
|
||||
request, channel = self.make_request("POST", self.register_url, request_data)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(channel.code, 400, channel.result)
|
||||
self.assertEqual(
|
||||
channel.json_body["errcode"],
|
||||
Codes.PASSWORD_NO_DIGIT,
|
||||
channel.result,
|
||||
)
|
||||
|
||||
def test_password_no_symbol(self):
|
||||
request_data = json.dumps({"username": "kermit", "password": "l0ngerpassword"})
|
||||
request, channel = self.make_request("POST", self.register_url, request_data)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(channel.code, 400, channel.result)
|
||||
self.assertEqual(
|
||||
channel.json_body["errcode"],
|
||||
Codes.PASSWORD_NO_SYMBOL,
|
||||
channel.result,
|
||||
)
|
||||
|
||||
def test_password_no_uppercase(self):
|
||||
request_data = json.dumps({"username": "kermit", "password": "l0ngerpassword!"})
|
||||
request, channel = self.make_request("POST", self.register_url, request_data)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(channel.code, 400, channel.result)
|
||||
self.assertEqual(
|
||||
channel.json_body["errcode"],
|
||||
Codes.PASSWORD_NO_UPPERCASE,
|
||||
channel.result,
|
||||
)
|
||||
|
||||
def test_password_no_lowercase(self):
|
||||
request_data = json.dumps({"username": "kermit", "password": "L0NGERPASSWORD!"})
|
||||
request, channel = self.make_request("POST", self.register_url, request_data)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(channel.code, 400, channel.result)
|
||||
self.assertEqual(
|
||||
channel.json_body["errcode"],
|
||||
Codes.PASSWORD_NO_LOWERCASE,
|
||||
channel.result,
|
||||
)
|
||||
|
||||
def test_password_compliant(self):
|
||||
request_data = json.dumps({"username": "kermit", "password": "L0ngerpassword!"})
|
||||
request, channel = self.make_request("POST", self.register_url, request_data)
|
||||
self.render(request)
|
||||
|
||||
# Getting a 401 here means the password has passed validation and the server has
|
||||
# responded with a list of registration flows.
|
||||
self.assertEqual(channel.code, 401, channel.result)
|
||||
|
||||
def test_password_change(self):
|
||||
"""This doesn't test every possible use case, only that hitting /account/password
|
||||
triggers the password validation code.
|
||||
"""
|
||||
compliant_password = "C0mpl!antpassword"
|
||||
not_compliant_password = "notcompliantpassword"
|
||||
|
||||
user_id = self.register_user("kermit", compliant_password)
|
||||
tok = self.login("kermit", compliant_password)
|
||||
|
||||
request_data = json.dumps({
|
||||
"new_password": not_compliant_password,
|
||||
"auth": {
|
||||
"password": compliant_password,
|
||||
"type": LoginType.PASSWORD,
|
||||
"user": user_id,
|
||||
}
|
||||
})
|
||||
request, channel = self.make_request(
|
||||
"POST",
|
||||
"/_matrix/client/r0/account/password",
|
||||
request_data,
|
||||
access_token=tok,
|
||||
)
|
||||
self.render(request)
|
||||
|
||||
self.assertEqual(channel.code, 400, channel.result)
|
||||
self.assertEqual(channel.json_body["errcode"], Codes.PASSWORD_NO_DIGIT)
|
Loading…
Reference in New Issue
Block a user