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

Commit

Permalink
create support user (#4141)
Browse files Browse the repository at this point in the history
Allow for the creation of a support user.

A support user can access the server, join rooms, interact with other users, but does not appear in the user directory nor does it contribute to monthly active user limits.
  • Loading branch information
neilisfragile authored Dec 14, 2018
1 parent e93a0eb commit d2f7c4e
Show file tree
Hide file tree
Showing 20 changed files with 371 additions and 47 deletions.
1 change: 1 addition & 0 deletions changelog.d/4141.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Special-case a support user for use in verifying behaviour of a given server. The support user does not appear in user directory or monthly active user counts.
11 changes: 7 additions & 4 deletions docs/admin_api/register_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ As an example::
}

The MAC is the hex digest output of the HMAC-SHA1 algorithm, with the key being
the shared secret and the content being the nonce, user, password, and either
the string "admin" or "notadmin", each separated by NULs. For an example of
generation in Python::
the shared secret and the content being the nonce, user, password, either the
string "admin" or "notadmin", and optionally the user_type
each separated by NULs. For an example of generation in Python::

import hmac, hashlib

def generate_mac(nonce, user, password, admin=False):
def generate_mac(nonce, user, password, admin=False, user_type=None):

mac = hmac.new(
key=shared_secret,
Expand All @@ -59,5 +59,8 @@ generation in Python::
mac.update(password.encode('utf8'))
mac.update(b"\x00")
mac.update(b"admin" if admin else b"notadmin")
if user_type:
mac.update(b"\x00")
mac.update(user_type.encode('utf8'))

return mac.hexdigest()
19 changes: 16 additions & 3 deletions synapse/_scripts/register_new_matrix_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def request_registration(
server_location,
shared_secret,
admin=False,
user_type=None,
requests=_requests,
_print=print,
exit=sys.exit,
Expand Down Expand Up @@ -65,6 +66,9 @@ def request_registration(
mac.update(password.encode('utf8'))
mac.update(b"\x00")
mac.update(b"admin" if admin else b"notadmin")
if user_type:
mac.update(b"\x00")
mac.update(user_type.encode('utf8'))

mac = mac.hexdigest()

Expand All @@ -74,6 +78,7 @@ def request_registration(
"password": password,
"mac": mac,
"admin": admin,
"user_type": user_type,
}

_print("Sending registration request...")
Expand All @@ -91,7 +96,7 @@ def request_registration(
_print("Success!")


def register_new_user(user, password, server_location, shared_secret, admin):
def register_new_user(user, password, server_location, shared_secret, admin, user_type):
if not user:
try:
default_user = getpass.getuser()
Expand Down Expand Up @@ -129,7 +134,8 @@ def register_new_user(user, password, server_location, shared_secret, admin):
else:
admin = False

request_registration(user, password, server_location, shared_secret, bool(admin))
request_registration(user, password, server_location, shared_secret,
bool(admin), user_type)


def main():
Expand All @@ -154,6 +160,12 @@ def main():
default=None,
help="New password for user. Will prompt if omitted.",
)
parser.add_argument(
"-t",
"--user_type",
default=None,
help="User type as specified in synapse.api.constants.UserTypes",
)
admin_group = parser.add_mutually_exclusive_group()
admin_group.add_argument(
"-a",
Expand Down Expand Up @@ -208,7 +220,8 @@ def main():
if args.admin or args.no_admin:
admin = args.admin

register_new_user(args.user, args.password, args.server_url, secret, admin)
register_new_user(args.user, args.password, args.server_url, secret,
admin, args.user_type)


if __name__ == "__main__":
Expand Down
5 changes: 3 additions & 2 deletions synapse/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -802,9 +802,10 @@ def check_auth_blocking(self, user_id=None, threepid=None):
threepid should never be set at the same time.
"""

# Never fail an auth check for the server notices users
# Never fail an auth check for the server notices users or support user
# This can be a problem where event creation is prohibited due to blocking
if user_id == self.hs.config.server_notices_mxid:
is_support = yield self.store.is_support_user(user_id)
if user_id == self.hs.config.server_notices_mxid or is_support:
return

if self.hs.config.hs_disabled:
Expand Down
8 changes: 8 additions & 0 deletions synapse/api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,11 @@ class RoomVersions(object):

ServerNoticeMsgType = "m.server_notice"
ServerNoticeLimitReached = "m.server_notice.usage_limit_reached"


class UserTypes(object):
"""Allows for user type specific behaviour. With the benefit of hindsight
'admin' and 'guest' users should also be UserTypes. Normal users are type None
"""
SUPPORT = "support"
ALL_USER_TYPES = (SUPPORT)
15 changes: 13 additions & 2 deletions synapse/handlers/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ def register(
make_guest=False,
admin=False,
threepid=None,
user_type=None,
default_display_name=None,
):
"""Registers a new client on the server.
Expand All @@ -141,6 +142,8 @@ def register(
since it offers no means of associating a device_id with the
access_token. Instead you should call auth_handler.issue_access_token
after registration.
user_type (str|None): type of user. One of the values from
api.constants.UserTypes, or None for a normal user.
default_display_name (unicode|None): if set, the new user's displayname
will be set to this. Defaults to 'localpart'.
Returns:
Expand Down Expand Up @@ -190,6 +193,7 @@ def register(
make_guest=make_guest,
create_profile_with_displayname=default_display_name,
admin=admin,
user_type=user_type,
)

if self.hs.config.user_directory_search_all_users:
Expand Down Expand Up @@ -242,9 +246,16 @@ def _auto_join_rooms(self, user_id):
# auto-join the user to any rooms we're supposed to dump them into
fake_requester = create_requester(user_id)

# try to create the room if we're the first user on the server
# try to create the room if we're the first real user on the server. Note
# that an auto-generated support user is not a real user and will never be
# the user to create the room
should_auto_create_rooms = False
if self.hs.config.autocreate_auto_join_rooms:
is_support = yield self.store.is_support_user(user_id)
# There is an edge case where the first user is the support user, then
# the room is never created, though this seems unlikely and
# recoverable from given the support user being involved in the first
# place.
if self.hs.config.autocreate_auto_join_rooms and not is_support:
count = yield self.store.count_all_users()
should_auto_create_rooms = count == 1
for r in self.hs.config.auto_join_rooms:
Expand Down
2 changes: 1 addition & 1 deletion synapse/handlers/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ def create_room(self, requester, config, ratelimit=True,
"""
user_id = requester.user.to_string()

self.auth.check_auth_blocking(user_id)
yield self.auth.check_auth_blocking(user_id)

if not self.spam_checker.user_may_create_room(user_id):
raise SynapseError(403, "You are not permitted to create rooms")
Expand Down
45 changes: 25 additions & 20 deletions synapse/handlers/user_directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,12 @@ def handle_local_profile_change(self, user_id, profile):
"""
# FIXME(#3714): We should probably do this in the same worker as all
# the other changes.
yield self.store.update_profile_in_user_dir(
user_id, profile.display_name, profile.avatar_url, None,
)
is_support = yield self.store.is_support_user(user_id)
# Support users are for diagnostics and should not appear in the user directory.
if not is_support:
yield self.store.update_profile_in_user_dir(
user_id, profile.display_name, profile.avatar_url, None,
)

@defer.inlineCallbacks
def handle_user_deactivated(self, user_id):
Expand Down Expand Up @@ -329,14 +332,7 @@ def _handle_deltas(self, deltas):
public_value=Membership.JOIN,
)

if change is None:
# Handle any profile changes
yield self._handle_profile_change(
state_key, room_id, prev_event_id, event_id,
)
continue

if not change:
if change is False:
# Need to check if the server left the room entirely, if so
# we might need to remove all the users in that room
is_in_room = yield self.store.is_host_joined(
Expand All @@ -354,16 +350,25 @@ def _handle_deltas(self, deltas):
else:
logger.debug("Server is still in room: %r", room_id)

if change: # The user joined
event = yield self.store.get_event(event_id, allow_none=True)
profile = ProfileInfo(
avatar_url=event.content.get("avatar_url"),
display_name=event.content.get("displayname"),
)
is_support = yield self.store.is_support_user(state_key)
if not is_support:
if change is None:
# Handle any profile changes
yield self._handle_profile_change(
state_key, room_id, prev_event_id, event_id,
)
continue

if change: # The user joined
event = yield self.store.get_event(event_id, allow_none=True)
profile = ProfileInfo(
avatar_url=event.content.get("avatar_url"),
display_name=event.content.get("displayname"),
)

yield self._handle_new_user(room_id, state_key, profile)
else: # The user left
yield self._handle_remove_user(room_id, state_key)
yield self._handle_new_user(room_id, state_key, profile)
else: # The user left
yield self._handle_remove_user(room_id, state_key)
else:
logger.debug("Ignoring irrelevant type: %r", typ)

Expand Down
11 changes: 10 additions & 1 deletion synapse/rest/client/v1/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

from twisted.internet import defer

from synapse.api.constants import Membership
from synapse.api.constants import Membership, UserTypes
from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
from synapse.http.servlet import (
assert_params_in_dict,
Expand Down Expand Up @@ -158,6 +158,11 @@ def on_POST(self, request):
raise SynapseError(400, "Invalid password")

admin = body.get("admin", None)
user_type = body.get("user_type", None)

if user_type is not None and user_type not in UserTypes.ALL_USER_TYPES:
raise SynapseError(400, "Invalid user type")

got_mac = body["mac"]

want_mac = hmac.new(
Expand All @@ -171,6 +176,9 @@ def on_POST(self, request):
want_mac.update(password)
want_mac.update(b"\x00")
want_mac.update(b"admin" if admin else b"notadmin")
if user_type:
want_mac.update(b"\x00")
want_mac.update(user_type.encode('utf8'))
want_mac = want_mac.hexdigest()

if not hmac.compare_digest(
Expand All @@ -189,6 +197,7 @@ def on_POST(self, request):
password=body["password"],
admin=bool(admin),
generate_token=False,
user_type=user_type,
)

result = yield register._create_registration_details(user_id, body)
Expand Down
30 changes: 28 additions & 2 deletions synapse/storage/monthly_active_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,12 @@ def _initialise_reserved_users(self, txn, threepids):
txn,
tp["medium"], tp["address"]
)

if user_id:
self.upsert_monthly_active_user_txn(txn, user_id)
reserved_user_list.append(user_id)
is_support = self.is_support_user_txn(txn, user_id)
if not is_support:
self.upsert_monthly_active_user_txn(txn, user_id)
reserved_user_list.append(user_id)
else:
logger.warning(
"mau limit reserved threepid %s not found in db" % tp
Expand Down Expand Up @@ -182,6 +185,18 @@ def upsert_monthly_active_user(self, user_id):
Args:
user_id (str): user to add/update
"""
# Support user never to be included in MAU stats. Note I can't easily call this
# from upsert_monthly_active_user_txn because then I need a _txn form of
# is_support_user which is complicated because I want to cache the result.
# Therefore I call it here and ignore the case where
# upsert_monthly_active_user_txn is called directly from
# _initialise_reserved_users reasoning that it would be very strange to
# include a support user in this context.

is_support = yield self.is_support_user(user_id)
if is_support:
return

is_insert = yield self.runInteraction(
"upsert_monthly_active_user", self.upsert_monthly_active_user_txn,
user_id
Expand All @@ -200,6 +215,16 @@ def upsert_monthly_active_user_txn(self, txn, user_id):
in a database thread rather than the main thread, and we can't call
txn.call_after because txn may not be a LoggingTransaction.
We consciously do not call is_support_txn from this method because it
is not possible to cache the response. is_support_txn will be false in
almost all cases, so it seems reasonable to call it only for
upsert_monthly_active_user and to call is_support_txn manually
for cases where upsert_monthly_active_user_txn is called directly,
like _initialise_reserved_users
In short, don't call this method with support users. (Support users
should not appear in the MAU stats).
Args:
txn (cursor):
user_id (str): user to add/update
Expand All @@ -208,6 +233,7 @@ def upsert_monthly_active_user_txn(self, txn, user_id):
bool: True if a new entry was created, False if an
existing one was updated.
"""

# Am consciously deciding to lock the table on the basis that is ought
# never be a big table and alternative approaches (batching multiple
# upserts into a single txn) introduced a lot of extra complexity.
Expand Down
Loading

0 comments on commit d2f7c4e

Please sign in to comment.