Skip to content

Commit b387430

Browse files
committed
Iterate
1 parent 13a8727 commit b387430

7 files changed

Lines changed: 230 additions & 31 deletions

File tree

changelog.d/18456.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support configuration of default and extra user types.

docs/admin_api/user_admin_api.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,8 @@ Body parameters:
163163
- `locked` - **bool**, optional. If unspecified, locked state will be left unchanged.
164164
- `user_type` - **string** or null, optional. If not provided, the user type will be
165165
not be changed. If `null` is given, the user type will be cleared.
166-
Other allowed options are: `bot` and `support`. # XXXTODO
166+
Other allowed options are: `bot` and `support` and any extra values defined in the homserver
167+
[configuration](../usage/configuration/config_documentation.md#user_types).
167168

168169
## List Accounts
169170
### List Accounts (V2)

docs/usage/configuration/config_documentation.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,24 @@ Example configuration:
834834
```yaml
835835
max_event_delay_duration: 24h
836836
```
837+
---
838+
### `user_types`
839+
840+
Configuration settings related to the user types feature.
841+
842+
This setting has the following sub-options:
843+
* `default_user_type`: The default user type to use for registering new users when no value has been specified.
844+
Defaults to none.
845+
* `extra_user_types`: Array of additional user types to allow. These are treated as real users. Defaults to [].
846+
847+
Example configuration:
848+
```yaml
849+
user_types:
850+
default_user_type: "custom"
851+
extra_user_types:
852+
- "custom"
853+
- "custom2"
854+
```
837855

838856
## Homeserver blocking
839857
Useful options for Synapse admins.

synapse/config/user_types.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from synapse.api.constants import UserTypes
1818
from synapse.types import JsonDict
1919

20-
from ._base import Config
20+
from ._base import Config, ConfigError
2121

2222

2323
class UserTypesConfig(Config):
@@ -30,10 +30,15 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
3030
"default_user_type", None
3131
)
3232
self.extra_user_types: List[str] = user_types.get("extra_user_types", [])
33-
# XXXTODO: provide a way to set which extra user types should be treated as "real" users
3433

3534
all_user_types: List[str] = []
3635
all_user_types.extend(UserTypes.ALL_USER_TYPES)
3736
all_user_types.extend(self.extra_user_types)
3837

3938
self.all_user_types = all_user_types
39+
40+
if self.default_user_type is not None:
41+
if self.default_user_type not in all_user_types:
42+
raise ConfigError(
43+
f"Default user type {self.default_user_type} is not in the list of all user types: {all_user_types}"
44+
)

synapse/storage/databases/main/registration.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -685,8 +685,7 @@ def is_real_user_txn(self, txn: LoggingTransaction, user_id: str) -> bool:
685685
retcol="user_type",
686686
allow_none=True,
687687
)
688-
# XXXTODO
689-
return res is None
688+
return res is None or res not in [UserTypes.BOT, UserTypes.SUPPORT]
690689

691690
def is_support_user_txn(self, txn: LoggingTransaction, user_id: str) -> bool:
692691
res = self.db_pool.simple_select_one_onecol_txn(
@@ -962,11 +961,12 @@ def _count_users(txn: LoggingTransaction) -> int:
962961
return await self.db_pool.runInteraction("count_users", _count_users)
963962

964963
async def count_real_users(self) -> int:
965-
"""Counts all users without a special user_type registered on the homeserver."""
964+
"""Counts all users without the bot or support user_types registered on the homeserver."""
966965

967-
# XXXTODO
968966
def _count_users(txn: LoggingTransaction) -> int:
969-
txn.execute("SELECT COUNT(*) FROM users where user_type is null")
967+
txn.execute(
968+
f"SELECT COUNT(*) FROM users WHERE user_type IS NULL OR user_type NOT IN ('{UserTypes.BOT}', '{UserTypes.SUPPORT}')"
969+
)
970970
row = txn.fetchone()
971971
assert row is not None
972972
return row[0]

tests/handlers/test_register.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,41 @@ def test_spam_checker_receives_sso_type(self) -> None:
738738
self.handler.register_user(localpart="bobflimflob", auth_provider_id="saml")
739739
)
740740

741+
def test_register_default_user_type(self) -> None:
742+
"""Test that the default user type is none when registering a user."""
743+
user_id = self.get_success(self.handler.register_user(localpart="user"))
744+
user_info = self.get_success(self.store.get_user_by_id(user_id))
745+
assert user_info is not None
746+
self.assertEqual(user_info.user_type, None)
747+
748+
def test_register_extra_user_types_valid(self) -> None:
749+
"""
750+
Test that the specified user type is set correctly when registering a user.
751+
n.b. No validation is done on the user type, so this test
752+
is only to ensure that the user type can be set to any value.
753+
"""
754+
user_id = self.get_success(
755+
self.handler.register_user(localpart="user", user_type="anyvalue")
756+
)
757+
user_info = self.get_success(self.store.get_user_by_id(user_id))
758+
assert user_info is not None
759+
self.assertEqual(user_info.user_type, "anyvalue")
760+
761+
@override_config(
762+
{
763+
"user_types": {
764+
"extra_user_types": ["extra1", "extra2"],
765+
"default_user_type": "extra1",
766+
}
767+
}
768+
)
769+
def test_register_extra_user_types_with_default(self) -> None:
770+
"""Test that the default_user_type in config is set correctly when registering a user."""
771+
user_id = self.get_success(self.handler.register_user(localpart="user"))
772+
user_info = self.get_success(self.store.get_user_by_id(user_id))
773+
assert user_info is not None
774+
self.assertEqual(user_info.user_type, "extra1")
775+
741776
async def get_or_create_user(
742777
self,
743778
requester: Requester,

tests/rest/admin/test_user.py

Lines changed: 162 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,61 @@ def nonce() -> str:
328328
self.assertEqual(400, channel.code, msg=channel.json_body)
329329
self.assertEqual("Invalid user type", channel.json_body["error"])
330330

331+
@override_config(
332+
{
333+
"user_types": {
334+
"extra_user_types": ["extra1", "extra2"],
335+
}
336+
}
337+
)
338+
def test_extra_user_type(self) -> None:
339+
"""
340+
Check that the extra user type can be used when registering a user.
341+
"""
342+
343+
def nonce_mac(user_type: str) -> tuple[str, str]:
344+
"""
345+
Get a nonce and the expected HMAC for that nonce.
346+
"""
347+
channel = self.make_request("GET", self.url)
348+
nonce = channel.json_body["nonce"]
349+
350+
want_mac = hmac.new(key=b"shared", digestmod=hashlib.sha1)
351+
want_mac.update(
352+
nonce.encode("ascii")
353+
+ b"\x00alice\x00abc123\x00notadmin\x00"
354+
+ user_type.encode("ascii")
355+
)
356+
want_mac_str = want_mac.hexdigest()
357+
358+
return nonce, want_mac_str
359+
360+
nonce, mac = nonce_mac("extra1")
361+
# Valid user_type
362+
body = {
363+
"nonce": nonce,
364+
"username": "alice",
365+
"password": "abc123",
366+
"user_type": "extra1",
367+
"mac": mac,
368+
}
369+
channel = self.make_request("POST", self.url, body)
370+
self.assertEqual(200, channel.code, msg=channel.json_body)
371+
372+
nonce, mac = nonce_mac("extra3")
373+
# Invalid user_type
374+
body = {
375+
"nonce": nonce,
376+
"username": "alice",
377+
"password": "abc123",
378+
"user_type": "extra3",
379+
"mac": mac,
380+
}
381+
channel = self.make_request("POST", self.url, body)
382+
383+
self.assertEqual(400, channel.code, msg=channel.json_body)
384+
self.assertEqual("Invalid user type", channel.json_body["error"])
385+
331386
def test_displayname(self) -> None:
332387
"""
333388
Test that displayname of new user is set
@@ -1186,6 +1241,80 @@ def test_user_type(
11861241
not_user_types=["custom"],
11871242
)
11881243

1244+
@override_config(
1245+
{
1246+
"user_types": {
1247+
"extra_user_types": ["extra1", "extra2"],
1248+
}
1249+
}
1250+
)
1251+
def test_filter_not_user_types_with_extra(self) -> None:
1252+
"""Tests that the endpoint handles the not_user_types param when extra_user_types are configured"""
1253+
1254+
regular_user_id = self.register_user("normalo", "secret")
1255+
1256+
extra1_user_id = self.register_user("extra1", "secret")
1257+
self.make_request(
1258+
"PUT",
1259+
"/_synapse/admin/v2/users/" + urllib.parse.quote(extra1_user_id),
1260+
{"user_type": "extra1"},
1261+
access_token=self.admin_user_tok,
1262+
)
1263+
1264+
def test_user_type(
1265+
expected_user_ids: List[str], not_user_types: Optional[List[str]] = None
1266+
) -> None:
1267+
"""Runs a test for the not_user_types param
1268+
Args:
1269+
expected_user_ids: Ids of the users that are expected to be returned
1270+
not_user_types: List of values for the not_user_types param
1271+
"""
1272+
1273+
user_type_query = ""
1274+
1275+
if not_user_types is not None:
1276+
user_type_query = "&".join(
1277+
[f"not_user_type={u}" for u in not_user_types]
1278+
)
1279+
1280+
test_url = f"{self.url}?{user_type_query}"
1281+
channel = self.make_request(
1282+
"GET",
1283+
test_url,
1284+
access_token=self.admin_user_tok,
1285+
)
1286+
1287+
self.assertEqual(200, channel.code)
1288+
self.assertEqual(channel.json_body["total"], len(expected_user_ids))
1289+
self.assertEqual(
1290+
expected_user_ids,
1291+
[u["name"] for u in channel.json_body["users"]],
1292+
)
1293+
1294+
# Request without user_types → all users expected
1295+
test_user_type([self.admin_user, extra1_user_id, regular_user_id])
1296+
1297+
# Request and exclude extra1 user type
1298+
test_user_type(
1299+
[self.admin_user, regular_user_id],
1300+
not_user_types=["extra1"],
1301+
)
1302+
1303+
# Request and exclude extra1 and extra2 user types
1304+
test_user_type(
1305+
[self.admin_user, regular_user_id],
1306+
not_user_types=["extra1", "extra2"],
1307+
)
1308+
1309+
# Request and exclude empty user types → only expected the extra1 user
1310+
test_user_type([extra1_user_id], not_user_types=[""])
1311+
1312+
# Request and exclude an unregistered type → expect all users
1313+
test_user_type(
1314+
[self.admin_user, extra1_user_id, regular_user_id],
1315+
not_user_types=["extra3"],
1316+
)
1317+
11891318
def test_erasure_status(self) -> None:
11901319
# Create a new user.
11911320
user_id = self.register_user("eraseme", "eraseme")
@@ -2977,22 +3106,18 @@ def test_set_user_as_admin(self) -> None:
29773106
self.assertEqual("@user:test", channel.json_body["name"])
29783107
self.assertTrue(channel.json_body["admin"])
29793108

2980-
def test_set_user_type(self) -> None:
2981-
"""
2982-
Test changing user type.
2983-
"""
2984-
2985-
# Set to support type
3109+
def set_user_type(self, user_type: Optional[str]) -> None:
3110+
# Set to user_type
29863111
channel = self.make_request(
29873112
"PUT",
29883113
self.url_other_user,
29893114
access_token=self.admin_user_tok,
2990-
content={"user_type": UserTypes.SUPPORT},
3115+
content={"user_type": user_type},
29913116
)
29923117

29933118
self.assertEqual(200, channel.code, msg=channel.json_body)
29943119
self.assertEqual("@user:test", channel.json_body["name"])
2995-
self.assertEqual(UserTypes.SUPPORT, channel.json_body["user_type"])
3120+
self.assertEqual(user_type, channel.json_body["user_type"])
29963121

29973122
# Get user
29983123
channel = self.make_request(
@@ -3003,30 +3128,44 @@ def test_set_user_type(self) -> None:
30033128

30043129
self.assertEqual(200, channel.code, msg=channel.json_body)
30053130
self.assertEqual("@user:test", channel.json_body["name"])
3006-
self.assertEqual(UserTypes.SUPPORT, channel.json_body["user_type"])
3131+
self.assertEqual(user_type, channel.json_body["user_type"])
3132+
3133+
def test_set_user_type(self) -> None:
3134+
"""
3135+
Test changing user type.
3136+
"""
3137+
3138+
# Set to support type
3139+
self.set_user_type(UserTypes.SUPPORT)
30073140

30083141
# Change back to a regular user
3009-
channel = self.make_request(
3010-
"PUT",
3011-
self.url_other_user,
3012-
access_token=self.admin_user_tok,
3013-
content={"user_type": None},
3014-
)
3142+
self.set_user_type(None)
30153143

3016-
self.assertEqual(200, channel.code, msg=channel.json_body)
3017-
self.assertEqual("@user:test", channel.json_body["name"])
3018-
self.assertIsNone(channel.json_body["user_type"])
3144+
@override_config({"user_types": {"extra_user_types": ["extra1", "extra2"]}})
3145+
def test_set_user_type_with_extras(self) -> None:
3146+
"""
3147+
Test changing user type with extra_user_types configured.
3148+
"""
30193149

3020-
# Get user
3150+
# Check that we can still set to support type
3151+
self.set_user_type(UserTypes.SUPPORT)
3152+
3153+
# Check that we can set to an extra user type
3154+
self.set_user_type("extra2")
3155+
3156+
# Change back to a regular user
3157+
self.set_user_type(None)
3158+
3159+
# Try setting to invalid type
30213160
channel = self.make_request(
3022-
"GET",
3161+
"PUT",
30233162
self.url_other_user,
30243163
access_token=self.admin_user_tok,
3164+
content={"user_type": "extra3"},
30253165
)
30263166

3027-
self.assertEqual(200, channel.code, msg=channel.json_body)
3028-
self.assertEqual("@user:test", channel.json_body["name"])
3029-
self.assertIsNone(channel.json_body["user_type"])
3167+
self.assertEqual(400, channel.code, msg=channel.json_body)
3168+
self.assertEqual("Invalid user type", channel.json_body["error"])
30303169

30313170
def test_accidental_deactivation_prevention(self) -> None:
30323171
"""

0 commit comments

Comments
 (0)