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

Allow server admins to define and enforce a password policy (MSC2000) #5214

Merged
merged 7 commits into from
May 22, 2019
Merged
Show file tree
Hide file tree
Changes from 6 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/5214.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow server admins to define and enforce a password policy (MSC2000).
30 changes: 30 additions & 0 deletions docs/sample_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,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
Expand Down
26 changes: 25 additions & 1 deletion synapse/api/errors.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -349,6 +357,22 @@ def error_dict(self):
)


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.
Expand Down
38 changes: 37 additions & 1 deletion synapse/config/password.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -28,6 +30,10 @@ def read_config(self, 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:
Expand All @@ -39,4 +45,34 @@ def default_config(self, config_dir_path, server_name, **kwargs):
# 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 changes: 93 additions & 0 deletions synapse/handlers/password_policy.py
Original file line number Diff line number Diff line change
@@ -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,
)
6 changes: 5 additions & 1 deletion synapse/handlers/set_password.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -29,9 +30,12 @@ def __init__(self, hs):
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
Expand Down
2 changes: 2 additions & 0 deletions synapse/rest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
keys,
notifications,
openid,
password_policy,
read_marker,
receipts,
register,
Expand Down Expand Up @@ -115,6 +116,7 @@ def register_servlets(client_resource, hs):
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(
Expand Down
58 changes: 58 additions & 0 deletions synapse/rest/client/v2_alpha/password_policy.py
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 5 additions & 2 deletions synapse/rest/client/v2_alpha/register.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -200,6 +201,7 @@ def __init__(self, hs):
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
Expand Down Expand Up @@ -243,6 +245,7 @@ def on_POST(self, request):
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
Expand Down
7 changes: 7 additions & 0 deletions synapse/server.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -62,6 +64,7 @@
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
Expand Down Expand Up @@ -187,6 +190,7 @@ def build_DEPENDENCY(self)
'registration_handler',
'account_validity_handler',
'event_client_serializer',
'password_policy_handler',
]

REQUIRED_ON_MASTER_STARTUP = [
Expand Down Expand Up @@ -516,6 +520,9 @@ def build_account_validity_handler(self):
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)

Expand Down
Loading