Compare commits

...

356 Commits

Author SHA1 Message Date
Brendan Abolivier
84e695f506 Merge pull request #5932 from matrix-org/babolivier/account_validity_template_encode
Fix encoding for account validity HTML files on Python 2
2019-08-29 16:50:10 +01:00
Brendan Abolivier
91694227b6 Bump requirement for six to 1.12 2019-08-29 16:20:21 +01:00
Brendan Abolivier
7a95d8fff9 Fix encoding for account validity HTML files on Python 2 2019-08-29 16:03:59 +01:00
Brendan Abolivier
99eec6d2d5 Merge pull request #5815 from matrix-org/babolivier/dinsic-message-retention
Message retention policies at the room and server levels
2019-08-28 18:49:13 +01:00
Brendan Abolivier
e5df12ad16 Merge branch 'dinsic' into babolivier/dinsic-message-retention 2019-08-28 18:17:26 +01:00
Brendan Abolivier
70714595bc Implement per-room message retention policies 2019-08-28 18:16:17 +01:00
Michael Kaye
62f5e3bc4c Merge pull request #5812 from matrix-org/babolivier/account-validity-messages
Account validity: allow defining HTML templates to serve the user on account renewal attempt
2019-08-12 13:26:49 +01:00
Michael Kaye
63d0d21a28 Merge pull request #5781 from matrix-org/baboliver/loopingcall-args
Add ability to pass arguments to looping calls
2019-08-08 12:10:36 +01:00
Michael Kaye
8551b4f336 Merge pull request #5760 from matrix-org/babolivier/access-rules-public-restricted
Force the access rule to be "restricted" if the join rule is "public"
2019-08-08 12:10:22 +01:00
Brendan Abolivier
359aed4168 Lint 2019-08-01 16:08:39 +02:00
Brendan Abolivier
cd47482257 Sample config 2019-08-01 16:08:39 +02:00
Brendan Abolivier
a9567ee1a6 Changelog 2019-08-01 16:08:38 +02:00
Brendan Abolivier
9502bd8d78 Add tests 2019-08-01 16:08:38 +02:00
Brendan Abolivier
235271be4f Allow defining HTML templates to serve the user on account renewal 2019-08-01 16:08:38 +02:00
Brendan Abolivier
0c6500a08b Explain rationale 2019-08-01 10:19:04 +02:00
Brendan Abolivier
c862d5baf6 Check room ID and type of redacted event (#5784)
Check room ID and type of redacted event
2019-07-31 16:44:19 +02:00
Brendan Abolivier
35ec13baab Ignore redactions of redactions in get_events_as_list 2019-07-31 15:48:57 +02:00
Brendan Abolivier
c4e56a8ee9 Ignore invalid redactions in _get_event_from_row 2019-07-31 15:11:27 +02:00
Brendan Abolivier
d2bb51080e Incorporate review 2019-07-30 16:15:01 +02:00
Brendan Abolivier
0fda4e2e50 Should now work, unless we can't find the redaction event which happens for some reason (need to investigate) 2019-07-30 15:56:02 +02:00
Brendan Abolivier
8ced9a2f58 Don't make the checks depend on recheck_redaction 2019-07-30 15:55:18 +02:00
Brendan Abolivier
bbd6208b3e Do checks sooner 2019-07-29 17:22:42 +02:00
Brendan Abolivier
36c61df659 Check room ID and type of redacted event 2019-07-29 16:07:12 +02:00
Brendan Abolivier
132887db8c Changelog 2019-07-29 11:06:43 +02:00
Brendan Abolivier
bec6d9e090 Add kwargs and doc 2019-07-29 11:06:43 +02:00
Brendan Abolivier
8a47155763 Add ability to pass arguments to looping calls 2019-07-29 11:06:43 +02:00
Brendan Abolivier
ffa30b507f Fix changelog 2019-07-25 10:19:13 +02:00
Brendan Abolivier
00b47fdfc7 Changelog 2019-07-25 10:17:07 +02:00
Brendan Abolivier
2526b79ce6 Merge ifs 2019-07-25 10:15:44 +02:00
Brendan Abolivier
aea03c9d73 Doc 2019-07-25 10:14:41 +02:00
Brendan Abolivier
ddf256c77f Fix status code for forbidden events 2019-07-25 10:03:59 +02:00
Brendan Abolivier
dd92685179 Only check the join rule on room creation if an access rule is also provided 2019-07-25 10:03:36 +02:00
Brendan Abolivier
0bb375c124 Turns out the default preset is public_chat 2019-07-24 17:29:17 +02:00
Brendan Abolivier
ea5f86304e Implement restrictions on new events 2019-07-24 17:27:07 +02:00
Brendan Abolivier
d1a78ba2a3 Implement restriction on public room creation 2019-07-24 17:06:23 +02:00
Brendan Abolivier
dcdfdb3f17 Add test case 2019-07-24 16:38:49 +02:00
Brendan Abolivier
4dc2eda1d3 Merge pull request #5702 from matrix-org/babolivier/3pid-invite
Fix check of the association between a 3PID invite and the subsequent m.room.member event in the 3rd party rules
2019-07-18 13:38:05 +02:00
Brendan Abolivier
13ad3f5ab1 Fix changelog 2019-07-17 15:10:16 +02:00
Brendan Abolivier
28803ad56a Merge branch 'dinsic' into babolivier/3pid-invite 2019-07-17 15:06:41 +02:00
Brendan Abolivier
446fb64d50 Changelog 2019-07-17 15:05:35 +02:00
Brendan Abolivier
34dd738e4b There's no third_party_signed property in an invite's content 2019-07-17 15:03:23 +02:00
Brendan Abolivier
048f86ef5d Fix check of the association between a 3PID invite and the subsequent m.room.member event in the 3rd party rules 2019-07-17 14:54:58 +02:00
Brendan Abolivier
3bc4ea841d Merge pull request #5646 from matrix-org/babolivier/profile-allow-self
Port #5644 to dinsic
2019-07-09 13:19:57 +01:00
Amber H. Brown
4e954f4bbd make the base branch dinsic for dinsic 2019-07-09 20:53:38 +10:00
Amber H. Brown
f10b6f7385 cut down circleci config to just what dinsic needs 2019-07-09 20:38:24 +10:00
Brendan Abolivier
9fc40f7b3f Lint 2019-07-09 10:30:43 +01:00
Brendan Abolivier
cf22aae355 Add test case 2019-07-09 10:27:53 +01:00
Brendan Abolivier
6b9d27d793 Changelog 2019-07-09 10:27:53 +01:00
Brendan Abolivier
25fff95448 Allow newly-registered users to lookup their own profiles
When a user creates an account and the 'require_auth_for_profile_requests' config flag is set, and a client that performed the registration wants to lookup the newly-created profile, the request will be denied because the user doesn't share a room with themselves yet.
2019-07-09 10:27:53 +01:00
Brendan Abolivier
5fe0cea37e Merge pull request #5610 from matrix-org/babolivier/power-levels
Implement new restrictions on power levels
2019-07-04 11:12:47 +01:00
Brendan Abolivier
95b5d4ad54 Merge branch 'babolivier/power-levels' of github.com:matrix-org/synapse into babolivier/power-levels 2019-07-03 18:30:13 +01:00
Brendan Abolivier
4dd7de17b7 Incorporate review 2019-07-03 18:26:48 +01:00
Brendan Abolivier
8b44097771 Update synapse/third_party_rules/access_rules.py
Co-Authored-By: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2019-07-03 18:21:42 +01:00
Brendan Abolivier
6b83a1826c Incorporate review 2019-07-03 17:32:52 +01:00
Brendan Abolivier
aeda7fde78 Changelog 2019-07-03 15:47:58 +01:00
Brendan Abolivier
d085e0df2a Change the rule for applying PL restrictions 2019-07-03 15:44:22 +01:00
Brendan Abolivier
8d28817eac Add tests 2019-07-03 15:43:52 +01:00
Brendan Abolivier
aa3ba41933 Default return value for events we're not interested in 2019-07-03 12:05:56 +01:00
Brendan Abolivier
724ddaddb3 Refactor part of the access rules module
Since we're not processing only membership events and 3PID invites anymore, it's nice to know which function is supposed to process what.
2019-07-03 12:03:22 +01:00
Brendan Abolivier
8636ec042b Implement restrictions for power levels 2019-07-03 11:45:07 +01:00
Brendan Abolivier
78b737ff71 Merge pull request #5577 from matrix-org/babolivier/3pid-invite-ratelimit
Port #5576 to dinsic
2019-06-28 18:23:53 +01:00
Brendan Abolivier
a7a8bbf502 Only ratelimit when sending the email
If we do the opposite, an event can arrive after or while sending the email and the 3PID invite event will get ratelimited.
2019-06-28 17:47:38 +01:00
Brendan Abolivier
f304996c61 Changelog 2019-06-28 17:44:28 +01:00
Brendan Abolivier
7c644198c8 Don't update the ratelimiter before sending a 3PID invite
This would cause emails being sent, but Synapse responding with a 429 when creating the event. The client would then retry, and with bad timing the same scenario would happen again. Some testing I did ended up sending me 10 emails for one single invite because of this.
2019-06-28 17:44:28 +01:00
Brendan Abolivier
ee0ee97447 Add test case for #5574
There's no test that makes sure #5574 didn't break things or works correctly (my bad), so this PR adds a test case that makes sure of it.
2019-06-28 10:29:02 +01:00
Brendan Abolivier
f370fd01ec Merge pull request #5574 from matrix-org/babolivier/fix-3pid-bind
Fix new 3PID bindings being associated with rewritten IS's server names instead of the original one
2019-06-27 15:17:15 +01:00
Brendan Abolivier
1ef3627b05 Fix new 3PID bindings being associated with rewritten IS's server names instead of the original one
This would make unbind requests fail because Synapse is using the server name it has in DB as destination_is to generate signature, and Sydent uses its own server name there when verifying it, so if a mismatch happens, which is the case if the name gets rewritten for routing purposes, the unbind requests fail on the signature verification.
2019-06-27 14:53:10 +01:00
Brendan Abolivier
fbc5b58d21 Merge pull request #5542 from matrix-org/babolivier/federation-publicrooms
Port "Split public rooms directory auth config in two" to dinsic
2019-06-24 16:42:21 +01:00
Brendan Abolivier
84875301b6 Split public rooms directory auth config in two 2019-06-24 16:41:48 +01:00
Brendan Abolivier
18f0b69c49 Merge pull request #5495 from matrix-org/babolivier/deactivate_bg_job_typo
Port "Fix typo in deactivation background job" to dinsic
2019-06-19 11:58:52 +01:00
Brendan Abolivier
4875746d82 Fix typo in deactivation background job 2019-06-19 11:39:18 +01:00
Brendan Abolivier
fa4efb5967 Merge pull request #5484 from matrix-org/babolivier/dinsic_access_rules
Implement custom access rules
2019-06-19 10:41:37 +01:00
Brendan Abolivier
c1bc48f9d4 Add tests for constraints on changing the rule for a room 2019-06-18 18:07:05 +01:00
Brendan Abolivier
9b3c69f661 Add tests for 3PID invites 2019-06-18 17:59:07 +01:00
Brendan Abolivier
7157276124 Don't process 3PIDs in _apply_restricted 2019-06-18 17:43:34 +01:00
Brendan Abolivier
1532369dcd Remove unused import 2019-06-18 16:46:57 +01:00
Brendan Abolivier
145291108d Add tests for inviting with access rules 2019-06-18 16:32:54 +01:00
Brendan Abolivier
cefc5542fb Lint 2019-06-18 14:59:05 +01:00
Brendan Abolivier
2a1f35193b Remove unused imports 2019-06-18 14:57:04 +01:00
Brendan Abolivier
a09767d57d Lint 2019-06-18 14:55:24 +01:00
Brendan Abolivier
bc0fd8f170 Add tests for room creation hook 2019-06-18 14:54:19 +01:00
Brendan Abolivier
d36a876d2d Incorporate review 2019-06-18 14:53:33 +01:00
Brendan Abolivier
f7339d42ee Fixes 2019-06-18 11:28:33 +01:00
Brendan Abolivier
8aea2c3be1 Docstrings 2019-06-17 20:24:36 +01:00
Brendan Abolivier
1cd0ecc1f2 Rename into RoomAccessRules 2019-06-17 20:24:30 +01:00
Brendan Abolivier
19a4298a51 Fix function call 2019-06-17 20:24:26 +01:00
Brendan Abolivier
073dd7778e Implement rules change 2019-06-17 20:24:22 +01:00
Brendan Abolivier
00736c8935 Improve doc 2019-06-17 20:24:16 +01:00
Brendan Abolivier
112a48a5aa Implement 3PID invite hook 2019-06-17 20:24:07 +01:00
Brendan Abolivier
53aff08ebe Implement restricted rules and room creation hook 2019-06-17 20:22:53 +01:00
Brendan Abolivier
8b2f655589 Implement rules for direct 2019-06-17 20:22:27 +01:00
Brendan Abolivier
bd8448ccb2 Backbone 2019-06-17 20:19:12 +01:00
Brendan Abolivier
110608d1d6 Base 2019-06-17 20:19:08 +01:00
Brendan Abolivier
5a28504c08 Merge pull request #5481 from matrix-org/babolivier/third_party_rules_3pid
Port "Add third party rules hook for 3PID invites" to dinsic
2019-06-17 18:35:54 +01:00
Brendan Abolivier
9bb7cec90f Make check_threepid_can_be_invited async 2019-06-17 18:10:00 +01:00
Brendan Abolivier
8a8644045f Changelog 2019-06-17 18:10:00 +01:00
Brendan Abolivier
08d4534771 Add third party rules hook for 3PID invites 2019-06-17 18:10:00 +01:00
Erik Johnston
c8349787b7 Merge pull request #5474 from matrix-org/erikj/add_create_room_hook
Add third party rules hook into create room
2019-06-17 17:30:08 +01:00
Erik Johnston
e5e690041d Newsfile 2019-06-17 15:52:15 +01:00
Erik Johnston
fb1b76ff4c Add third party rules hook into create room 2019-06-17 15:48:57 +01:00
Brendan Abolivier
40ea934de0 Merge pull request #5471 from matrix-org/erikj/3pid_remote_invite_state
Port "Fix 3PID invite room state over federation" to dinsic
2019-06-17 11:43:02 +01:00
Brendan Abolivier
5c4296b47a Merge branch 'babolivier/third_party_event_rules' into dinsic 2019-06-14 19:38:44 +01:00
Brendan Abolivier
8b2543c0ec Merge pull request #5466 from matrix-org/babolivier/fix_deactivation_bg_job
Port "Fix background job for deactivated flag" to dinsic
2019-06-14 19:22:56 +01:00
Brendan Abolivier
bd59e154c1 Fix changelog 2019-06-14 18:29:13 +01:00
Brendan Abolivier
66378d13ce Changelog 2019-06-14 18:29:13 +01:00
Brendan Abolivier
65d3652cb7 Fix background job for deactivated flag 2019-06-14 18:29:13 +01:00
Brendan Abolivier
c644755123 Merge pull request #5463 from matrix-org/babolivier/account_validity_deactivated_accounts_2
Port "Don't send renewal emails to deactivated users" to dinsic
2019-06-14 17:19:57 +01:00
Brendan Abolivier
c7964e7430 Fix test 2019-06-14 16:25:18 +01:00
Erik Johnston
3c9bb86fde Newsfile 2019-06-14 16:19:11 +01:00
Erik Johnston
304a1376c2 Fix 3PID invite room state over federation.
Fixes that when a user exchanges a 3PID invite for a proper invite over
federation it does not include the `invite_room_state` key.

This was due to synapse incorrectly sending out two invite requests.
2019-06-14 16:19:11 +01:00
Brendan Abolivier
aded5cfb72 Don't send renewal emails to deactivated users 2019-06-14 15:39:18 +01:00
Brendan Abolivier
57bd5cfc9a Track deactivated accounts in the database (#5378) 2019-06-14 13:45:36 +01:00
Brendan Abolivier
ad566df746 Merge pull request #5420 from matrix-org/babolivier/userdir_hide_users
Add configuration option to hide new users from the user directory
2019-06-14 10:59:45 +01:00
Brendan Abolivier
c4759cd041 Merge branch 'dinsic' into babolivier/userdir_hide_users 2019-06-14 10:37:43 +01:00
Brendan Abolivier
4dda68da9a Make index unique 2019-06-14 10:37:12 +01:00
Brendan Abolivier
adf3eee668 Merge branch 'babolivier/fix_profile_replication' into babolivier/userdir_hide_users 2019-06-13 18:11:11 +01:00
Brendan Abolivier
a6cf1e079e Merge pull request #5416 from matrix-org/babolivier/fix_profile_replication
Add unique index to the profile_replication_status table.
2019-06-13 18:10:14 +01:00
Brendan Abolivier
996cf15d42 Lint 2019-06-13 17:41:13 +01:00
Brendan Abolivier
27cd0b3037 Fix snafu 2019-06-13 17:40:14 +01:00
Neil Johnson
4f68188d0b Change to absolute path for contrib/docker
because this file is reproduced on dockerhub and relative paths don't work
2019-06-13 16:42:36 +01:00
Brendan Abolivier
26a27e06b9 Create the index in a background update 2019-06-13 15:57:55 +01:00
Brendan Abolivier
5283f65316 Fix bogus changelog 2019-06-12 16:49:10 +01:00
Brendan Abolivier
4a1b749582 Lint 2019-06-12 16:48:51 +01:00
Brendan Abolivier
e0c56d6527 Merge branch 'dinsic' into babolivier/fix_profile_replication 2019-06-12 16:41:08 +01:00
Brendan Abolivier
7c2d936802 Fix bogus changelog 2019-06-12 16:38:00 +01:00
Brendan Abolivier
1c7628eb3a Lint 2019-06-12 16:36:28 +01:00
Brendan Abolivier
8400e5f624 Merge branch 'dinsic' into babolivier/userdir_hide_users 2019-06-12 16:29:56 +01:00
Brendan Abolivier
02a659ae30 Regenerate full schemas 2019-06-12 15:51:59 +01:00
Brendan Abolivier
a55b2efca1 Fix path for password policy endpoints 2019-06-12 15:51:42 +01:00
Brendan Abolivier
d907e8f599 Merge branch 'master' into dinsic 2019-06-12 14:36:44 +01:00
Erik Johnston
97174780ce 1.0.0 2019-06-11 17:10:01 +01:00
Erik Johnston
9532eb55ec Merge pull request #5424 from matrix-org/erikj/change_password_reset_links
Change password reset links to /_matrix.
2019-06-11 13:29:42 +01:00
Erik Johnston
a766c41d25 Bump bleach version so that tests can run on old deps. 2019-06-11 12:34:18 +01:00
Neil Johnson
426218323b Neilj/improve federation docs (#5419)
Add FAQ questions to federate.md. Add a health warning making it clear that the 1711 upgrade FAQ is now out of date.
2019-06-11 12:17:43 +01:00
Erik Johnston
453aaaadc0 Newsfile 2019-06-11 11:34:38 +01:00
Erik Johnston
10383e6e6f Change password reset links to /_matrix. 2019-06-11 11:34:33 +01:00
Brendan Abolivier
3c8262b181 Merge branch 'master' into dinsic 2019-06-11 10:55:53 +01:00
Brendan Abolivier
389aa20170 Generate sample config 2019-06-10 17:31:45 +01:00
Brendan Abolivier
c10226581a Lint 2019-06-10 17:25:42 +01:00
Brendan Abolivier
10510f1e7e Lint 2019-06-10 17:23:11 +01:00
Brendan Abolivier
9ece96f5c8 Changelog 2019-06-10 17:17:18 +01:00
Brendan Abolivier
1924848dfa Add test case 2019-06-10 17:16:17 +01:00
Brendan Abolivier
deee82aebc Fix variable definition 2019-06-10 17:16:03 +01:00
Brendan Abolivier
0e63dd89a4 Fix condition 2019-06-10 16:56:53 +01:00
Brendan Abolivier
d331119758 Hide new users from the user directory if enabled in the server config. 2019-06-10 16:56:53 +01:00
Brendan Abolivier
9cce175bf0 Move schema update to 54 and add a warning comment 2019-06-10 15:34:41 +01:00
Brendan Abolivier
4f0e9a3f01 New line 2019-06-10 15:22:55 +01:00
Brendan Abolivier
35eea39c8c Add unique index to the profile_replication_status table. 2019-06-10 15:18:22 +01:00
Brendan Abolivier
8d16321edc Merge branch 'babolivier/account_validity_send_mail_auth' into dinsic 2019-06-10 11:59:20 +01:00
Brendan Abolivier
028f674cd3 Better wording 2019-06-10 11:35:54 +01:00
Brendan Abolivier
4914a88829 Doc 2019-06-10 11:34:45 +01:00
Brendan Abolivier
ccbc9e5e17 Gah towncrier 2019-06-05 16:41:26 +01:00
Brendan Abolivier
d51ca9d9b3 Changelog 2019-06-05 16:38:51 +01:00
Brendan Abolivier
fe13bd52ac Don't check whether the user's account is expired on /send_mail requests 2019-06-05 16:35:05 +01:00
Brendan Abolivier
116f7778f4 Merge branch 'babolivier/email_config' into dinsic 2019-06-04 14:49:55 +01:00
Erik Johnston
d8fdba7bfb Merge branch 'babolivier/account_validity_job_delta' of github.com:matrix-org/synapse into dinsic 2019-05-31 12:12:24 +01:00
Brendan Abolivier
dde07c6859 Merge branch 'babolivier/account_validity_fix_schema' into dinsic 2019-05-28 11:36:11 +01:00
Brendan Abolivier
64365fcbdd Merge pull request #5214 from matrix-org/babolivier/password-policy
Allow server admins to define and enforce a password policy (MSC2000)
2019-05-22 11:09:42 +01:00
Brendan Abolivier
4a9eba9576 Test whole dict instead of individual fields 2019-05-22 10:43:23 +01:00
Brendan Abolivier
50cd07a836 Merge branch 'babolivier/account_validity_expiration_date' into dinsic 2019-05-21 14:56:05 +01:00
Brendan Abolivier
7dfc3c327c Improve documentation on generated configuration 2019-05-21 10:49:44 +01:00
Brendan Abolivier
42cea6b437 Make error messages more explicit 2019-05-21 10:21:27 +01:00
Brendan Abolivier
d9105b5ed8 Also test the /password client route 2019-05-21 09:55:32 +01:00
Brendan Abolivier
6fbf2ae9a8 Remove unused import 2019-05-20 19:49:19 +01:00
Brendan Abolivier
ed2b5b77f2 Config and changelog 2019-05-20 19:44:37 +01:00
Brendan Abolivier
6d54f1534a First implementation of MSC2000 2019-05-20 19:42:39 +01:00
Brendan Abolivier
d6e2f9f9da Merge branch 'babolivier/per_room_profiles' into dinsic 2019-05-17 15:07:41 +01:00
Brendan Abolivier
f608ddbe5c Merge branch 'release-v0.99.4' into dinsic 2019-05-14 11:43:03 +01:00
Brendan Abolivier
38e3d9ed67 Merge pull request #5115 from matrix-org/babolivier/lookup_path
[DINSIC] Move 3PID lookup endpoint to CS API and proxy bulk lookups
2019-05-09 15:02:21 +01:00
Brendan Abolivier
aa5c42f5bc Fix failing test 2019-05-09 13:59:57 +01:00
Brendan Abolivier
8bcb78891e Merge branch 'babolivier/lookup_path' of github.com:matrix-org/synapse into babolivier/lookup_path 2019-05-09 13:09:19 +01:00
Brendan Abolivier
f304f1a574 Incorporate review 2019-05-09 13:07:43 +01:00
Brendan Abolivier
532ba44add Fix test 2019-05-09 13:01:40 +01:00
Brendan Abolivier
10e3ed83e9 Check if Synapse should check given ISs 2019-05-09 12:53:24 +01:00
Richard van der Hoff
622ecec24b Merge remote-tracking branch 'origin/dinsic' into babolivier/lookup_path 2019-05-09 11:06:02 +01:00
Richard van der Hoff
09ea63ae7a Merge pull request #5098 from matrix-org/rav/fix_pep_517
Workarounds for pep-517 errors
2019-05-08 18:13:15 +01:00
Brendan Abolivier
f059a91085 /bulk_lookup doesn't return a signature 2019-05-07 14:16:26 +01:00
Brendan Abolivier
66f7588f87 Fix expected key in bulk lookup response 2019-05-07 12:06:24 +01:00
Brendan Abolivier
b4f3d70b21 Incorporate review 2019-05-07 11:47:37 +01:00
Brendan Abolivier
3d031c211d Fix error message 2019-05-01 10:10:38 +01:00
Andrew Morgan
371296443f Update synapse/handlers/identity.py
Co-Authored-By: babolivier <contact@brendanabolivier.com>
2019-05-01 10:03:46 +01:00
Andrew Morgan
1973eb11d2 Update synapse/rest/client/v2_alpha/account.py
Co-Authored-By: babolivier <contact@brendanabolivier.com>
2019-05-01 10:02:18 +01:00
Andrew Morgan
26c004129f Update synapse/rest/client/v2_alpha/account.py
Co-Authored-By: babolivier <contact@brendanabolivier.com>
2019-05-01 10:01:57 +01:00
Richard van der Hoff
0cc91efd6a Merge pull request #5098 from matrix-org/rav/fix_pep_517
Workarounds for pep-517 errors
2019-04-30 18:33:19 +01:00
Brendan Abolivier
fe6ac9c5d7 Fixed return value 2019-04-30 18:28:01 +01:00
Brendan Abolivier
96bd70f6d0 Bunch of tests 2019-04-30 18:22:52 +01:00
Brendan Abolivier
3a9c405a0f Fix url and method 2019-04-30 18:22:42 +01:00
Brendan Abolivier
909ceecc28 Tests 2019-04-30 17:27:58 +01:00
Brendan Abolivier
70da5202ba Fixes 2019-04-30 17:27:47 +01:00
Brendan Abolivier
34bbbe81a6 Fix method 2019-04-30 16:59:46 +01:00
Brendan Abolivier
2d979e639d Register servlets 2019-04-30 16:51:50 +01:00
Brendan Abolivier
d296cdc9dd Add bulk lookup 2019-04-30 15:15:02 +01:00
Brendan Abolivier
8df16a8aee Move lookup endpoint to CS API (and s/is_server/id_server/) 2019-04-30 15:11:21 +01:00
Matthew Hodgson
15b7a84aa8 changelog 2019-04-21 01:01:40 +01:00
Andrew Morgan
2f61dd058d [DINSIC] Add ability to proxy identity lookups (#5048) 2019-04-16 17:41:01 +01:00
Andrew Morgan
e6218e4880 [DINSIC] Block internal users from inviting external users to a public room (#5061)
Co-Authored-By: babolivier <contact@brendanabolivier.com>
2019-04-16 16:41:01 +01:00
Andrew Morgan
7d71975e6a Merge branch 'develop' into dinsic 2019-04-15 15:22:14 +01:00
Brendan Abolivier
0fcf7e5c57 Add config option to block users from looking up 3PIDs (#5010) 2019-04-05 14:25:27 +01:00
Erik Johnston
9bf49abc07 Merge branch 'master' of github.com:matrix-org/synapse into dinsic 2019-04-05 14:10:16 +01:00
Michael Kaye
dd747ba045 Prevent exception when hs or shadow_hs missing (#5012)
* Prevent exception when hs or shadow_hs missing
2019-04-04 17:45:12 +01:00
Erik Johnston
b00a8d870c Fix registration with register_mxid_from_3pid enabled (#5011) 2019-04-04 16:45:15 +01:00
Michael Kaye
8fa09c7479 Rewrite more internal IS urls. (#5006)
* Rewrite more internal IS urls.
2019-04-04 15:28:37 +01:00
Erik Johnston
d94873d525 Merge pull request #4997 from matrix-org/erikj/dinsic_email_display_name
Add config option to use email as display name for new users
2019-04-03 10:57:20 +01:00
Erik Johnston
f2fa172375 Sample config 2019-04-03 10:40:40 +01:00
Erik Johnston
8c0ebe3026 Add config option to use email as display name for new users 2019-04-03 10:26:56 +01:00
Erik Johnston
53dd358c83 Merge pull request #4910 from matrix-org/erikj/third_party_invite_create_spam
Add third party invite support to spam checker
2019-03-21 16:07:27 +00:00
Erik Johnston
f5c944c7f2 Fix comments 2019-03-21 15:31:34 +00:00
Erik Johnston
aefb7a1146 Update sample config 2019-03-21 12:52:22 +00:00
Erik Johnston
479b7b1eff Add another test 2019-03-21 12:48:37 +00:00
Erik Johnston
f04ee0b351 Run black on tests/rulecheck/ 2019-03-21 12:48:37 +00:00
Erik Johnston
164798ec32 Add 3PID invite support to spam checker 2019-03-21 12:48:37 +00:00
Erik Johnston
b7d7d20a38 Correctly handle 3PID invites in create room spam check
We also add an option to outright deny third party invites
2019-03-21 12:48:37 +00:00
Erik Johnston
2bb6d85736 Turn off newsfile check 2019-03-21 12:48:25 +00:00
Erik Johnston
0f0671e5e0 Merge branch 'develop' of github.com:matrix-org/synapse into erikj/dinsic-merged 2019-03-21 12:41:42 +00:00
Erik Johnston
8eb9f37a01 Merge pull request #4875 from matrix-org/erikj/spam_checker
Extend spam checking rules
2019-03-18 16:28:46 +00:00
Erik Johnston
ea89e73ebf Add unit tests 2019-03-18 15:56:08 +00:00
Erik Johnston
68a9d1fc34 Add rules to DomainRuleChecker 2019-03-18 15:56:04 +00:00
Erik Johnston
feae387576 Don't spam check actions by admins 2019-03-18 15:51:00 +00:00
Erik Johnston
e64f7c0188 Run black on tests/rulecheck/test_domainrulecheck.py 2019-03-18 15:51:00 +00:00
Erik Johnston
b85ff4b894 Add user_may_join_room spam check 2019-03-18 15:51:00 +00:00
Erik Johnston
4eca8d3fb3 Add invite_list and cloning param to create room rule 2019-03-18 15:50:55 +00:00
Michael Kaye
aa3dc78f65 Merge pull request #4862 from matrix-org/erikj/dinsic-merged-master
Merge master into dinsic
2019-03-15 17:21:22 +01:00
Erik Johnston
de874364e7 Fixup config 2019-03-15 14:41:27 +00:00
Erik Johnston
7e94e2ad94 Disable changelog CI checks 2019-03-15 14:40:28 +00:00
Erik Johnston
e7ec6f78ca Manifest 2019-03-15 14:39:25 +00:00
Erik Johnston
7a9aa4b81b Isort 2019-03-15 14:39:25 +00:00
Erik Johnston
5827e976fe Fixups 2019-03-15 14:39:25 +00:00
Erik Johnston
44c0661d97 Merge branch 'master' of github.com:matrix-org/synapse into erikj/dinsic-merged-master 2019-03-15 14:36:44 +00:00
Erik Johnston
85db96cc81 Add some debug logging for device list handling 2019-03-08 12:31:41 +00:00
Andrew Morgan
3271742905 Merge pull request #4681 from matrix-org/dinsic_anoa/info_split
[DINSIC] Use internal-info for identity server
2019-02-28 11:28:55 +00:00
Andrew Morgan
0a23bf442f Merge branch 'dinsic_anoa/info_split' of github.com:matrix-org/synapse into dinsic_anoa/info_split 2019-02-27 15:23:19 +00:00
Andrew Morgan
9b13038d05 Check shadow_hs as well as hs during 3pid reg 2019-02-27 15:22:41 +00:00
Andrew Morgan
dfe09ec313 Merge pull request #4747 from matrix-org/anoa/public_rooms_federate
[DINSIC] Prevent showing non-fed rooms in fed /publicRooms
2019-02-26 14:40:45 +00:00
Richard van der Hoff
13bc1e5307 Update synapse/util/threepids.py
Co-Authored-By: anoadragon453 <1342360+anoadragon453@users.noreply.github.com>
2019-02-25 16:08:58 +00:00
Andrew Morgan
70e039c7ae Use internal-info for identity server. Block reg on fields 2019-02-19 15:29:49 +00:00
Michael Kaye
2712a9ef8f Fix to keep the signature valid for the request. (#4480) 2019-01-28 14:24:28 +00:00
Michael Kaye
da757b7759 Rewrite identity server URLs as required. (#4393)
This takes a mapping of <old> -> <new> and looks up and replaces as
required.
2019-01-21 15:48:15 +00:00
Michael Kaye
59bc7debf0 When performing shadow registration, don't inhibit login. (#4385) 2019-01-14 18:01:34 +00:00
Matthew Hodgson
cf68593544 Synchronise account metadata onto another server. (#4145)
* implement shadow registration via AS (untested)
* shadow support for 3pid binding/unbinding (untested)
2019-01-11 15:50:28 +00:00
Michael Kaye
9cc95fd0a5 Merge pull request #4148 from matrix-org/matthew/red_list
add im.vector.hide_profile to user account_data
2018-12-19 15:42:19 +00:00
Matthew Hodgson
82886e4c8f hide_profile: false should mean not to hide the profile :/ 2018-12-12 00:57:39 +00:00
Matthew Hodgson
08919847c1 fix sense of 'hide' 2018-11-04 23:49:57 +00:00
Matthew Hodgson
c3acc45a87 pull in profile_handler correctly 2018-11-04 23:49:19 +00:00
Matthew Hodgson
ae5bb32ad0 add im.vector.hide_profile to user account_data 2018-11-04 23:46:11 +00:00
Matthew Hodgson
7ed3232b08 fix tests 2018-11-03 12:58:25 +00:00
Matthew Hodgson
6e7488ce11 merge master into dinsic, again... 2018-11-03 12:14:24 +00:00
Michael Kaye
41585e1340 Merge pull request #4047 from matrix-org/michaelkaye/dinsic_allow_user_directory_url
user_directory.defer_to_id_server should be an URL, not a hostname
2018-11-02 12:00:18 +00:00
Matthew Hodgson
9498cd3e7b fix conflict and reinstate 6372dff771 2018-10-22 20:27:25 +02:00
Matthew Hodgson
c7503f8f33 merge in master 2018-10-22 20:19:40 +02:00
Michael Kaye
9d8baa1595 Allow us to configure http vs https for user_directory 2018-10-17 11:38:48 +01:00
Matthew Hodgson
4ff8486f0f fix missing import and run isort 2018-07-20 11:29:18 +01:00
David Baker
2669e494e0 Merge remote-tracking branch 'origin/master' into dinsic 2018-07-19 18:25:25 +01:00
David Baker
b6d8a808a4 Merge pull request #3557 from matrix-org/dbkr/delete_profiles
Remove deactivated users from profile search
2018-07-19 15:58:40 +01:00
David Baker
0cb5d34756 Hopefully fix postgres 2018-07-19 15:12:48 +01:00
David Baker
650761666d More run_on_reactor 2018-07-19 14:52:35 +01:00
David Baker
aa2a4b4b42 run_on_reactor is dead 2018-07-19 14:48:24 +01:00
David Baker
022469d819 Change column def so it works on pgsql & sqlite
Now I remember discovering previously there was no way to make boolean
columns work
2018-07-19 10:28:26 +01:00
David Baker
45d06c754a Add hopefully enlightening comment 2018-07-18 20:52:21 +01:00
David Baker
dbd0821c43 Oops, didn't mean to commit that 2018-07-18 20:50:20 +01:00
David Baker
0476852fc6 Remove deactivated users from profile search 2018-07-18 18:05:29 +01:00
David Baker
1d11d9323d Merge remote-tracking branch 'origin/master' into dinsic 2018-07-17 15:43:33 +01:00
Michael Kaye
261e4f2542 Merge pull request #3502 from matrix-org/matthew/dinsic-tweak-display-names
tweak dinsic display names
2018-07-10 15:59:26 +01:00
Matthew Hodgson
11728561f3 improve domain matches 2018-07-10 15:21:14 +01:00
Matthew Hodgson
9d57abcadd fix bounds error 2018-07-10 13:58:01 +01:00
Matthew Hodgson
cb0bbde981 tweak dinsic display names 2018-07-10 13:56:32 +01:00
Matthew Hodgson
abc97bd1de Merge pull request #3487 from matrix-org/matthew/dinsic-encrypt-for-invited-users
Query the device lists of users when they are invited to a room.
2018-07-10 12:21:57 +01:00
Matthew Hodgson
ee238254a0 Query the device lists of users when they are invited to a room.
Previously we only queried the device list when the user joined the room; now we
do it when they are invited too.  This means that new messages can be encrypted
for the devices of the invited user as of the point they were invited.

WARNING: This commit has two major problems however:
 1. If the invited user adds devices after being invited but before joining, the
    device-list will not be updated to the other servers in the room (as we don't
    know who those servers are).
 2. This introduces a regression, as previously the device-list would be correctly
    updated when when user joined the room.  However, this resync doesn't happen
    now, so devices which joined after the invite and before the join may never
    be added to the device-list.

This is being merged for DINSIC given the edge case of adding devices between
invite & join is pretty rare in their use case, but before it can be merged to
synapse in general we need to at least re-sync the devicelist when the user joins
or to implement some kind of pubsub mechanism to let interested servers subscribe
to devicelist updates on other servers irrespective of user join/invite membership.

This was originally https://github.com/matrix-org/synapse/pull/3484
2018-07-06 16:29:08 +02:00
Matthew Hodgson
0125b5d002 typos 2018-06-25 17:37:00 +01:00
Michael Kaye
fe265fe990 Merge tag 'v0.31.2' into dinsic 2018-06-22 17:04:50 +01:00
David Baker
7735eee41d Merge pull request #3426 from matrix-org/dbkr/e2e_by_default
Server-enforced e2e for private rooms
2018-06-22 16:49:42 +01:00
David Baker
3d0faa39fb Add m.encryption to event types 2018-06-22 16:47:49 +01:00
David Baker
fd28d13e19 Server-enforced e2e for private rooms 2018-06-22 13:54:17 +01:00
Michael Kaye
d18731e252 Merge pull request #3202 from matrix-org/michaelkaye/domain_based_rule_checker
DomainRuleChecker
2018-05-21 09:32:47 +01:00
Michael Kaye
81beae30b8 Update with documentation suggestions 2018-05-18 16:12:22 +01:00
Michael Kaye
11f1bace3c Address PR feedback
- add code and test to handle configuration of an empty array
- move docstrings around and update class level documentation
2018-05-11 12:51:03 +01:00
Michael Kaye
1e8cfc9e77 pep8 style fixes 2018-05-09 15:11:19 +01:00
Michael Kaye
488ed3e444 Generic "are users in domain X allowed to invite users in domain Y" logic 2018-05-09 14:50:48 +01:00
Matthew Hodgson
c3ec84dbcd Merge pull request #3096 from matrix-org/matthew/derive-mxid-from-3pid
add the register_mxid_from_3pid setting (untested)
2018-05-04 02:26:46 +01:00
Matthew Hodgson
0783801659 unbreak tests 2018-05-04 02:18:01 +01:00
Matthew Hodgson
9f2fd29c14 fix double negative 2018-05-04 02:11:22 +01:00
Matthew Hodgson
6372dff771 remove create_profile from tests 2018-05-04 01:58:45 +01:00
Matthew Hodgson
b3e346f40c don't pass a requester if we don't have one to set_displayname 2018-05-04 01:56:01 +01:00
Matthew Hodgson
fb47ce3e6a make set_profile_* an upsert rather than update, now create_profile is gone 2018-05-04 01:46:26 +01:00
Matthew Hodgson
debf04556b fix user in user regexp 2018-05-04 01:15:33 +01:00
Matrix
907a62df28 fix strip_invalid_mxid_characters 2018-05-03 23:54:36 +00:00
Matrix
41b987cbc5 unbreak 3pid deletion 2018-05-03 23:54:36 +00:00
Matthew Hodgson
5c74ab4064 fix user_id / user confusion 2018-05-04 00:53:56 +01:00
Matrix
06820250c9 unbreak 3pid deletion 2018-05-03 23:27:34 +00:00
Matthew Hodgson
383c4ae59c Merge branch 'dinsic' into matthew/derive-mxid-from-3pid 2018-05-03 23:39:08 +01:00
Matthew Hodgson
f639ac143d Merge pull request #3180 from matrix-org/matthew/disable-3pid-changes
add option to disable changes to the 3PIDs for an account.
2018-05-03 18:19:58 +01:00
Matthew Hodgson
ad0424bab0 Merge pull request #3179 from matrix-org/matthew/disable-set-profile
options to disable setting profile info
2018-05-03 18:19:48 +01:00
Matthew Hodgson
2992125561 special case msisdns when deriving mxids from 3pids 2018-05-03 17:52:46 +01:00
David Baker
ef56b6e27c Merge pull request #3185 from matrix-org/dbkr/change_profile_replication_uri
Change profile replication URI
2018-05-03 15:17:51 +01:00
David Baker
53d6245529 Change profile replication URI 2018-05-03 14:55:40 +01:00
Matthew Hodgson
25e471dac3 fix defaults in config example 2018-05-03 11:46:56 +01:00
Matthew Hodgson
76fca1730e fix defaults in example config 2018-05-03 11:46:11 +01:00
Matthew Hodgson
32e4420a66 improve mxid & displayname selection for register_mxid_from_3pid
* [x] strip invalid characters from generated mxid
* [x] append numbers to disambiguate clashing mxids
* [x] generate displayanames from 3pids using a dodgy heuristic
* [x] get rid of the create_profile_with_localpart and instead
      explicitly set displaynames so they propagate correctly
2018-05-03 04:21:20 +01:00
Matthew Hodgson
79b2583f1b Merge branch 'dinsic' into matthew/derive-mxid-from-3pid 2018-05-03 02:51:49 +01:00
Matthew Hodgson
8a24c4eee5 add option to disable changes to the 3PIDs for an account.
This only considers the /account/3pid API, which should be sufficient
as currently we can't change emails associated with push notifs
(which are provisioned at registration), and we can't directly create
mappings for accounts in an IS other than by answering an invite
2018-05-03 02:47:55 +01:00
Matthew Hodgson
f93cb7410d options to disable setting profile info 2018-05-03 01:29:12 +01:00
Matthew Hodgson
50d5a97c1b Merge branch 'master' into dinsic 2018-05-03 00:26:33 +01:00
David Baker
c06932a029 Merge pull request #3166 from matrix-org/dbkr/postgres_doesnt_have_ifnull
Use COALESCE rather than IFNULL
2018-05-01 18:15:28 +01:00
David Baker
3a62cacfb0 Use COALESCE rather than IFNULL
as this works on sqlite and postgres (postgres doesn't have IFNULL)
2018-05-01 17:54:03 +01:00
David Baker
4d55b16faa Fix python synatx 2018-05-01 14:32:30 +01:00
David Baker
105709bf32 Fix profile repl 2018-05-01 14:27:14 +01:00
David Baker
d7fad867fa Merge pull request #3123 from matrix-org/dbkr/user_directory_defer_to_is
Option to defer user_directory search to an ID server
2018-04-27 17:18:13 +01:00
David Baker
8fddcf703e Merge remote-tracking branch 'origin/dinsic' into dbkr/user_directory_defer_to_is 2018-04-26 10:23:12 +01:00
David Baker
e2adb360eb Merge pull request #3112 from matrix-org/dbkr/profile_replication
Option to replicate user profiles to another server
2018-04-26 10:22:28 +01:00
David Baker
47ed4a4aa7 PR feedback
Unnecessary inlineCallbacks, missing yield, SQL comments & trailing
commas.
2018-04-25 13:58:37 +01:00
David Baker
7fafa838ae Comment why the looping call loops 2018-04-25 11:59:22 +01:00
David Baker
de341bec1b Add 'ex[erimental API' comment 2018-04-25 11:51:57 +01:00
David Baker
643c89d497 Fix spelling & add experimental API comment 2018-04-25 11:40:37 +01:00
David Baker
6554253f48 Option to defer to an ID server for user_directory 2018-04-19 19:28:12 +01:00
David Baker
3add16df49 pep8 again 2018-04-17 13:23:16 +01:00
David Baker
dde01efbcb Don't do profile repl if no repl targets 2018-04-17 12:26:45 +01:00
David Baker
22e416b726 Update profile cache only on master
and same for the profile replication
2018-04-17 12:17:16 +01:00
David Baker
b4b7c80181 Fix other tests 2018-04-17 11:03:10 +01:00
David Baker
5fc3477fd3 Fix tests 2018-04-17 10:46:49 +01:00
David Baker
8743f42b49 pep8 2018-04-17 10:34:04 +01:00
David Baker
7285afa4be Handle current batch number being null 2018-04-17 10:28:00 +01:00
Matthew Hodgson
b22a53e357 turn @'s to -'s rather than .'s 2018-04-17 09:32:42 +01:00
David Baker
3c446d0a81 Merge remote-tracking branch 'origin/dinsic' into dbkr/profile_replication 2018-04-16 18:35:25 +01:00
Matthew Hodgson
240e940c3f handle medium checks correctly 2018-04-12 18:30:32 +01:00
Matthew Hodgson
969ed2e49d add the register_mxid_from_3pid setting (untested) 2018-04-12 18:20:51 +01:00
David Baker
1147ce7e18 Include origin_server in the sig!
Also be consistent with underscores
2018-04-12 17:59:37 +01:00
Matthew Hodgson
0d2b7fdcec Merge branch 'develop' into dinsic 2018-04-12 13:29:57 +01:00
David Baker
4e12b10c7c Trigger profile replication on profile change 2018-04-11 10:17:07 +01:00
David Baker
e654230a51 Written but untested profile replication 2018-04-10 17:41:58 +01:00
Matthew Hodgson
ef5193e0cb Merge pull request #2973 from matrix-org/matthew/dinsic_3pid_check
Delegate 3PID registration determination to experimental IS API
2018-03-14 22:35:58 +00:00
Matthew Hodgson
7b3959c7f3 Merge branch 'develop' into dinsic 2018-03-14 22:31:34 +00:00
Matthew Hodgson
2e4a6c5aab incorporate PR feedback and rename URL 2018-03-14 22:09:08 +00:00
Matthew Hodgson
e3eb2cfe8b Merge branch 'develop' into matthew/dinsic_3pid_check 2018-03-14 21:56:58 +00:00
Matthew Hodgson
5c341c99f6 add 'allow_invited_3pids' option to invited 3PIDs to register 2018-03-13 21:15:14 +00:00
Matthew Hodgson
739d3500fe pep8 2018-03-13 01:50:32 +00:00
Matthew Hodgson
0e2d70e101 typos 2018-03-13 01:41:20 +00:00
Matthew Hodgson
82c4fd7226 add yields 2018-03-13 01:38:02 +00:00
Matthew Hodgson
e446077478 delegate to the IS to check 3PID signup eligibility 2018-03-13 01:34:20 +00:00
Matthew Hodgson
d82c89ac22 fix thinko on 3pid whitelisting 2018-01-24 11:07:24 +01:00
Matthew Hodgson
75b25b3f1f Merge branch 'develop' into dinsic 2018-01-23 10:00:53 +01:00
AmandineLP
1df10d8814 Fixed translation 2018-01-22 21:18:44 +01:00
AmandineLP
8f9340d248 Fixed translation 2018-01-22 21:17:36 +01:00
AmandineLP
c5034cd4b0 More translation 2018-01-22 21:16:46 +01:00
AmandineLP
f7f937d051 Translate to FR 2018-01-22 21:14:13 +01:00
AmandineLP
e52b5d94a9 Translate to FR 2018-01-22 21:13:03 +01:00
AmandineLP
d90f27a21f Translate to FR 2018-01-22 21:12:06 +01:00
AmandineLP
03cf9710e3 Translate to FR 2018-01-22 21:10:00 +01:00
AmandineLP
1dcdd8d568 Translate to FR 2018-01-22 20:02:47 +01:00
AmandineLP
4344fb1faf translate to FR 2018-01-22 20:01:00 +01:00
Matthew Hodgson
846577ebde fork notif templates 2018-01-22 19:55:27 +01:00
Matthew Hodgson
3869981227 remove unreachable except block 2018-01-22 18:43:41 +01:00
Matthew Hodgson
fa80b492a5 fix thinko 2018-01-22 18:43:41 +01:00
Richard van der Hoff
c776c52eed Back out unrelated changes 2018-01-22 16:44:39 +00:00
Matthew Hodgson
b424c16f50 fix tests 2018-01-22 15:25:25 +01:00
Matthew Hodgson
313a489fc9 incorporate PR feedback 2018-01-22 14:54:46 +01:00
Matthew Hodgson
4b090cb273 add federation_domain_whitelist
gives a way to restrict which domains your HS is allowed to federate with.
useful mainly for gracefully preventing a private but internet-connected HS from trying to federate to the wider public Matrix network
2018-01-22 12:13:41 +01:00
Matthew Hodgson
3f79378d4b make replication tests pass on OSX 2018-01-20 17:23:27 +00:00
122 changed files with 6324 additions and 479 deletions

View File

@@ -1,116 +1,8 @@
version: 2 version: 2
jobs: jobs:
dockerhubuploadrelease:
machine: true
steps:
- checkout
- run: docker build -f docker/Dockerfile --label gitsha1=${CIRCLE_SHA1} -t matrixdotorg/synapse:${CIRCLE_TAG}-py2 .
- run: docker build -f docker/Dockerfile --label gitsha1=${CIRCLE_SHA1} -t matrixdotorg/synapse:${CIRCLE_TAG} -t matrixdotorg/synapse:${CIRCLE_TAG}-py3 --build-arg PYTHON_VERSION=3.6 .
- run: docker login --username $DOCKER_HUB_USERNAME --password $DOCKER_HUB_PASSWORD
- run: docker push matrixdotorg/synapse:${CIRCLE_TAG}
- run: docker push matrixdotorg/synapse:${CIRCLE_TAG}-py2
- run: docker push matrixdotorg/synapse:${CIRCLE_TAG}-py3
dockerhubuploadlatest:
machine: true
steps:
- checkout
- run: docker build -f docker/Dockerfile --label gitsha1=${CIRCLE_SHA1} -t matrixdotorg/synapse:latest-py2 .
- run: docker build -f docker/Dockerfile --label gitsha1=${CIRCLE_SHA1} -t matrixdotorg/synapse:latest -t matrixdotorg/synapse:latest-py3 --build-arg PYTHON_VERSION=3.6 .
- run: docker login --username $DOCKER_HUB_USERNAME --password $DOCKER_HUB_PASSWORD
- run: docker push matrixdotorg/synapse:latest
- run: docker push matrixdotorg/synapse:latest-py2
- run: docker push matrixdotorg/synapse:latest-py3
sytestpy2:
docker:
- image: matrixdotorg/sytest-synapsepy2
working_directory: /src
steps:
- checkout
- run: /synapse_sytest.sh
- store_artifacts:
path: /logs
destination: logs
- store_test_results:
path: /logs
sytestpy2postgres:
docker:
- image: matrixdotorg/sytest-synapsepy2
working_directory: /src
steps:
- checkout
- run: POSTGRES=1 /synapse_sytest.sh
- store_artifacts:
path: /logs
destination: logs
- store_test_results:
path: /logs
sytestpy2merged:
docker:
- image: matrixdotorg/sytest-synapsepy2
working_directory: /src
steps:
- checkout
- run: bash .circleci/merge_base_branch.sh
- run: /synapse_sytest.sh
- store_artifacts:
path: /logs
destination: logs
- store_test_results:
path: /logs
sytestpy2postgresmerged: sytestpy2postgresmerged:
docker: docker:
- image: matrixdotorg/sytest-synapsepy2 - image: matrixdotorg/sytest-synapse:dinsic
working_directory: /src
steps:
- checkout
- run: bash .circleci/merge_base_branch.sh
- run: POSTGRES=1 /synapse_sytest.sh
- store_artifacts:
path: /logs
destination: logs
- store_test_results:
path: /logs
sytestpy3:
docker:
- image: matrixdotorg/sytest-synapsepy3
working_directory: /src
steps:
- checkout
- run: /synapse_sytest.sh
- store_artifacts:
path: /logs
destination: logs
- store_test_results:
path: /logs
sytestpy3postgres:
docker:
- image: matrixdotorg/sytest-synapsepy3
working_directory: /src
steps:
- checkout
- run: POSTGRES=1 /synapse_sytest.sh
- store_artifacts:
path: /logs
destination: logs
- store_test_results:
path: /logs
sytestpy3merged:
docker:
- image: matrixdotorg/sytest-synapsepy3
working_directory: /src
steps:
- checkout
- run: bash .circleci/merge_base_branch.sh
- run: /synapse_sytest.sh
- store_artifacts:
path: /logs
destination: logs
- store_test_results:
path: /logs
sytestpy3postgresmerged:
docker:
- image: matrixdotorg/sytest-synapsepy3
working_directory: /src working_directory: /src
steps: steps:
- checkout - checkout
@@ -126,45 +18,7 @@ workflows:
version: 2 version: 2
build: build:
jobs: jobs:
- sytestpy2:
filters:
branches:
only: /develop|master|release-.*/
- sytestpy2postgres:
filters:
branches:
only: /develop|master|release-.*/
- sytestpy3:
filters:
branches:
only: /develop|master|release-.*/
- sytestpy3postgres:
filters:
branches:
only: /develop|master|release-.*/
- sytestpy2merged:
filters:
branches:
ignore: /develop|master|release-.*/
- sytestpy2postgresmerged: - sytestpy2postgresmerged:
filters: filters:
branches: branches:
ignore: /develop|master|release-.*/ ignore: /develop|master|release-.*/
- sytestpy3merged:
filters:
branches:
ignore: /develop|master|release-.*/
- sytestpy3postgresmerged:
filters:
branches:
ignore: /develop|master|release-.*/
- dockerhubuploadrelease:
filters:
tags:
only: /v[0-9].[0-9]+.[0-9]+.*/
branches:
ignore: /.*/
- dockerhubuploadlatest:
filters:
branches:
only: master

View File

@@ -9,11 +9,12 @@ source $BASH_ENV
if [[ -z "${CIRCLE_PR_NUMBER}" ]] if [[ -z "${CIRCLE_PR_NUMBER}" ]]
then then
echo "Can't figure out what the PR number is! Assuming merge target is develop." echo "Can't figure out what the PR number is! Assuming merge target is dinsic."
# It probably hasn't had a PR opened yet. Since all PRs land on develop, we # It probably hasn't had a PR opened yet. Since all PRs for dinsic land on
# can probably assume it's based on it and will be merged into it. # dinsic, we can probably assume it's based on it and will be merged into
GITBASE="develop" # it.
GITBASE="dinsic"
else else
# Get the reference, using the GitHub API # Get the reference, using the GitHub API
GITBASE=`wget -O- https://api.github.com/repos/matrix-org/synapse/pulls/${CIRCLE_PR_NUMBER} | jq -r '.base.ref'` GITBASE=`wget -O- https://api.github.com/repos/matrix-org/synapse/pulls/${CIRCLE_PR_NUMBER} | jq -r '.base.ref'`

View File

@@ -1,3 +1,24 @@
Synapse 1.0.0 (2019-06-11)
==========================
Bugfixes
--------
- Fix bug where attempting to send transactions with large number of EDUs can fail. ([\#5418](https://github.com/matrix-org/synapse/issues/5418))
Improved Documentation
----------------------
- Expand the federation guide to include relevant content from the MSC1711 FAQ ([\#5419](https://github.com/matrix-org/synapse/issues/5419))
Internal Changes
----------------
- Move password reset links to /_matrix/client/unstable namespace. ([\#5424](https://github.com/matrix-org/synapse/issues/5424))
Synapse 1.0.0rc3 (2019-06-10) Synapse 1.0.0rc3 (2019-06-10)
============================= =============================

View File

@@ -1,14 +1,14 @@
* [Installing Synapse](#installing-synapse) - [Installing Synapse](#installing-synapse)
* [Installing from source](#installing-from-source) - [Installing from source](#installing-from-source)
* [Platform-Specific Instructions](#platform-specific-instructions) - [Platform-Specific Instructions](#platform-specific-instructions)
* [Troubleshooting Installation](#troubleshooting-installation) - [Troubleshooting Installation](#troubleshooting-installation)
* [Prebuilt packages](#prebuilt-packages) - [Prebuilt packages](#prebuilt-packages)
* [Setting up Synapse](#setting-up-synapse) - [Setting up Synapse](#setting-up-synapse)
* [TLS certificates](#tls-certificates) - [TLS certificates](#tls-certificates)
* [Email](#email) - [Email](#email)
* [Registering a user](#registering-a-user) - [Registering a user](#registering-a-user)
* [Setting up a TURN server](#setting-up-a-turn-server) - [Setting up a TURN server](#setting-up-a-turn-server)
* [URL previews](#url-previews) - [URL previews](#url-previews)
# Installing Synapse # Installing Synapse
@@ -395,8 +395,9 @@ To configure Synapse to expose an HTTPS port, you will need to edit
instance, if using certbot, use `fullchain.pem` as your certificate, not instance, if using certbot, use `fullchain.pem` as your certificate, not
`cert.pem`). `cert.pem`).
For those of you upgrading your TLS certificate for Synapse 1.0 compliance, For a more detailed guide to configuring your server for federation, see
please take a look at [our guide](docs/MSC1711_certificates_FAQ.md#configuring-certificates-for-compatibility-with-synapse-100). [federate.md](docs/federate.md)
## Email ## Email

View File

@@ -48,3 +48,8 @@ prune .buildkite
exclude jenkins* exclude jenkins*
recursive-exclude jenkins *.sh recursive-exclude jenkins *.sh
# FIXME: we shouldn't have these templates here
recursive-include res/templates-dinsic *.css
recursive-include res/templates-dinsic *.html
recursive-include res/templates-dinsic *.txt

1
changelog.d/5083.feature Normal file
View File

@@ -0,0 +1 @@
Adds auth_profile_reqs option to require access_token to GET /profile endpoints on CS API.

1
changelog.d/5098.misc Normal file
View File

@@ -0,0 +1 @@
Add workarounds for pep-517 install errors.

1
changelog.d/5214.feature Normal file
View File

@@ -0,0 +1 @@
Allow server admins to define and enforce a password policy (MSC2000).

1
changelog.d/5363.feature Normal file
View File

@@ -0,0 +1 @@
Allow expired user to trigger renewal email sending manually.

1
changelog.d/5378.misc Normal file
View File

@@ -0,0 +1 @@
Track deactivated accounts in the database.

1
changelog.d/5394.bugfix Normal file
View File

@@ -0,0 +1 @@
Fix a bug where deactivated users could receive renewal emails if the account validity feature is on.

1
changelog.d/5416.misc Normal file
View File

@@ -0,0 +1 @@
Add unique index to the profile_replication_status table.

View File

@@ -1 +0,0 @@
Fix bug where attempting to send transactions with large number of EDUs can fail.

1
changelog.d/5420.feature Normal file
View File

@@ -0,0 +1 @@
Add configuration option to hide new users from the user directory.

1
changelog.d/5464.bugfix Normal file
View File

@@ -0,0 +1 @@
Fix missing invite state after exchanging 3PID invites over federaton.

2
changelog.d/5465.misc Normal file
View File

@@ -0,0 +1,2 @@
Track deactivated accounts in the database.

1
changelog.d/5474.feature Normal file
View File

@@ -0,0 +1 @@
Allow server admins to define implementations of extra rules for allowing or denying incoming events.

1
changelog.d/5477.feature Normal file
View File

@@ -0,0 +1 @@
Allow server admins to define implementations of extra rules for allowing or denying incoming events.

1
changelog.d/5493.misc Normal file
View File

@@ -0,0 +1 @@
Track deactivated accounts in the database.

1
changelog.d/5534.feature Normal file
View File

@@ -0,0 +1 @@
Split public rooms directory auth config in two settings, in order to manage client auth independently from the federation part of it. Obsoletes the "restrict_public_rooms_to_local_users" configuration setting. If "restrict_public_rooms_to_local_users" is set in the config, Synapse will act as if both new options are enabled, i.e. require authentication through the client API and deny federation requests.

1
changelog.d/5576.bugfix Normal file
View File

@@ -0,0 +1 @@
Fix a bug that would cause invited users to receive several emails for a single 3PID invite in case the inviter is rate limited.

1
changelog.d/5610.feature Normal file
View File

@@ -0,0 +1 @@
Implement new custom event rules for power levels.

1
changelog.d/5644.bugfix Normal file
View File

@@ -0,0 +1 @@
Fix newly-registered users not being able to lookup their own profile without joining a room.

1
changelog.d/5702.bugfix Normal file
View File

@@ -0,0 +1 @@
Fix 3PID invite to invite association detection in the Tchap room access rules.

1
changelog.d/5760.feature Normal file
View File

@@ -0,0 +1 @@
Force the access rule to be "restricted" if the join rule is "public".

1
changelog.d/5780.misc Normal file
View File

@@ -0,0 +1 @@
Allow looping calls to be given arguments.

1
changelog.d/5807.feature Normal file
View File

@@ -0,0 +1 @@
Allow defining HTML templates to serve the user on account renewal attempt when using the account validity feature.

1
changelog.d/5815.feature Normal file
View File

@@ -0,0 +1 @@
Implement per-room message retention policies.

6
debian/changelog vendored
View File

@@ -1,3 +1,9 @@
matrix-synapse-py3 (1.0.0) stable; urgency=medium
* New synapse release 1.0.0.
-- Synapse Packaging team <packages@matrix.org> Tue, 11 Jun 2019 17:09:53 +0100
matrix-synapse-py3 (0.99.5.2) stable; urgency=medium matrix-synapse-py3 (0.99.5.2) stable; urgency=medium
* New synapse release 0.99.5.2. * New synapse release 0.99.5.2.

View File

@@ -14,7 +14,7 @@ This image is designed to run either with an automatically generated
configuration file or with a custom configuration that requires manual editing. configuration file or with a custom configuration that requires manual editing.
An easy way to make use of this image is via docker-compose. See the An easy way to make use of this image is via docker-compose. See the
[contrib/docker](../contrib/docker) section of the synapse project for [contrib/docker](https://github.com/matrix-org/synapse/tree/master/contrib/docker) section of the synapse project for
examples. examples.
### Without Compose (harder) ### Without Compose (harder)

View File

@@ -1,5 +1,22 @@
# MSC1711 Certificates FAQ # MSC1711 Certificates FAQ
## Historical Note
This document was originally written to guide server admins through the upgrade
path towards Synapse 1.0. Specifically,
[MSC1711](https://github.com/matrix-org/matrix-doc/blob/master/proposals/1711-x509-for-federation.md)
required that all servers present valid TLS certificates on their federation
API. Admins were encouraged to achieve compliance from version 0.99.0 (released
in February 2019) ahead of version 1.0 (released June 2019) enforcing the
certificate checks.
Much of what follows is now outdated since most admins will have already
upgraded, however it may be of use to those with old installs returning to the
project.
If you are setting up a server from scratch you almost certainly should look at
the [installation guide](INSTALL.md) instead.
## Introduction
The goal of Synapse 0.99.0 is to act as a stepping stone to Synapse 1.0.0. It The goal of Synapse 0.99.0 is to act as a stepping stone to Synapse 1.0.0. It
supports the r0.1 release of the server to server specification, but is supports the r0.1 release of the server to server specification, but is
compatible with both the legacy Matrix federation behaviour (pre-r0.1) as well compatible with both the legacy Matrix federation behaviour (pre-r0.1) as well

View File

@@ -14,9 +14,9 @@ up and will work provided you set the ``server_name`` to match your
machine's public DNS hostname, and provide Synapse with a TLS certificate machine's public DNS hostname, and provide Synapse with a TLS certificate
which is valid for your ``server_name``. which is valid for your ``server_name``.
Once you have completed the steps necessary to federate, you should be able to Once federation has been configured, you should be able to join a room over
join a room via federation. (A good place to start is ``#synapse:matrix.org`` - a federation. A good place to start is ``#synapse:matrix.org`` - a room for
room for Synapse admins.) Synapse admins.
## Delegation ## Delegation
@@ -98,6 +98,77 @@ _matrix._tcp.<server_name>``. In our example, we would expect this:
Note that the target of a SRV record cannot be an alias (CNAME record): it has to point Note that the target of a SRV record cannot be an alias (CNAME record): it has to point
directly to the server hosting the synapse instance. directly to the server hosting the synapse instance.
### Delegation FAQ
#### When do I need a SRV record or .well-known URI?
If your homeserver listens on the default federation port (8448), and your
`server_name` points to the host that your homeserver runs on, you do not need an SRV
record or `.well-known/matrix/server` URI.
For instance, if you registered `example.com` and pointed its DNS A record at a
fresh server, you could install Synapse on that host,
giving it a `server_name` of `example.com`, and once [ACME](acme.md) support is enabled,
it would automatically generate a valid TLS certificate for you via Let's Encrypt
and no SRV record or .well-known URI would be needed.
This is the common case, although you can add an SRV record or
`.well-known/matrix/server` URI for completeness if you wish.
**However**, if your server does not listen on port 8448, or if your `server_name`
does not point to the host that your homeserver runs on, you will need to let
other servers know how to find it. The way to do this is via .well-known or an
SRV record.
#### I have created a .well-known URI. Do I still need an SRV record?
As of Synapse 0.99, Synapse will first check for the existence of a .well-known
URI and follow any delegation it suggests. It will only then check for the
existence of an SRV record.
That means that the SRV record will often be redundant. However, you should
remember that there may still be older versions of Synapse in the federation
which do not understand .well-known URIs, so if you removed your SRV record
you would no longer be able to federate with them.
It is therefore best to leave the SRV record in place for now. Synapse 0.34 and
earlier will follow the SRV record (and not care about the invalid
certificate). Synapse 0.99 and later will follow the .well-known URI, with the
correct certificate chain.
#### Can I manage my own certificates rather than having Synapse renew certificates itself?
Yes, you are welcome to manage your certificates yourself. Synapse will only
attempt to obtain certificates from Let's Encrypt if you configure it to do
so.The only requirement is that there is a valid TLS cert present for
federation end points.
#### Do you still recommend against using a reverse proxy on the federation port?
We no longer actively recommend against using a reverse proxy. Many admins will
find it easier to direct federation traffic to a reverse proxy and manage their
own TLS certificates, and this is a supported configuration.
See [reverse_proxy.rst](reverse_proxy.rst) for information on setting up a
reverse proxy.
#### Do I still need to give my TLS certificates to Synapse if I am using a reverse proxy?
Practically speaking, this is no longer necessary.
If you are using a reverse proxy for all of your TLS traffic, then you can set
`no_tls: True` in the Synapse config. In that case, the only reason Synapse
needs the certificate is to populate a legacy `tls_fingerprints` field in the
federation API. This is ignored by Synapse 0.99.0 and later, and the only time
pre-0.99 Synapses will check it is when attempting to fetch the server keys -
and generally this is delegated via `matrix.org`, which will be running a modern
version of Synapse.
#### Do I need the same certificate for the client and federation port?
No. There is nothing stopping you from using different certificates,
particularly if you are using a reverse proxy. However, Synapse will use the
same certificate on any ports where TLS is configured.
## Troubleshooting ## Troubleshooting
You can use the [federation tester]( You can use the [federation tester](

View File

@@ -77,11 +77,15 @@ pid_file: DATADIR/homeserver.pid
# #
#require_auth_for_profile_requests: true #require_auth_for_profile_requests: true
# If set to 'true', requires authentication to access the server's # If set to 'false', requires authentication to access the server's public rooms
# public rooms directory through the client API, and forbids any other # directory through the client API. Defaults to 'true'.
# homeserver to fetch it via federation. Defaults to 'false'.
# #
#restrict_public_rooms_to_local_users: true #allow_public_rooms_without_auth: false
# If set to 'false', forbids any other homeserver to fetch the server's public
# rooms directory via federation. Defaults to 'true'.
#
#allow_public_rooms_over_federation: false
# The default room version for newly created rooms. # The default room version for newly created rooms.
# #
@@ -308,6 +312,74 @@ listeners:
# #
#allow_per_room_profiles: false #allow_per_room_profiles: false
# Whether to show the users on this homeserver in the user directory. Defaults to
# 'true'.
#
#show_users_in_user_directory: false
# Message retention policy at the server level.
#
# Room admins and mods can define a retention period for their rooms using the
# 'm.room.retention' state event, and server admins can cap this period by setting
# the 'allowed_lifetime_min' and 'allowed_lifetime_max' config options.
#
# If this feature is enabled, Synapse will regularly look for and purge events
# which are older than the room's maximum retention period. Synapse will also
# filter events received over federation so that events that should have been
# purged are ignored and not stored again.
#
retention:
# The message retention policies feature is disabled by default. Uncomment the
# following line to enable it.
#
#enabled: true
# Default retention policy. If set, Synapse will apply it to rooms that lack the
# 'm.room.retention' state event. Currently, the value of 'min_lifetime' doesn't
# matter much because Synapse doesn't take it into account yet.
#
#default_policy:
# min_lifetime: 1d
# max_lifetime: 1y
# Retention policy limits. If set, a user won't be able to send a
# 'm.room.retention' event which features a 'min_lifetime' or a 'max_lifetime'
# that's not within this range. This is especially useful in closed federations,
# in which server admins can make sure every federating server applies the same
# rules.
#
#allowed_lifetime_min: 1d
#allowed_lifetime_max: 1y
# Server admins can define the settings of the background jobs purging the
# events which lifetime has expired under the 'purge_jobs' section.
#
# If no configuration is provided, a single job will be set up to delete expired
# events in every room daily.
#
# Each job's configuration defines which range of message lifetimes the job
# takes care of. For example, if 'shortest_max_lifetime' is '2d' and
# 'longest_max_lifetime' is '3d', the job will handle purging expired events in
# rooms whose state defines a 'max_lifetime' that's both higher than 2 days, and
# lower than or equal to 3 days. Both the minimum and the maximum value of a
# range are optional, e.g. a job with no 'shortest_max_lifetime' and a
# 'longest_max_lifetime' of '3d' will handle every room with a retention policy
# which 'max_lifetime' is lower than or equal to three days.
#
# The rationale for this per-job configuration is that some rooms might have a
# retention policy with a low 'max_lifetime', where history needs to be purged
# of outdated messages on a very frequent basis (e.g. every 5min), but not want
# that purge to be performed by a job that's iterating over every room it knows,
# which would be quite heavy on the server.
#
#purge_jobs:
# - shortest_max_lifetime: 1d
# longest_max_lifetime: 3d
# interval: 5m:
# - shortest_max_lifetime: 3d
# longest_max_lifetime: 1y
# interval: 24h
## TLS ## ## TLS ##
@@ -788,6 +860,16 @@ uploads_path: "DATADIR/uploads"
# period: 6w # period: 6w
# renew_at: 1w # renew_at: 1w
# renew_email_subject: "Renew your %(app)s account" # renew_email_subject: "Renew your %(app)s account"
# # Directory in which Synapse will try to find the HTML files to serve to the
# # user when trying to renew an account. Optional, defaults to
# # synapse/res/templates.
# template_dir: "res/templates"
# # HTML to be displayed to the user after they successfully renewed their
# # account. Optional.
# account_renewed_html_path: "account_renewed.html"
# # HTML to be displayed when the user tries to renew an account with an invalid
# # renewal token. Optional.
# invalid_token_html_path: "invalid_token.html"
# The user must provide all of the below types of 3PID when registering. # The user must provide all of the below types of 3PID when registering.
# #
@@ -800,9 +882,32 @@ uploads_path: "DATADIR/uploads"
# #
#disable_msisdn_registration: true #disable_msisdn_registration: true
# Derive the user's matrix ID from a type of 3PID used when registering.
# This overrides any matrix ID the user proposes when calling /register
# The 3PID type should be present in registrations_require_3pid to avoid
# users failing to register if they don't specify the right kind of 3pid.
#
#register_mxid_from_3pid: email
# Uncomment to set the display name of new users to their email address,
# rather than using the default heuristic.
#
#register_just_use_email_for_display_name: true
# Mandate that users are only allowed to associate certain formats of # Mandate that users are only allowed to associate certain formats of
# 3PIDs with accounts on this server. # 3PIDs with accounts on this server.
# #
# Use an Identity Server to establish which 3PIDs are allowed to register?
# Overrides allowed_local_3pids below.
#
#check_is_for_allowed_local_3pids: matrix.org
#
# If you are using an IS you can also check whether that IS registers
# pending invites for the given 3PID (and then allow it to sign up on
# the platform):
#
#allow_invited_3pids: False
#
#allowed_local_3pids: #allowed_local_3pids:
# - medium: email # - medium: email
# pattern: '.*@matrix\.org' # pattern: '.*@matrix\.org'
@@ -811,6 +916,11 @@ uploads_path: "DATADIR/uploads"
# - medium: msisdn # - medium: msisdn
# pattern: '\+44' # pattern: '\+44'
# If true, stop users from trying to change the 3PIDs associated with
# their accounts.
#
#disable_3pid_changes: False
# Enable 3PIDs lookup requests to identity servers from this server. # Enable 3PIDs lookup requests to identity servers from this server.
# #
#enable_3pid_lookup: true #enable_3pid_lookup: true
@@ -852,6 +962,30 @@ uploads_path: "DATADIR/uploads"
# - matrix.org # - matrix.org
# - vector.im # - vector.im
# If enabled, user IDs, display names and avatar URLs will be replicated
# to this server whenever they change.
# This is an experimental API currently implemented by sydent to support
# cross-homeserver user directories.
#
#replicate_user_profiles_to: example.com
# If specified, attempt to replay registrations, profile changes & 3pid
# bindings on the given target homeserver via the AS API. The HS is authed
# via a given AS token.
#
#shadow_server:
# hs_url: https://shadow.example.com
# hs: shadow.example.com
# as_token: 12u394refgbdhivsia
# If enabled, don't let users set their own display names/avatars
# other than for the very first time (unless they are a server admin).
# Useful when provisioning users based on the contents of a 3rd party
# directory and to avoid ambiguities.
#
#disable_set_displayname: False
#disable_set_avatar_url: False
# Users who register on this homeserver will automatically be joined # Users who register on this homeserver will automatically be joined
# to these rooms # to these rooms
# #
@@ -1063,6 +1197,36 @@ password_config:
# #
#pepper: "EVEN_MORE_SECRET" #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 password resets, notification events or # Enable sending emails for password resets, notification events or
@@ -1199,6 +1363,11 @@ password_config:
#user_directory: #user_directory:
# enabled: true # enabled: true
# search_all_users: false # search_all_users: false
#
# # If this is set, user search will be delegated to this ID server instead
# # of synapse performing the search itself.
# # This is an experimental API.
# defer_to_id_server: https://id.example.com
# User Consent configuration # User Consent configuration

View File

@@ -0,0 +1,7 @@
.header {
border-bottom: 4px solid #e4f7ed ! important;
}
.notif_link a, .footer a {
color: #76CFA6 ! important;
}

View File

@@ -0,0 +1,156 @@
body {
margin: 0px;
}
pre, code {
word-break: break-word;
white-space: pre-wrap;
}
#page {
font-family: 'Open Sans', Helvetica, Arial, Sans-Serif;
font-color: #454545;
font-size: 12pt;
width: 100%;
padding: 20px;
}
#inner {
width: 640px;
}
.header {
width: 100%;
height: 87px;
color: #454545;
border-bottom: 4px solid #e5e5e5;
}
.logo {
text-align: right;
margin-left: 20px;
}
.salutation {
padding-top: 10px;
font-weight: bold;
}
.summarytext {
}
.room {
width: 100%;
color: #454545;
border-bottom: 1px solid #e5e5e5;
}
.room_header td {
padding-top: 38px;
padding-bottom: 10px;
border-bottom: 1px solid #e5e5e5;
}
.room_name {
vertical-align: middle;
font-size: 18px;
font-weight: bold;
}
.room_header h2 {
margin-top: 0px;
margin-left: 75px;
font-size: 20px;
}
.room_avatar {
width: 56px;
line-height: 0px;
text-align: center;
vertical-align: middle;
}
.room_avatar img {
width: 48px;
height: 48px;
object-fit: cover;
border-radius: 24px;
}
.notif {
border-bottom: 1px solid #e5e5e5;
margin-top: 16px;
padding-bottom: 16px;
}
.historical_message .sender_avatar {
opacity: 0.3;
}
/* spell out opacity and historical_message class names for Outlook aka Word */
.historical_message .sender_name {
color: #e3e3e3;
}
.historical_message .message_time {
color: #e3e3e3;
}
.historical_message .message_body {
color: #c7c7c7;
}
.historical_message td,
.message td {
padding-top: 10px;
}
.sender_avatar {
width: 56px;
text-align: center;
vertical-align: top;
}
.sender_avatar img {
margin-top: -2px;
width: 32px;
height: 32px;
border-radius: 16px;
}
.sender_name {
display: inline;
font-size: 13px;
color: #a2a2a2;
}
.message_time {
text-align: right;
width: 100px;
font-size: 11px;
color: #a2a2a2;
}
.message_body {
}
.notif_link td {
padding-top: 10px;
padding-bottom: 10px;
font-weight: bold;
}
.notif_link a, .footer a {
color: #454545;
text-decoration: none;
}
.debug {
font-size: 10px;
color: #888;
}
.footer {
margin-top: 20px;
text-align: center;
}

View File

@@ -0,0 +1,45 @@
{% for message in notif.messages %}
<tr class="{{ "historical_message" if message.is_historical else "message" }}">
<td class="sender_avatar">
{% if loop.index0 == 0 or notif.messages[loop.index0 - 1].sender_name != notif.messages[loop.index0].sender_name %}
{% if message.sender_avatar_url %}
<img alt="" class="sender_avatar" src="{{ message.sender_avatar_url|mxc_to_http(32,32) }}" />
{% else %}
{% if message.sender_hash % 3 == 0 %}
<img class="sender_avatar" src="https://vector.im/beta/img/76cfa6.png" />
{% elif message.sender_hash % 3 == 1 %}
<img class="sender_avatar" src="https://vector.im/beta/img/50e2c2.png" />
{% else %}
<img class="sender_avatar" src="https://vector.im/beta/img/f4c371.png" />
{% endif %}
{% endif %}
{% endif %}
</td>
<td class="message_contents">
{% if loop.index0 == 0 or notif.messages[loop.index0 - 1].sender_name != notif.messages[loop.index0].sender_name %}
<div class="sender_name">{% if message.msgtype == "m.emote" %}*{% endif %} {{ message.sender_name }}</div>
{% endif %}
<div class="message_body">
{% if message.msgtype == "m.text" %}
{{ message.body_text_html }}
{% elif message.msgtype == "m.emote" %}
{{ message.body_text_html }}
{% elif message.msgtype == "m.notice" %}
{{ message.body_text_html }}
{% elif message.msgtype == "m.image" %}
<img src="{{ message.image_url|mxc_to_http(640, 480, scale) }}" />
{% elif message.msgtype == "m.file" %}
<span class="filename">{{ message.body_text_plain }}</span>
{% endif %}
</div>
</td>
<td class="message_time">{{ message.ts|format_ts("%H:%M") }}</td>
</tr>
{% endfor %}
<tr class="notif_link">
<td></td>
<td>
<a href="{{ notif.link }}">Voir {{ room.title }}</a>
</td>
<td></td>
</tr>

View File

@@ -0,0 +1,16 @@
{% for message in notif.messages %}
{% if message.msgtype == "m.emote" %}* {% endif %}{{ message.sender_name }} ({{ message.ts|format_ts("%H:%M") }})
{% if message.msgtype == "m.text" %}
{{ message.body_text_plain }}
{% elif message.msgtype == "m.emote" %}
{{ message.body_text_plain }}
{% elif message.msgtype == "m.notice" %}
{{ message.body_text_plain }}
{% elif message.msgtype == "m.image" %}
{{ message.body_text_plain }}
{% elif message.msgtype == "m.file" %}
{{ message.body_text_plain }}
{% endif %}
{% endfor %}
Voir {{ room.title }} à {{ notif.link }}

View File

@@ -0,0 +1,55 @@
<!doctype html>
<html lang="en">
<head>
<style type="text/css">
{% include 'mail.css' without context %}
{% include "mail-%s.css" % app_name ignore missing without context %}
</style>
</head>
<body>
<table id="page">
<tr>
<td> </td>
<td id="inner">
<table class="header">
<tr>
<td>
<div class="salutation">Bonjour {{ user_display_name }},</div>
<div class="summarytext">{{ summary_text }}</div>
</td>
<td class="logo">
{% if app_name == "Riot" %}
<img src="http://matrix.org/img/riot-logo-email.png" width="83" height="83" alt="[Riot]"/>
{% elif app_name == "Vector" %}
<img src="http://matrix.org/img/vector-logo-email.png" width="64" height="83" alt="[Vector]"/>
{% else %}
<img src="http://matrix.org/img/matrix-120x51.png" width="120" height="51" alt="[matrix]"/>
{% endif %}
</td>
</tr>
</table>
{% for room in rooms %}
{% include 'room.html' with context %}
{% endfor %}
<div class="footer">
<a href="{{ unsubscribe_link }}">Se désinscrire</a>
<br/>
<br/>
<div class="debug">
Sending email at {{ reason.now|format_ts("%c") }} due to activity in room {{ reason.room_name }} because
an event was received at {{ reason.received_at|format_ts("%c") }}
which is more than {{ "%.1f"|format(reason.delay_before_mail_ms / (60*1000)) }} ({{ reason.delay_before_mail_ms }}) mins ago,
{% if reason.last_sent_ts %}
and the last time we sent a mail for this room was {{ reason.last_sent_ts|format_ts("%c") }},
which is more than {{ "%.1f"|format(reason.throttle_ms / (60*1000)) }} (current throttle_ms) mins ago.
{% else %}
and we don't have a last time we sent a mail for this room.
{% endif %}
</div>
</div>
</td>
<td> </td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,10 @@
Bonjour {{ user_display_name }},
{{ summary_text }}
{% for room in rooms %}
{% include 'room.txt' with context %}
{% endfor %}
Vous pouvez désactiver ces notifications en cliquant ici {{ unsubscribe_link }}

View File

@@ -0,0 +1,33 @@
<table class="room">
<tr class="room_header">
<td class="room_avatar">
{% if room.avatar_url %}
<img alt="" src="{{ room.avatar_url|mxc_to_http(48,48) }}" />
{% else %}
{% if room.hash % 3 == 0 %}
<img alt="" src="https://vector.im/beta/img/76cfa6.png" />
{% elif room.hash % 3 == 1 %}
<img alt="" src="https://vector.im/beta/img/50e2c2.png" />
{% else %}
<img alt="" src="https://vector.im/beta/img/f4c371.png" />
{% endif %}
{% endif %}
</td>
<td class="room_name" colspan="2">
{{ room.title }}
</td>
</tr>
{% if room.invite %}
<tr>
<td></td>
<td>
<a href="{{ room.link }}">Rejoindre la conversation.</a>
</td>
<td></td>
</tr>
{% else %}
{% for notif in room.notifs %}
{% include 'notif.html' with context %}
{% endfor %}
{% endif %}
</table>

View File

@@ -0,0 +1,9 @@
{{ room.title }}
{% if room.invite %}
  Vous avez été invité, rejoignez la conversation en cliquant sur le lien suivant {{ room.link }}
{% else %}
{% for notif in room.notifs %}
{% include 'notif.txt' with context %}
{% endfor %}
{% endif %}

View File

@@ -27,4 +27,4 @@ try:
except ImportError: except ImportError:
pass pass
__version__ = "1.0.0rc3" __version__ = "1.0.0"

View File

@@ -184,11 +184,22 @@ class Auth(object):
return event_auth.get_public_keys(invite_event) return event_auth.get_public_keys(invite_event)
@defer.inlineCallbacks @defer.inlineCallbacks
def get_user_by_req(self, request, allow_guest=False, rights="access"): def get_user_by_req(
self,
request,
allow_guest=False,
rights="access",
allow_expired=False,
):
""" Get a registered user's ID. """ Get a registered user's ID.
Args: Args:
request - An HTTP request with an access_token query parameter. request - An HTTP request with an access_token query parameter.
allow_expired - Whether to allow the request through even if the account is
expired. If true, Synapse will still require an access token to be
provided but won't check if the account it belongs to has expired. This
works thanks to /login delivering access tokens regardless of accounts'
expiration.
Returns: Returns:
defer.Deferred: resolves to a ``synapse.types.Requester`` object defer.Deferred: resolves to a ``synapse.types.Requester`` object
Raises: Raises:
@@ -207,6 +218,7 @@ class Auth(object):
) )
user_id, app_service = yield self._get_appservice_user_id(request) user_id, app_service = yield self._get_appservice_user_id(request)
if user_id: if user_id:
request.authenticated_entity = user_id request.authenticated_entity = user_id
@@ -229,7 +241,7 @@ class Auth(object):
is_guest = user_info["is_guest"] is_guest = user_info["is_guest"]
# Deny the request if the user account has expired. # Deny the request if the user account has expired.
if self._account_validity.enabled: if self._account_validity.enabled and not allow_expired:
user_id = user.to_string() user_id = user.to_string()
expiration_ts = yield self.store.get_expiration_ts_for_user(user_id) expiration_ts = yield self.store.get_expiration_ts_for_user(user_id)
if expiration_ts is not None and self.clock.time_msec() >= expiration_ts: if expiration_ts is not None and self.clock.time_msec() >= expiration_ts:
@@ -268,39 +280,40 @@ class Auth(object):
errcode=Codes.MISSING_TOKEN errcode=Codes.MISSING_TOKEN
) )
@defer.inlineCallbacks
def _get_appservice_user_id(self, request): def _get_appservice_user_id(self, request):
app_service = self.store.get_app_service_by_token( app_service = self.store.get_app_service_by_token(
self.get_access_token_from_request( self.get_access_token_from_request(
request, self.TOKEN_NOT_FOUND_HTTP_STATUS request, self.TOKEN_NOT_FOUND_HTTP_STATUS
) )
) )
if app_service is None: if app_service is None:
defer.returnValue((None, None)) return(None, None)
if app_service.ip_range_whitelist: if app_service.ip_range_whitelist:
ip_address = IPAddress(self.hs.get_ip_from_request(request)) ip_address = IPAddress(self.hs.get_ip_from_request(request))
if ip_address not in app_service.ip_range_whitelist: if ip_address not in app_service.ip_range_whitelist:
defer.returnValue((None, None)) return(None, None)
if b"user_id" not in request.args: if b"user_id" not in request.args:
defer.returnValue((app_service.sender, app_service)) return(app_service.sender, app_service)
user_id = request.args[b"user_id"][0].decode('utf8') user_id = request.args[b"user_id"][0].decode('utf8')
if app_service.sender == user_id: if app_service.sender == user_id:
defer.returnValue((app_service.sender, app_service)) return(app_service.sender, app_service)
if not app_service.is_interested_in_user(user_id): if not app_service.is_interested_in_user(user_id):
raise AuthError( raise AuthError(
403, 403,
"Application service cannot masquerade as this user." "Application service cannot masquerade as this user."
) )
if not (yield self.store.get_user_by_id(user_id)): # Let ASes manipulate nonexistent users (e.g. to shadow-register them)
raise AuthError( # if not (yield self.store.get_user_by_id(user_id)):
403, # raise AuthError(
"Application service has not registered this user" # 403,
) # "Application service has not registered this user"
defer.returnValue((user_id, app_service)) # )
return(user_id, app_service)
@defer.inlineCallbacks @defer.inlineCallbacks
def get_user_by_access_token(self, token, rights="access"): def get_user_by_access_token(self, token, rights="access"):
@@ -533,24 +546,15 @@ class Auth(object):
defer.returnValue(user_info) defer.returnValue(user_info)
def get_appservice_by_req(self, request): def get_appservice_by_req(self, request):
try: (user_id, app_service) = self._get_appservice_user_id(request)
token = self.get_access_token_from_request( if not app_service:
request, self.TOKEN_NOT_FOUND_HTTP_STATUS
)
service = self.store.get_app_service_by_token(token)
if not service:
logger.warn("Unrecognised appservice access token.")
raise AuthError(
self.TOKEN_NOT_FOUND_HTTP_STATUS,
"Unrecognised access token.",
errcode=Codes.UNKNOWN_TOKEN
)
request.authenticated_entity = service.sender
return defer.succeed(service)
except KeyError:
raise AuthError( raise AuthError(
self.TOKEN_NOT_FOUND_HTTP_STATUS, "Missing access token." self.TOKEN_NOT_FOUND_HTTP_STATUS,
"Unrecognised access token.",
errcode=Codes.UNKNOWN_TOKEN,
) )
request.authenticated_entity = app_service.sender
return app_service
def is_server_admin(self, user): def is_server_admin(self, user):
""" Check if the given user is a local server admin. """ Check if the given user is a local server admin.

View File

@@ -83,6 +83,7 @@ class EventTypes(object):
RoomAvatar = "m.room.avatar" RoomAvatar = "m.room.avatar"
RoomEncryption = "m.room.encryption" RoomEncryption = "m.room.encryption"
GuestAccess = "m.room.guest_access" GuestAccess = "m.room.guest_access"
Encryption = "m.room.encryption"
# These are used for validation # These are used for validation
Message = "m.room.message" Message = "m.room.message"
@@ -92,6 +93,8 @@ class EventTypes(object):
ServerACL = "m.room.server_acl" ServerACL = "m.room.server_acl"
Pinned = "m.room.pinned_events" Pinned = "m.room.pinned_events"
Retention = "m.room.retention"
class RejectedReason(object): class RejectedReason(object):
AUTH_ERROR = "auth_error" AUTH_ERROR = "auth_error"

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2014-2016 OpenMarket Ltd # 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"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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" INCOMPATIBLE_ROOM_VERSION = "M_INCOMPATIBLE_ROOM_VERSION"
WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION" WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
EXPIRED_ACCOUNT = "ORG_MATRIX_EXPIRED_ACCOUNT" 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): class CodeMessageException(RuntimeError):
@@ -372,6 +380,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): class RequestSendFailed(RuntimeError):
"""Sending a HTTP request over federation failed due to not being able to """Sending a HTTP request over federation failed due to not being able to
talk to the remote server for some reason. talk to the remote server for some reason.

View File

@@ -176,7 +176,6 @@ class SynapseHomeServer(HomeServer):
resources.update({ resources.update({
"/_matrix/client/api/v1": client_resource, "/_matrix/client/api/v1": client_resource,
"/_synapse/password_reset": client_resource,
"/_matrix/client/r0": client_resource, "/_matrix/client/r0": client_resource,
"/_matrix/client/unstable": client_resource, "/_matrix/client/unstable": client_resource,
"/_matrix/client/v2_alpha": client_resource, "/_matrix/client/v2_alpha": client_resource,

View File

@@ -265,7 +265,7 @@ class ApplicationService(object):
def is_exclusive_room(self, room_id): def is_exclusive_room(self, room_id):
return self._is_exclusive(ApplicationService.NS_ROOMS, room_id) return self._is_exclusive(ApplicationService.NS_ROOMS, room_id)
def get_exlusive_user_regexes(self): def get_exclusive_user_regexes(self):
"""Get the list of regexes used to determine if a user is exclusively """Get the list of regexes used to determine if a user is exclusively
registered by the AS registered by the AS
""" """

View File

@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*- # -*- 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"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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_enabled = password_config.get("enabled", True)
self.password_pepper = password_config.get("pepper", "") 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): def default_config(self, config_dir_path, server_name, **kwargs):
return """\ return """\
password_config: password_config:
@@ -39,4 +45,34 @@ class PasswordConfig(Config):
# DO NOT CHANGE THIS AFTER INITIAL SETUP! # DO NOT CHANGE THIS AFTER INITIAL SETUP!
# #
#pepper: "EVEN_MORE_SECRET" #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
""" """

View File

@@ -13,8 +13,11 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import os
from distutils.util import strtobool from distutils.util import strtobool
import pkg_resources
from synapse.config._base import Config, ConfigError from synapse.config._base import Config, ConfigError
from synapse.types import RoomAlias from synapse.types import RoomAlias
from synapse.util.stringutils import random_string_with_symbols from synapse.util.stringutils import random_string_with_symbols
@@ -41,8 +44,36 @@ class AccountValidityConfig(Config):
self.startup_job_max_delta = self.period * 10. / 100. self.startup_job_max_delta = self.period * 10. / 100.
if self.renew_by_email_enabled and "public_baseurl" not in synapse_config: if self.renew_by_email_enabled:
raise ConfigError("Can't send renewal emails without 'public_baseurl'") if "public_baseurl" not in synapse_config:
raise ConfigError("Can't send renewal emails without 'public_baseurl'")
template_dir = config.get("template_dir")
if not template_dir:
template_dir = pkg_resources.resource_filename("synapse", "res/templates")
if "account_renewed_html_path" in config:
file_path = os.path.join(template_dir, config["account_renewed_html_path"])
self.account_renewed_html_content = self.read_file(
file_path, "account_validity.account_renewed_html_path"
)
else:
self.account_renewed_html_content = (
"<html><body>Your account has been successfully renewed.</body><html>"
)
if "invalid_token_html_path" in config:
file_path = os.path.join(template_dir, config["invalid_token_html_path"])
self.invalid_token_html_content = self.read_file(
file_path, "account_validity.invalid_token_html_path"
)
else:
self.invalid_token_html_content = (
"<html><body>Invalid renewal token.</body><html>"
)
class RegistrationConfig(Config): class RegistrationConfig(Config):
@@ -62,8 +93,19 @@ class RegistrationConfig(Config):
self.registrations_require_3pid = config.get("registrations_require_3pid", []) self.registrations_require_3pid = config.get("registrations_require_3pid", [])
self.allowed_local_3pids = config.get("allowed_local_3pids", []) self.allowed_local_3pids = config.get("allowed_local_3pids", [])
self.check_is_for_allowed_local_3pids = config.get(
"check_is_for_allowed_local_3pids", None
)
self.allow_invited_3pids = config.get("allow_invited_3pids", False)
self.disable_3pid_changes = config.get("disable_3pid_changes", False)
self.enable_3pid_lookup = config.get("enable_3pid_lookup", True) self.enable_3pid_lookup = config.get("enable_3pid_lookup", True)
self.registration_shared_secret = config.get("registration_shared_secret") self.registration_shared_secret = config.get("registration_shared_secret")
self.register_mxid_from_3pid = config.get("register_mxid_from_3pid")
self.register_just_use_email_for_display_name = config.get(
"register_just_use_email_for_display_name", False,
)
self.bcrypt_rounds = config.get("bcrypt_rounds", 12) self.bcrypt_rounds = config.get("bcrypt_rounds", 12)
self.trusted_third_party_id_servers = config.get( self.trusted_third_party_id_servers = config.get(
@@ -83,6 +125,16 @@ class RegistrationConfig(Config):
raise ConfigError('Invalid auto_join_rooms entry %s' % (room_alias,)) raise ConfigError('Invalid auto_join_rooms entry %s' % (room_alias,))
self.autocreate_auto_join_rooms = config.get("autocreate_auto_join_rooms", True) self.autocreate_auto_join_rooms = config.get("autocreate_auto_join_rooms", True)
self.disable_set_displayname = config.get("disable_set_displayname", False)
self.disable_set_avatar_url = config.get("disable_set_avatar_url", False)
self.replicate_user_profiles_to = config.get("replicate_user_profiles_to", [])
if not isinstance(self.replicate_user_profiles_to, list):
self.replicate_user_profiles_to = [self.replicate_user_profiles_to, ]
self.shadow_server = config.get("shadow_server", None)
self.rewrite_identity_server_urls = config.get("rewrite_identity_server_urls", {})
self.disable_msisdn_registration = ( self.disable_msisdn_registration = (
config.get("disable_msisdn_registration", False) config.get("disable_msisdn_registration", False)
) )
@@ -140,6 +192,16 @@ class RegistrationConfig(Config):
# period: 6w # period: 6w
# renew_at: 1w # renew_at: 1w
# renew_email_subject: "Renew your %%(app)s account" # renew_email_subject: "Renew your %%(app)s account"
# # Directory in which Synapse will try to find the HTML files to serve to the
# # user when trying to renew an account. Optional, defaults to
# # synapse/res/templates.
# template_dir: "res/templates"
# # HTML to be displayed to the user after they successfully renewed their
# # account. Optional.
# account_renewed_html_path: "account_renewed.html"
# # HTML to be displayed when the user tries to renew an account with an invalid
# # renewal token. Optional.
# invalid_token_html_path: "invalid_token.html"
# The user must provide all of the below types of 3PID when registering. # The user must provide all of the below types of 3PID when registering.
# #
@@ -152,9 +214,32 @@ class RegistrationConfig(Config):
# #
#disable_msisdn_registration: true #disable_msisdn_registration: true
# Derive the user's matrix ID from a type of 3PID used when registering.
# This overrides any matrix ID the user proposes when calling /register
# The 3PID type should be present in registrations_require_3pid to avoid
# users failing to register if they don't specify the right kind of 3pid.
#
#register_mxid_from_3pid: email
# Uncomment to set the display name of new users to their email address,
# rather than using the default heuristic.
#
#register_just_use_email_for_display_name: true
# Mandate that users are only allowed to associate certain formats of # Mandate that users are only allowed to associate certain formats of
# 3PIDs with accounts on this server. # 3PIDs with accounts on this server.
# #
# Use an Identity Server to establish which 3PIDs are allowed to register?
# Overrides allowed_local_3pids below.
#
#check_is_for_allowed_local_3pids: matrix.org
#
# If you are using an IS you can also check whether that IS registers
# pending invites for the given 3PID (and then allow it to sign up on
# the platform):
#
#allow_invited_3pids: False
#
#allowed_local_3pids: #allowed_local_3pids:
# - medium: email # - medium: email
# pattern: '.*@matrix\\.org' # pattern: '.*@matrix\\.org'
@@ -163,6 +248,11 @@ class RegistrationConfig(Config):
# - medium: msisdn # - medium: msisdn
# pattern: '\\+44' # pattern: '\\+44'
# If true, stop users from trying to change the 3PIDs associated with
# their accounts.
#
#disable_3pid_changes: False
# Enable 3PIDs lookup requests to identity servers from this server. # Enable 3PIDs lookup requests to identity servers from this server.
# #
#enable_3pid_lookup: true #enable_3pid_lookup: true
@@ -204,6 +294,30 @@ class RegistrationConfig(Config):
# - matrix.org # - matrix.org
# - vector.im # - vector.im
# If enabled, user IDs, display names and avatar URLs will be replicated
# to this server whenever they change.
# This is an experimental API currently implemented by sydent to support
# cross-homeserver user directories.
#
#replicate_user_profiles_to: example.com
# If specified, attempt to replay registrations, profile changes & 3pid
# bindings on the given target homeserver via the AS API. The HS is authed
# via a given AS token.
#
#shadow_server:
# hs_url: https://shadow.example.com
# hs: shadow.example.com
# as_token: 12u394refgbdhivsia
# If enabled, don't let users set their own display names/avatars
# other than for the very first time (unless they are a server admin).
# Useful when provisioning users based on the contents of a 3rd party
# directory and to avoid ambiguities.
#
#disable_set_displayname: False
#disable_set_avatar_url: False
# Users who register on this homeserver will automatically be joined # Users who register on this homeserver will automatically be joined
# to these rooms # to these rooms
# #

View File

@@ -84,12 +84,32 @@ class ServerConfig(Config):
"require_auth_for_profile_requests", False, "require_auth_for_profile_requests", False,
) )
# If set to 'True', requires authentication to access the server's if "restrict_public_rooms_to_local_users" in config and (
# public rooms directory through the client API, and forbids any other "allow_public_rooms_without_auth" in config
# homeserver to fetch it via federation. or "allow_public_rooms_over_federation" in config
self.restrict_public_rooms_to_local_users = config.get( ):
"restrict_public_rooms_to_local_users", False, raise ConfigError(
) "Can't use 'restrict_public_rooms_to_local_users' if"
" 'allow_public_rooms_without_auth' and/or"
" 'allow_public_rooms_over_federation' is set."
)
# Check if the legacy "restrict_public_rooms_to_local_users" flag is set. This
# flag is now obsolete but we need to check it for backward-compatibility.
if config.get("restrict_public_rooms_to_local_users", False):
self.allow_public_rooms_without_auth = False
self.allow_public_rooms_over_federation = False
else:
# If set to 'False', requires authentication to access the server's public
# rooms directory through the client API. Defaults to 'True'.
self.allow_public_rooms_without_auth = config.get(
"allow_public_rooms_without_auth", True
)
# If set to 'False', forbids any other homeserver to fetch the server's public
# rooms directory via federation. Defaults to 'True'.
self.allow_public_rooms_over_federation = config.get(
"allow_public_rooms_over_federation", True
)
default_room_version = config.get( default_room_version = config.get(
"default_room_version", DEFAULT_ROOM_VERSION, "default_room_version", DEFAULT_ROOM_VERSION,
@@ -202,6 +222,121 @@ class ServerConfig(Config):
# events with profile information that differ from the target's global profile. # events with profile information that differ from the target's global profile.
self.allow_per_room_profiles = config.get("allow_per_room_profiles", True) self.allow_per_room_profiles = config.get("allow_per_room_profiles", True)
# Whether to show the users on this homeserver in the user directory. Defaults to
# True.
self.show_users_in_user_directory = config.get(
"show_users_in_user_directory", True,
)
retention_config = config.get("retention")
if retention_config is None:
retention_config = {}
self.retention_enabled = retention_config.get("enabled", False)
retention_default_policy = retention_config.get("default_policy")
if retention_default_policy is not None:
self.retention_default_min_lifetime = retention_default_policy.get(
"min_lifetime"
)
if self.retention_default_min_lifetime is not None:
self.retention_default_min_lifetime = self.parse_duration(
self.retention_default_min_lifetime
)
self.retention_default_max_lifetime = retention_default_policy.get(
"max_lifetime"
)
if self.retention_default_max_lifetime is not None:
self.retention_default_max_lifetime = self.parse_duration(
self.retention_default_max_lifetime
)
if (
self.retention_default_min_lifetime is not None
and self.retention_default_max_lifetime is not None
and (
self.retention_default_min_lifetime
> self.retention_default_max_lifetime
)
):
raise ConfigError(
"The default retention policy's 'min_lifetime' can not be greater"
" than its 'max_lifetime'"
)
else:
self.retention_default_min_lifetime = None
self.retention_default_max_lifetime = None
self.retention_allowed_lifetime_min = retention_config.get("allowed_lifetime_min")
if self.retention_allowed_lifetime_min is not None:
self.retention_allowed_lifetime_min = self.parse_duration(
self.retention_allowed_lifetime_min
)
self.retention_allowed_lifetime_max = retention_config.get("allowed_lifetime_max")
if self.retention_allowed_lifetime_max is not None:
self.retention_allowed_lifetime_max = self.parse_duration(
self.retention_allowed_lifetime_max
)
if (
self.retention_allowed_lifetime_min is not None
and self.retention_allowed_lifetime_max is not None
and self.retention_allowed_lifetime_min > self.retention_allowed_lifetime_max
):
raise ConfigError(
"Invalid retention policy limits: 'allowed_lifetime_min' can not be"
" greater than 'allowed_lifetime_max'"
)
self.retention_purge_jobs = []
for purge_job_config in retention_config.get("purge_jobs", []):
interval_config = purge_job_config.get("interval")
if interval_config is None:
raise ConfigError(
"A retention policy's purge jobs configuration must have the"
" 'interval' key set."
)
interval = self.parse_duration(interval_config)
shortest_max_lifetime = purge_job_config.get("shortest_max_lifetime")
if shortest_max_lifetime is not None:
shortest_max_lifetime = self.parse_duration(shortest_max_lifetime)
longest_max_lifetime = purge_job_config.get("longest_max_lifetime")
if longest_max_lifetime is not None:
longest_max_lifetime = self.parse_duration(longest_max_lifetime)
if (
shortest_max_lifetime is not None
and longest_max_lifetime is not None
and shortest_max_lifetime > longest_max_lifetime
):
raise ConfigError(
"A retention policy's purge jobs configuration's"
" 'shortest_max_lifetime' value can not be greater than its"
" 'longest_max_lifetime' value."
)
self.retention_purge_jobs.append({
"interval": interval,
"shortest_max_lifetime": shortest_max_lifetime,
"longest_max_lifetime": longest_max_lifetime,
})
if not self.retention_purge_jobs:
self.retention_purge_jobs = [{
"interval": self.parse_duration("1d"),
"shortest_max_lifetime": None,
"longest_max_lifetime": None,
}]
self.listeners = [] self.listeners = []
for listener in config.get("listeners", []): for listener in config.get("listeners", []):
if not isinstance(listener.get("port", None), int): if not isinstance(listener.get("port", None), int):
@@ -401,11 +536,15 @@ class ServerConfig(Config):
# #
#require_auth_for_profile_requests: true #require_auth_for_profile_requests: true
# If set to 'true', requires authentication to access the server's # If set to 'false', requires authentication to access the server's public rooms
# public rooms directory through the client API, and forbids any other # directory through the client API. Defaults to 'true'.
# homeserver to fetch it via federation. Defaults to 'false'.
# #
#restrict_public_rooms_to_local_users: true #allow_public_rooms_without_auth: false
# If set to 'false', forbids any other homeserver to fetch the server's public
# rooms directory via federation. Defaults to 'true'.
#
#allow_public_rooms_over_federation: false
# The default room version for newly created rooms. # The default room version for newly created rooms.
# #
@@ -631,6 +770,74 @@ class ServerConfig(Config):
# Defaults to 'true'. # Defaults to 'true'.
# #
#allow_per_room_profiles: false #allow_per_room_profiles: false
# Whether to show the users on this homeserver in the user directory. Defaults to
# 'true'.
#
#show_users_in_user_directory: false
# Message retention policy at the server level.
#
# Room admins and mods can define a retention period for their rooms using the
# 'm.room.retention' state event, and server admins can cap this period by setting
# the 'allowed_lifetime_min' and 'allowed_lifetime_max' config options.
#
# If this feature is enabled, Synapse will regularly look for and purge events
# which are older than the room's maximum retention period. Synapse will also
# filter events received over federation so that events that should have been
# purged are ignored and not stored again.
#
retention:
# The message retention policies feature is disabled by default. Uncomment the
# following line to enable it.
#
#enabled: true
# Default retention policy. If set, Synapse will apply it to rooms that lack the
# 'm.room.retention' state event. Currently, the value of 'min_lifetime' doesn't
# matter much because Synapse doesn't take it into account yet.
#
#default_policy:
# min_lifetime: 1d
# max_lifetime: 1y
# Retention policy limits. If set, a user won't be able to send a
# 'm.room.retention' event which features a 'min_lifetime' or a 'max_lifetime'
# that's not within this range. This is especially useful in closed federations,
# in which server admins can make sure every federating server applies the same
# rules.
#
#allowed_lifetime_min: 1d
#allowed_lifetime_max: 1y
# Server admins can define the settings of the background jobs purging the
# events which lifetime has expired under the 'purge_jobs' section.
#
# If no configuration is provided, a single job will be set up to delete expired
# events in every room daily.
#
# Each job's configuration defines which range of message lifetimes the job
# takes care of. For example, if 'shortest_max_lifetime' is '2d' and
# 'longest_max_lifetime' is '3d', the job will handle purging expired events in
# rooms whose state defines a 'max_lifetime' that's both higher than 2 days, and
# lower than or equal to 3 days. Both the minimum and the maximum value of a
# range are optional, e.g. a job with no 'shortest_max_lifetime' and a
# 'longest_max_lifetime' of '3d' will handle every room with a retention policy
# which 'max_lifetime' is lower than or equal to three days.
#
# The rationale for this per-job configuration is that some rooms might have a
# retention policy with a low 'max_lifetime', where history needs to be purged
# of outdated messages on a very frequent basis (e.g. every 5min), but not want
# that purge to be performed by a job that's iterating over every room it knows,
# which would be quite heavy on the server.
#
#purge_jobs:
# - shortest_max_lifetime: 1d
# longest_max_lifetime: 3d
# interval: 5m:
# - shortest_max_lifetime: 3d
# longest_max_lifetime: 1y
# interval: 24h
""" % locals() """ % locals()
def read_arguments(self, args): def read_arguments(self, args):

View File

@@ -24,6 +24,7 @@ class UserDirectoryConfig(Config):
def read_config(self, config): def read_config(self, config):
self.user_directory_search_enabled = True self.user_directory_search_enabled = True
self.user_directory_search_all_users = False self.user_directory_search_all_users = False
self.user_directory_defer_to_id_server = None
user_directory_config = config.get("user_directory", None) user_directory_config = config.get("user_directory", None)
if user_directory_config: if user_directory_config:
self.user_directory_search_enabled = ( self.user_directory_search_enabled = (
@@ -32,6 +33,9 @@ class UserDirectoryConfig(Config):
self.user_directory_search_all_users = ( self.user_directory_search_all_users = (
user_directory_config.get("search_all_users", False) user_directory_config.get("search_all_users", False)
) )
self.user_directory_defer_to_id_server = (
user_directory_config.get("defer_to_id_server", None)
)
def default_config(self, config_dir_path, server_name, **kwargs): def default_config(self, config_dir_path, server_name, **kwargs):
return """ return """
@@ -50,4 +54,9 @@ class UserDirectoryConfig(Config):
#user_directory: #user_directory:
# enabled: true # enabled: true
# search_all_users: false # search_all_users: false
#
# # If this is set, user search will be delegated to this ID server instead
# # of synapse performing the search itself.
# # This is an experimental API.
# defer_to_id_server: https://id.example.com
""" """

View File

@@ -46,13 +46,26 @@ class SpamChecker(object):
return self.spam_checker.check_event_for_spam(event) return self.spam_checker.check_event_for_spam(event)
def user_may_invite(self, inviter_userid, invitee_userid, room_id): def user_may_invite(self, inviter_userid, invitee_userid, third_party_invite,
room_id, new_room, published_room):
"""Checks if a given user may send an invite """Checks if a given user may send an invite
If this method returns false, the invite will be rejected. If this method returns false, the invite will be rejected.
Args: Args:
userid (string): The sender's user ID inviter_userid (str)
invitee_userid (str|None): The user ID of the invitee. Is None
if this is a third party invite and the 3PID is not bound to a
user ID.
third_party_invite (dict|None): If a third party invite then is a
dict containing the medium and address of the invitee.
room_id (str)
new_room (bool): Whether the user is being invited to the room as
part of a room creation, if so the invitee would have been
included in the call to `user_may_create_room`.
published_room (bool): Whether the room the user is being invited
to has been published in the local homeserver's public room
directory.
Returns: Returns:
bool: True if the user may send an invite, otherwise False bool: True if the user may send an invite, otherwise False
@@ -60,15 +73,25 @@ class SpamChecker(object):
if self.spam_checker is None: if self.spam_checker is None:
return True return True
return self.spam_checker.user_may_invite(inviter_userid, invitee_userid, room_id) return self.spam_checker.user_may_invite(
inviter_userid, invitee_userid, third_party_invite, room_id, new_room,
published_room,
)
def user_may_create_room(self, userid): def user_may_create_room(self, userid, invite_list, third_party_invite_list,
cloning):
"""Checks if a given user may create a room """Checks if a given user may create a room
If this method returns false, the creation request will be rejected. If this method returns false, the creation request will be rejected.
Args: Args:
userid (string): The sender's user ID userid (string): The sender's user ID
invite_list (list[str]): List of user IDs that would be invited to
the new room.
third_party_invite_list (list[dict]): List of third party invites
for the new room.
cloning (bool): Whether the user is cloning an existing room, e.g.
upgrading a room.
Returns: Returns:
bool: True if the user may create a room, otherwise False bool: True if the user may create a room, otherwise False
@@ -76,7 +99,9 @@ class SpamChecker(object):
if self.spam_checker is None: if self.spam_checker is None:
return True return True
return self.spam_checker.user_may_create_room(userid) return self.spam_checker.user_may_create_room(
userid, invite_list, third_party_invite_list, cloning,
)
def user_may_create_room_alias(self, userid, room_alias): def user_may_create_room_alias(self, userid, room_alias):
"""Checks if a given user may create a room alias """Checks if a given user may create a room alias
@@ -111,3 +136,21 @@ class SpamChecker(object):
return True return True
return self.spam_checker.user_may_publish_room(userid, room_id) return self.spam_checker.user_may_publish_room(userid, room_id)
def user_may_join_room(self, userid, room_id, is_invited):
"""Checks if a given users is allowed to join a room.
Is not called when the user creates a room.
Args:
userid (str)
room_id (str)
is_invited (bool): Whether the user is invited into the room
Returns:
bool: Whether the user may join the room
"""
if self.spam_checker is None:
return True
return self.spam_checker.user_may_join_room(userid, room_id, is_invited)

View File

@@ -17,8 +17,8 @@ from twisted.internet import defer
class ThirdPartyEventRules(object): class ThirdPartyEventRules(object):
"""Allows server admins to provide a Python module implementing an extra set of rules """Allows server admins to provide a Python module implementing an extra
to apply when processing events. set of rules to apply when processing events.
This is designed to help admins of closed federations with enforcing custom This is designed to help admins of closed federations with enforcing custom
behaviours. behaviours.
@@ -35,7 +35,10 @@ class ThirdPartyEventRules(object):
module, config = hs.config.third_party_event_rules module, config = hs.config.third_party_event_rules
if module is not None: if module is not None:
self.third_party_rules = module(config=config) self.third_party_rules = module(
config=config,
http_client=hs.get_simple_http_client(),
)
@defer.inlineCallbacks @defer.inlineCallbacks
def check_event_allowed(self, event, context): def check_event_allowed(self, event, context):
@@ -46,7 +49,7 @@ class ThirdPartyEventRules(object):
context (synapse.events.snapshot.EventContext): The context of the event. context (synapse.events.snapshot.EventContext): The context of the event.
Returns: Returns:
defer.Deferred(bool), True if the event should be allowed, False if not. defer.Deferred[bool]: True if the event should be allowed, False if not.
""" """
if self.third_party_rules is None: if self.third_party_rules is None:
defer.returnValue(True) defer.returnValue(True)
@@ -60,3 +63,52 @@ class ThirdPartyEventRules(object):
ret = yield self.third_party_rules.check_event_allowed(event, state_events) ret = yield self.third_party_rules.check_event_allowed(event, state_events)
defer.returnValue(ret) defer.returnValue(ret)
@defer.inlineCallbacks
def on_create_room(self, requester, config, is_requester_admin):
"""Intercept requests to create room to allow, deny or update the
request config.
Args:
requester (Requester)
config (dict): The creation config from the client.
is_requester_admin (bool): If the requester is an admin
Returns:
defer.Deferred
"""
if self.third_party_rules is None:
return
yield self.third_party_rules.on_create_room(
requester, config, is_requester_admin
)
@defer.inlineCallbacks
def check_threepid_can_be_invited(self, medium, address, room_id):
"""Check if a provided 3PID can be invited in the given room.
Args:
medium (str): The 3PID's medium.
address (str): The 3PID's address.
room_id (str): The room we want to invite the threepid to.
Returns:
defer.Deferred[bool], True if the 3PID can be invited, False if not.
"""
if self.third_party_rules is None:
defer.returnValue(True)
state_ids = yield self.store.get_filtered_current_state_ids(room_id)
room_state_events = yield self.store.get_events(state_ids.values())
state_events = {}
for key, event_id in state_ids.items():
state_events[key] = room_state_events[event_id]
ret = yield self.third_party_rules.check_threepid_can_be_invited(
medium, address, state_events,
)
defer.returnValue(ret)

View File

@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from six import string_types from six import integer_types, string_types
from synapse.api.constants import MAX_ALIAS_LENGTH, EventTypes, Membership from synapse.api.constants import MAX_ALIAS_LENGTH, EventTypes, Membership
from synapse.api.errors import Codes, SynapseError from synapse.api.errors import Codes, SynapseError
@@ -22,11 +22,12 @@ from synapse.types import EventID, RoomID, UserID
class EventValidator(object): class EventValidator(object):
def validate_new(self, event): def validate_new(self, event, config):
"""Validates the event has roughly the right format """Validates the event has roughly the right format
Args: Args:
event (FrozenEvent) event (FrozenEvent): The event to validate.
config (Config): The homeserver's configuration.
""" """
self.validate_builder(event) self.validate_builder(event)
@@ -67,6 +68,99 @@ class EventValidator(object):
Codes.INVALID_PARAM, Codes.INVALID_PARAM,
) )
if event.type == EventTypes.Retention:
self._validate_retention(event, config)
def _validate_retention(self, event, config):
"""Checks that an event that defines the retention policy for a room respects the
boundaries imposed by the server's administrator.
Args:
event (FrozenEvent): The event to validate.
config (Config): The homeserver's configuration.
"""
min_lifetime = event.content.get("min_lifetime")
max_lifetime = event.content.get("max_lifetime")
if min_lifetime is not None:
if not isinstance(min_lifetime, integer_types):
raise SynapseError(
code=400,
msg="'min_lifetime' must be an integer",
errcode=Codes.BAD_JSON,
)
if (
config.retention_allowed_lifetime_min is not None
and min_lifetime < config.retention_allowed_lifetime_min
):
raise SynapseError(
code=400,
msg=(
"'min_lifetime' can't be lower than the minimum allowed"
" value enforced by the server's administrator"
),
errcode=Codes.BAD_JSON,
)
if (
config.retention_allowed_lifetime_max is not None
and min_lifetime > config.retention_allowed_lifetime_max
):
raise SynapseError(
code=400,
msg=(
"'min_lifetime' can't be greater than the maximum allowed"
" value enforced by the server's administrator"
),
errcode=Codes.BAD_JSON,
)
if max_lifetime is not None:
if not isinstance(max_lifetime, integer_types):
raise SynapseError(
code=400,
msg="'max_lifetime' must be an integer",
errcode=Codes.BAD_JSON,
)
if (
config.retention_allowed_lifetime_min is not None
and max_lifetime < config.retention_allowed_lifetime_min
):
raise SynapseError(
code=400,
msg=(
"'max_lifetime' can't be lower than the minimum allowed value"
" enforced by the server's administrator"
),
errcode=Codes.BAD_JSON,
)
if (
config.retention_allowed_lifetime_max is not None
and max_lifetime > config.retention_allowed_lifetime_max
):
raise SynapseError(
code=400,
msg=(
"'max_lifetime' can't be greater than the maximum allowed"
" value enforced by the server's administrator"
),
errcode=Codes.BAD_JSON,
)
if (
min_lifetime is not None
and max_lifetime is not None
and min_lifetime > max_lifetime
):
raise SynapseError(
code=400,
msg="'min_lifetime' can't be greater than 'max_lifetime",
errcode=Codes.BAD_JSON,
)
def validate_builder(self, event): def validate_builder(self, event):
"""Validates that the builder/event has roughly the right format. Only """Validates that the builder/event has roughly the right format. Only
checks values that we expect a proto event to have, rather than all the checks values that we expect a proto event to have, rather than all the

View File

@@ -720,15 +720,15 @@ class PublicRoomList(BaseFederationServlet):
PATH = "/publicRooms" PATH = "/publicRooms"
def __init__(self, handler, authenticator, ratelimiter, server_name, deny_access): def __init__(self, handler, authenticator, ratelimiter, server_name, allow_access):
super(PublicRoomList, self).__init__( super(PublicRoomList, self).__init__(
handler, authenticator, ratelimiter, server_name, handler, authenticator, ratelimiter, server_name,
) )
self.deny_access = deny_access self.allow_access = allow_access
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, origin, content, query): def on_GET(self, origin, content, query):
if self.deny_access: if not self.allow_access:
raise FederationDeniedError(origin) raise FederationDeniedError(origin)
limit = parse_integer_from_args(query, "limit", 0) limit = parse_integer_from_args(query, "limit", 0)
@@ -1455,7 +1455,7 @@ def register_servlets(hs, resource, authenticator, ratelimiter, servlet_groups=N
authenticator=authenticator, authenticator=authenticator,
ratelimiter=ratelimiter, ratelimiter=ratelimiter,
server_name=hs.hostname, server_name=hs.hostname,
deny_access=hs.config.restrict_public_rooms_to_local_users, allow_access=hs.config.allow_public_rooms_over_federation,
).register(resource) ).register(resource)
if "group_server" in servlet_groups: if "group_server" in servlet_groups:

View File

@@ -110,6 +110,9 @@ class AccountValidityHandler(object):
# Stop right here if the user doesn't have at least one email address. # Stop right here if the user doesn't have at least one email address.
# In this case, they will have to ask their server admin to renew their # In this case, they will have to ask their server admin to renew their
# account manually. # account manually.
# We don't need to do a specific check to make sure the account isn't
# deactivated, as a deactivated account isn't supposed to have any
# email address attached to it.
if not addresses: if not addresses:
return return
@@ -220,11 +223,19 @@ class AccountValidityHandler(object):
Args: Args:
renewal_token (str): Token sent with the renewal request. renewal_token (str): Token sent with the renewal request.
Returns:
bool: Whether the provided token is valid.
""" """
user_id = yield self.store.get_user_from_renewal_token(renewal_token) try:
user_id = yield self.store.get_user_from_renewal_token(renewal_token)
except StoreError:
defer.returnValue(False)
logger.debug("Renewing an account for user %s", user_id) logger.debug("Renewing an account for user %s", user_id)
yield self.renew_account_for_user(user_id) yield self.renew_account_for_user(user_id)
defer.returnValue(True)
@defer.inlineCallbacks @defer.inlineCallbacks
def renew_account_for_user(self, user_id, expiration_ts=None, email_sent=False): def renew_account_for_user(self, user_id, expiration_ts=None, email_sent=False):
"""Renews the account attached to a given user by pushing back the """Renews the account attached to a given user by pushing back the

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2017, 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"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -33,6 +34,7 @@ class DeactivateAccountHandler(BaseHandler):
self._device_handler = hs.get_device_handler() self._device_handler = hs.get_device_handler()
self._room_member_handler = hs.get_room_member_handler() self._room_member_handler = hs.get_room_member_handler()
self._identity_handler = hs.get_handlers().identity_handler self._identity_handler = hs.get_handlers().identity_handler
self._profile_handler = hs.get_profile_handler()
self.user_directory_handler = hs.get_user_directory_handler() self.user_directory_handler = hs.get_user_directory_handler()
# Flag that indicates whether the process to part users from rooms is running # Flag that indicates whether the process to part users from rooms is running
@@ -42,6 +44,8 @@ class DeactivateAccountHandler(BaseHandler):
# it left off (if it has work left to do). # it left off (if it has work left to do).
hs.get_reactor().callWhenRunning(self._start_user_parting) hs.get_reactor().callWhenRunning(self._start_user_parting)
self._account_validity_enabled = hs.config.account_validity.enabled
@defer.inlineCallbacks @defer.inlineCallbacks
def deactivate_account(self, user_id, erase_data, id_server=None): def deactivate_account(self, user_id, erase_data, id_server=None):
"""Deactivate a user's account """Deactivate a user's account
@@ -98,6 +102,9 @@ class DeactivateAccountHandler(BaseHandler):
yield self.store.user_set_password_hash(user_id, None) yield self.store.user_set_password_hash(user_id, None)
user = UserID.from_string(user_id)
yield self._profile_handler.set_active(user, False, False)
# Add the user to a table of users pending deactivation (ie. # Add the user to a table of users pending deactivation (ie.
# removal from all the rooms they're a member of) # removal from all the rooms they're a member of)
yield self.store.add_user_pending_deactivation(user_id) yield self.store.add_user_pending_deactivation(user_id)
@@ -114,6 +121,13 @@ class DeactivateAccountHandler(BaseHandler):
# parts users from rooms (if it isn't already running) # parts users from rooms (if it isn't already running)
self._start_user_parting() self._start_user_parting()
# Remove all information on the user from the account_validity table.
if self._account_validity_enabled:
yield self.store.delete_account_validity_for_user(user_id)
# Mark the user as deactivated.
yield self.store.set_user_deactivated_status(user_id, True)
defer.returnValue(identity_server_supports_unbinding) defer.returnValue(identity_server_supports_unbinding)
def _start_user_parting(self): def _start_user_parting(self):

View File

@@ -568,6 +568,12 @@ class DeviceListEduUpdater(object):
stream_id = result["stream_id"] stream_id = result["stream_id"]
devices = result["devices"] devices = result["devices"]
for device in devices:
logger.debug(
"Handling resync update %r/%r, ID: %r",
user_id, device["device_id"], stream_id,
)
# If the remote server has more than ~1000 devices for this user # If the remote server has more than ~1000 devices for this user
# we assume that something is going horribly wrong (e.g. a bot # we assume that something is going horribly wrong (e.g. a bot
# that logs in and creates a new device every time it tries to # that logs in and creates a new device every time it tries to

View File

@@ -1363,8 +1363,12 @@ class FederationHandler(BaseHandler):
if self.hs.config.block_non_admin_invites: if self.hs.config.block_non_admin_invites:
raise SynapseError(403, "This server does not accept room invites") raise SynapseError(403, "This server does not accept room invites")
is_published = yield self.store.is_room_published(event.room_id)
if not self.spam_checker.user_may_invite( if not self.spam_checker.user_may_invite(
event.sender, event.state_key, event.room_id, event.sender, event.state_key, None,
room_id=event.room_id, new_room=False,
published_room=is_published,
): ):
raise SynapseError( raise SynapseError(
403, "This user is not permitted to send invites to this server/user" 403, "This user is not permitted to send invites to this server/user"
@@ -2606,7 +2610,7 @@ class FederationHandler(BaseHandler):
room_version, event_dict, event, context room_version, event_dict, event, context
) )
EventValidator().validate_new(event) EventValidator().validate_new(event, self.config)
# We need to tell the transaction queue to send this out, even # We need to tell the transaction queue to send this out, even
# though the sender isn't a local user. # though the sender isn't a local user.
@@ -2677,12 +2681,6 @@ class FederationHandler(BaseHandler):
# though the sender isn't a local user. # though the sender isn't a local user.
event.internal_metadata.send_on_behalf_of = get_domain_from_id(event.sender) event.internal_metadata.send_on_behalf_of = get_domain_from_id(event.sender)
# XXX we send the invite here, but send_membership_event also sends it,
# so we end up making two requests. I think this is redundant.
returned_invite = yield self.send_invite(origin, event)
# TODO: Make sure the signatures actually are correct.
event.signatures.update(returned_invite.signatures)
member_handler = self.hs.get_room_member_handler() member_handler = self.hs.get_room_member_handler()
yield member_handler.send_membership_event(None, event, context) yield member_handler.send_membership_event(None, event, context)
@@ -2717,7 +2715,7 @@ class FederationHandler(BaseHandler):
event, context = yield self.event_creation_handler.create_new_client_event( event, context = yield self.event_creation_handler.create_new_client_event(
builder=builder, builder=builder,
) )
EventValidator().validate_new(event) EventValidator().validate_new(event, self.config)
defer.returnValue((event, context)) defer.returnValue((event, context))
@defer.inlineCallbacks @defer.inlineCallbacks

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2015, 2016 OpenMarket Ltd # Copyright 2015, 2016 OpenMarket Ltd
# Copyright 2017 Vector Creations Ltd # Copyright 2017 Vector Creations Ltd
# Copyright 2018 New Vector Ltd # Copyright 2018, 2019 New Vector Ltd
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -20,13 +20,18 @@
import logging import logging
from canonicaljson import json from canonicaljson import json
from signedjson.key import decode_verify_key_bytes
from signedjson.sign import verify_signed_json
from unpaddedbase64 import decode_base64
from twisted.internet import defer from twisted.internet import defer
from synapse.api.errors import ( from synapse.api.errors import (
AuthError,
CodeMessageException, CodeMessageException,
Codes, Codes,
HttpResponseException, HttpResponseException,
ProxiedRequestError,
SynapseError, SynapseError,
) )
@@ -47,6 +52,8 @@ class IdentityHandler(BaseHandler):
self.trust_any_id_server_just_for_testing_do_not_use = ( self.trust_any_id_server_just_for_testing_do_not_use = (
hs.config.use_insecure_ssl_client_just_for_testing_do_not_use hs.config.use_insecure_ssl_client_just_for_testing_do_not_use
) )
self.rewrite_identity_server_urls = hs.config.rewrite_identity_server_urls
self._enable_lookup = hs.config.enable_3pid_lookup
def _should_trust_id_server(self, id_server): def _should_trust_id_server(self, id_server):
if id_server not in self.trusted_id_servers: if id_server not in self.trusted_id_servers:
@@ -84,7 +91,10 @@ class IdentityHandler(BaseHandler):
'credentials', id_server 'credentials', id_server
) )
defer.returnValue(None) defer.returnValue(None)
# if we have a rewrite rule set for the identity server,
# apply it now.
if id_server in self.rewrite_identity_server_urls:
id_server = self.rewrite_identity_server_urls[id_server]
try: try:
data = yield self.http_client.get_json( data = yield self.http_client.get_json(
"https://%s%s" % ( "https://%s%s" % (
@@ -120,10 +130,18 @@ class IdentityHandler(BaseHandler):
else: else:
raise SynapseError(400, "No client_secret in creds") raise SynapseError(400, "No client_secret in creds")
# if we have a rewrite rule set for the identity server,
# apply it now, but only for sending the request (not
# storing in the database).
if id_server in self.rewrite_identity_server_urls:
id_server_host = self.rewrite_identity_server_urls[id_server]
else:
id_server_host = id_server
try: try:
data = yield self.http_client.post_urlencoded_get_json( data = yield self.http_client.post_urlencoded_get_json(
"https://%s%s" % ( "https://%s%s" % (
id_server, "/_matrix/identity/api/v1/3pid/bind" id_server_host, "/_matrix/identity/api/v1/3pid/bind"
), ),
{ {
'sid': creds['sid'], 'sid': creds['sid'],
@@ -221,6 +239,16 @@ class IdentityHandler(BaseHandler):
b"Authorization": auth_headers, b"Authorization": auth_headers,
} }
# if we have a rewrite rule set for the identity server,
# apply it now.
#
# Note that destination_is has to be the real id_server, not
# the server we connect to.
if id_server in self.rewrite_identity_server_urls:
id_server = self.rewrite_identity_server_urls[id_server]
url = "https://%s/_matrix/identity/api/v1/3pid/unbind" % (id_server,)
try: try:
yield self.http_client.post_json_get_json( yield self.http_client.post_json_get_json(
url, url,
@@ -267,6 +295,11 @@ class IdentityHandler(BaseHandler):
'send_attempt': send_attempt, 'send_attempt': send_attempt,
} }
# if we have a rewrite rule set for the identity server,
# apply it now.
if id_server in self.rewrite_identity_server_urls:
id_server = self.rewrite_identity_server_urls[id_server]
if next_link: if next_link:
params.update({'next_link': next_link}) params.update({'next_link': next_link})
@@ -301,7 +334,10 @@ class IdentityHandler(BaseHandler):
'send_attempt': send_attempt, 'send_attempt': send_attempt,
} }
params.update(kwargs) params.update(kwargs)
# if we have a rewrite rule set for the identity server,
# apply it now.
if id_server in self.rewrite_identity_server_urls:
id_server = self.rewrite_identity_server_urls[id_server]
try: try:
data = yield self.http_client.post_json_get_json( data = yield self.http_client.post_json_get_json(
"https://%s%s" % ( "https://%s%s" % (
@@ -314,3 +350,125 @@ class IdentityHandler(BaseHandler):
except HttpResponseException as e: except HttpResponseException as e:
logger.info("Proxied requestToken failed: %r", e) logger.info("Proxied requestToken failed: %r", e)
raise e.to_synapse_error() raise e.to_synapse_error()
@defer.inlineCallbacks
def lookup_3pid(self, id_server, medium, address):
"""Looks up a 3pid in the passed identity server.
Args:
id_server (str): The server name (including port, if required)
of the identity server to use.
medium (str): The type of the third party identifier (e.g. "email").
address (str): The third party identifier (e.g. "foo@example.com").
Returns:
Deferred[dict]: The result of the lookup. See
https://matrix.org/docs/spec/identity_service/r0.1.0.html#association-lookup
for details
"""
if not self._should_trust_id_server(id_server):
raise SynapseError(
400, "Untrusted ID server '%s'" % id_server,
Codes.SERVER_NOT_TRUSTED
)
if not self._enable_lookup:
raise AuthError(
403, "Looking up third-party identifiers is denied from this server",
)
target = self.rewrite_identity_server_urls.get(id_server, id_server)
try:
data = yield self.http_client.get_json(
"https://%s/_matrix/identity/api/v1/lookup" % (target,),
{
"medium": medium,
"address": address,
}
)
if "mxid" in data:
if "signatures" not in data:
raise AuthError(401, "No signatures on 3pid binding")
yield self._verify_any_signature(data, id_server)
except HttpResponseException as e:
logger.info("Proxied lookup failed: %r", e)
raise e.to_synapse_error()
except IOError as e:
logger.info("Failed to contact %r: %s", id_server, e)
raise ProxiedRequestError(503, "Failed to contact identity server")
defer.returnValue(data)
@defer.inlineCallbacks
def bulk_lookup_3pid(self, id_server, threepids):
"""Looks up given 3pids in the passed identity server.
Args:
id_server (str): The server name (including port, if required)
of the identity server to use.
threepids ([[str, str]]): The third party identifiers to lookup, as
a list of 2-string sized lists ([medium, address]).
Returns:
Deferred[dict]: The result of the lookup. See
https://matrix.org/docs/spec/identity_service/r0.1.0.html#association-lookup
for details
"""
if not self._should_trust_id_server(id_server):
raise SynapseError(
400, "Untrusted ID server '%s'" % id_server,
Codes.SERVER_NOT_TRUSTED
)
if not self._enable_lookup:
raise AuthError(
403, "Looking up third-party identifiers is denied from this server",
)
target = self.rewrite_identity_server_urls.get(id_server, id_server)
try:
data = yield self.http_client.post_json_get_json(
"https://%s/_matrix/identity/api/v1/bulk_lookup" % (target,),
{
"threepids": threepids,
}
)
except HttpResponseException as e:
logger.info("Proxied lookup failed: %r", e)
raise e.to_synapse_error()
except IOError as e:
logger.info("Failed to contact %r: %s", id_server, e)
raise ProxiedRequestError(503, "Failed to contact identity server")
defer.returnValue(data)
@defer.inlineCallbacks
def _verify_any_signature(self, data, server_hostname):
if server_hostname not in data["signatures"]:
raise AuthError(401, "No signature from server %s" % (server_hostname,))
for key_name, signature in data["signatures"][server_hostname].items():
target = self.rewrite_identity_server_urls.get(
server_hostname, server_hostname,
)
key_data = yield self.http_client.get_json(
"https://%s/_matrix/identity/api/v1/pubkey/%s" %
(target, key_name,),
)
if "public_key" not in key_data:
raise AuthError(401, "No public key named %s from %s" %
(key_name, server_hostname,))
verify_signed_json(
data,
server_hostname,
decode_verify_key_bytes(key_name, decode_base64(key_data["public_key"]))
)
return
raise AuthError(401, "No signature from server %s" % (server_hostname,))

View File

@@ -372,7 +372,7 @@ class EventCreationHandler(object):
"You must be in the room to create an alias for it", "You must be in the room to create an alias for it",
) )
self.validator.validate_new(event) self.validator.validate_new(event, self.config)
defer.returnValue((event, context)) defer.returnValue((event, context))
@@ -604,7 +604,7 @@ class EventCreationHandler(object):
if requester: if requester:
context.app_service = requester.app_service context.app_service = requester.app_service
self.validator.validate_new(event) self.validator.validate_new(event, self.config)
# If this event is an annotation then we check that that the sender # If this event is an annotation then we check that that the sender
# can't annotate the same way twice (e.g. stops users from liking an # can't annotate the same way twice (e.g. stops users from liking an

View File

@@ -15,11 +15,14 @@
# limitations under the License. # limitations under the License.
import logging import logging
from six import iteritems
from twisted.internet import defer from twisted.internet import defer
from twisted.python.failure import Failure from twisted.python.failure import Failure
from synapse.api.constants import EventTypes, Membership from synapse.api.constants import EventTypes, Membership
from synapse.api.errors import SynapseError from synapse.api.errors import SynapseError
from synapse.metrics.background_process_metrics import run_as_background_process
from synapse.storage.state import StateFilter from synapse.storage.state import StateFilter
from synapse.types import RoomStreamToken from synapse.types import RoomStreamToken
from synapse.util.async_helpers import ReadWriteLock from synapse.util.async_helpers import ReadWriteLock
@@ -79,6 +82,114 @@ class PaginationHandler(object):
self._purges_by_id = {} self._purges_by_id = {}
self._event_serializer = hs.get_event_client_serializer() self._event_serializer = hs.get_event_client_serializer()
self._retention_default_max_lifetime = hs.config.retention_default_max_lifetime
if hs.config.retention_enabled:
# Run the purge jobs described in the configuration file.
for job in hs.config.retention_purge_jobs:
self.clock.looping_call(
run_as_background_process,
job["interval"],
"purge_history_for_rooms_in_range",
self.purge_history_for_rooms_in_range,
job["shortest_max_lifetime"],
job["longest_max_lifetime"],
)
@defer.inlineCallbacks
def purge_history_for_rooms_in_range(self, min_ms, max_ms):
"""Purge outdated events from rooms within the given retention range.
If a default retention policy is defined in the server's configuration and its
'max_lifetime' is within this range, also targets rooms which don't have a
retention policy.
Args:
min_ms (int|None): Duration in milliseconds that define the lower limit of
the range to handle (exclusive). If None, it means that the range has no
lower limit.
max_ms (int|None): Duration in milliseconds that define the upper limit of
the range to handle (inclusive). If None, it means that the range has no
upper limit.
"""
# We want the storage layer to to include rooms with no retention policy in its
# return value only if a default retention policy is defined in the server's
# configuration and that policy's 'max_lifetime' is either lower (or equal) than
# max_ms or higher than min_ms (or both).
if self._retention_default_max_lifetime is not None:
include_null = True
if min_ms is not None and min_ms >= self._retention_default_max_lifetime:
# The default max_lifetime is lower than (or equal to) min_ms.
include_null = False
if max_ms is not None and max_ms < self._retention_default_max_lifetime:
# The default max_lifetime is higher than max_ms.
include_null = False
else:
include_null = False
rooms = yield self.store.get_rooms_for_retention_period_in_range(
min_ms, max_ms, include_null
)
for room_id, retention_policy in iteritems(rooms):
if room_id in self._purges_in_progress_by_room:
logger.warning(
"[purge] not purging room %s as there's an ongoing purge running"
" for this room",
room_id,
)
continue
max_lifetime = retention_policy["max_lifetime"]
if max_lifetime is None:
# If max_lifetime is None, it means that include_null equals True,
# therefore we can safely assume that there is a default policy defined
# in the server's configuration.
max_lifetime = self._retention_default_max_lifetime
# Figure out what token we should start purging at.
ts = self.clock.time_msec() - max_lifetime
stream_ordering = (
yield self.store.find_first_stream_ordering_after_ts(ts)
)
r = (
yield self.store.get_room_event_after_stream_ordering(
room_id, stream_ordering,
)
)
if not r:
logger.warning(
"[purge] purging events not possible: No event found "
"(ts %i => stream_ordering %i)",
ts, stream_ordering,
)
continue
(stream, topo, _event_id) = r
token = "t%d-%d" % (topo, stream)
purge_id = random_string(16)
self._purges_by_id[purge_id] = PurgeStatus()
logger.info(
"Starting purging events in room %s (purge_id %s)" % (room_id, purge_id)
)
# We want to purge everything, including local events, and to run the purge in
# the background so that it's not blocking any other operation apart from
# other purges in the same room.
run_as_background_process(
"_purge_history",
self._purge_history,
purge_id, room_id, token, True,
)
def start_purge_history(self, room_id, token, def start_purge_history(self, room_id, token,
delete_local_events=False): delete_local_events=False):
"""Start off a history purge on a room. """Start off a history purge on a room.

View 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,
)

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2014-2016 OpenMarket Ltd # Copyright 2014-2016 OpenMarket Ltd
# Copyright 2018 New Vector Ltd
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -15,7 +16,11 @@
import logging import logging
from twisted.internet import defer from six.moves import range
from signedjson.sign import sign_json
from twisted.internet import defer, reactor
from synapse.api.errors import ( from synapse.api.errors import (
AuthError, AuthError,
@@ -26,6 +31,7 @@ from synapse.api.errors import (
) )
from synapse.metrics.background_process_metrics import run_as_background_process from synapse.metrics.background_process_metrics import run_as_background_process
from synapse.types import UserID, get_domain_from_id from synapse.types import UserID, get_domain_from_id
from synapse.util.logcontext import run_in_background
from ._base import BaseHandler from ._base import BaseHandler
@@ -43,6 +49,8 @@ class BaseProfileHandler(BaseHandler):
subclass MasterProfileHandler subclass MasterProfileHandler
""" """
PROFILE_REPLICATE_INTERVAL = 2 * 60 * 1000
def __init__(self, hs): def __init__(self, hs):
super(BaseProfileHandler, self).__init__(hs) super(BaseProfileHandler, self).__init__(hs)
@@ -53,6 +61,84 @@ class BaseProfileHandler(BaseHandler):
self.user_directory_handler = hs.get_user_directory_handler() self.user_directory_handler = hs.get_user_directory_handler()
self.http_client = hs.get_simple_http_client()
if hs.config.worker_app is None:
self.clock.looping_call(
self._start_update_remote_profile_cache, self.PROFILE_UPDATE_MS,
)
if len(self.hs.config.replicate_user_profiles_to) > 0:
reactor.callWhenRunning(self._assign_profile_replication_batches)
reactor.callWhenRunning(self._replicate_profiles)
# Add a looping call to replicate_profiles: this handles retries
# if the replication is unsuccessful when the user updated their
# profile.
self.clock.looping_call(
self._replicate_profiles, self.PROFILE_REPLICATE_INTERVAL
)
@defer.inlineCallbacks
def _assign_profile_replication_batches(self):
"""If no profile replication has been done yet, allocate replication batch
numbers to each profile to start the replication process.
"""
logger.info("Assigning profile batch numbers...")
total = 0
while True:
assigned = yield self.store.assign_profile_batch()
total += assigned
if assigned == 0:
break
logger.info("Assigned %d profile batch numbers", total)
@defer.inlineCallbacks
def _replicate_profiles(self):
"""If any profile data has been updated and not pushed to the replication targets,
replicate it.
"""
host_batches = yield self.store.get_replication_hosts()
latest_batch = yield self.store.get_latest_profile_replication_batch_number()
if latest_batch is None:
latest_batch = -1
for repl_host in self.hs.config.replicate_user_profiles_to:
if repl_host not in host_batches:
host_batches[repl_host] = -1
try:
for i in range(host_batches[repl_host] + 1, latest_batch + 1):
yield self._replicate_host_profile_batch(repl_host, i)
except Exception:
logger.exception(
"Exception while replicating to %s: aborting for now", repl_host,
)
@defer.inlineCallbacks
def _replicate_host_profile_batch(self, host, batchnum):
logger.info("Replicating profile batch %d to %s", batchnum, host)
batch_rows = yield self.store.get_profile_batch(batchnum)
batch = {
UserID(r["user_id"], self.hs.hostname).to_string(): ({
"display_name": r["displayname"],
"avatar_url": r["avatar_url"],
} if r["active"] else None) for r in batch_rows
}
url = "https://%s/_matrix/identity/api/v1/replicate_profiles" % (host,)
body = {
"batchnum": batchnum,
"batch": batch,
"origin_server": self.hs.hostname,
}
signed_body = sign_json(body, self.hs.hostname, self.hs.config.signing_key[0])
try:
yield self.http_client.post_json_get_json(url, signed_body)
yield self.store.update_replication_batch_for_host(host, batchnum)
logger.info("Sucessfully replicated profile batch %d to %s", batchnum, host)
except Exception:
# This will get retried when the looping call next comes around
logger.exception("Failed to replicate profile batch %d to %s", batchnum, host)
raise
@defer.inlineCallbacks @defer.inlineCallbacks
def get_profile(self, user_id): def get_profile(self, user_id):
target_user = UserID.from_string(user_id) target_user = UserID.from_string(user_id)
@@ -162,9 +248,14 @@ class BaseProfileHandler(BaseHandler):
if not self.hs.is_mine(target_user): if not self.hs.is_mine(target_user):
raise SynapseError(400, "User is not hosted on this Home Server") raise SynapseError(400, "User is not hosted on this Home Server")
if not by_admin and target_user != requester.user: if not by_admin and requester and target_user != requester.user:
raise AuthError(400, "Cannot set another user's displayname") raise AuthError(400, "Cannot set another user's displayname")
if not by_admin and self.hs.config.disable_set_displayname:
profile = yield self.store.get_profileinfo(target_user.localpart)
if profile.display_name:
raise SynapseError(400, "Changing displayname is disabled on this server")
if len(new_displayname) > MAX_DISPLAYNAME_LEN: if len(new_displayname) > MAX_DISPLAYNAME_LEN:
raise SynapseError( raise SynapseError(
400, "Displayname is too long (max %i)" % (MAX_DISPLAYNAME_LEN, ), 400, "Displayname is too long (max %i)" % (MAX_DISPLAYNAME_LEN, ),
@@ -173,8 +264,14 @@ class BaseProfileHandler(BaseHandler):
if new_displayname == '': if new_displayname == '':
new_displayname = None new_displayname = None
if len(self.hs.config.replicate_user_profiles_to) > 0:
cur_batchnum = yield self.store.get_latest_profile_replication_batch_number()
new_batchnum = 0 if cur_batchnum is None else cur_batchnum + 1
else:
new_batchnum = None
yield self.store.set_profile_displayname( yield self.store.set_profile_displayname(
target_user.localpart, new_displayname target_user.localpart, new_displayname, new_batchnum
) )
if self.hs.config.user_directory_search_all_users: if self.hs.config.user_directory_search_all_users:
@@ -183,7 +280,37 @@ class BaseProfileHandler(BaseHandler):
target_user.to_string(), profile target_user.to_string(), profile
) )
yield self._update_join_states(requester, target_user) if requester:
yield self._update_join_states(requester, target_user)
# start a profile replication push
run_in_background(self._replicate_profiles)
@defer.inlineCallbacks
def set_active(self, target_user, active, hide):
"""
Sets the 'active' flag on a user profile. If set to false, the user
account is considered deactivated or hidden.
If 'hide' is true, then we interpret active=False as a request to try to
hide the user rather than deactivating it. This means withholding the
profile from replication (and mark it as inactive) rather than clearing
the profile from the HS DB. Note that unlike set_displayname and
set_avatar_url, this does *not* perform authorization checks! This is
because the only place it's used currently is in account deactivation
where we've already done these checks anyway.
"""
if len(self.hs.config.replicate_user_profiles_to) > 0:
cur_batchnum = yield self.store.get_latest_profile_replication_batch_number()
new_batchnum = 0 if cur_batchnum is None else cur_batchnum + 1
else:
new_batchnum = None
yield self.store.set_profile_active(
target_user.localpart, active, hide, new_batchnum
)
# start a profile replication push
run_in_background(self._replicate_profiles)
@defer.inlineCallbacks @defer.inlineCallbacks
def get_avatar_url(self, target_user): def get_avatar_url(self, target_user):
@@ -225,13 +352,24 @@ class BaseProfileHandler(BaseHandler):
if not by_admin and target_user != requester.user: if not by_admin and target_user != requester.user:
raise AuthError(400, "Cannot set another user's avatar_url") raise AuthError(400, "Cannot set another user's avatar_url")
if not by_admin and self.hs.config.disable_set_avatar_url:
profile = yield self.store.get_profileinfo(target_user.localpart)
if profile.avatar_url:
raise SynapseError(400, "Changing avatar url is disabled on this server")
if len(self.hs.config.replicate_user_profiles_to) > 0:
cur_batchnum = yield self.store.get_latest_profile_replication_batch_number()
new_batchnum = 0 if cur_batchnum is None else cur_batchnum + 1
else:
new_batchnum = None
if len(new_avatar_url) > MAX_AVATAR_URL_LEN: if len(new_avatar_url) > MAX_AVATAR_URL_LEN:
raise SynapseError( raise SynapseError(
400, "Avatar URL is too long (max %i)" % (MAX_AVATAR_URL_LEN, ), 400, "Avatar URL is too long (max %i)" % (MAX_AVATAR_URL_LEN, ),
) )
yield self.store.set_profile_avatar_url( yield self.store.set_profile_avatar_url(
target_user.localpart, new_avatar_url target_user.localpart, new_avatar_url, new_batchnum,
) )
if self.hs.config.user_directory_search_all_users: if self.hs.config.user_directory_search_all_users:
@@ -242,6 +380,9 @@ class BaseProfileHandler(BaseHandler):
yield self._update_join_states(requester, target_user) yield self._update_join_states(requester, target_user)
# start a profile replication push
run_in_background(self._replicate_profiles)
@defer.inlineCallbacks @defer.inlineCallbacks
def on_profile_query(self, args): def on_profile_query(self, args):
user = UserID.from_string(args["user_id"]) user = UserID.from_string(args["user_id"])
@@ -321,6 +462,10 @@ class BaseProfileHandler(BaseHandler):
if not self.hs.config.require_auth_for_profile_requests or not requester: if not self.hs.config.require_auth_for_profile_requests or not requester:
return return
# Always allow the user to query their own profile.
if target_user.to_string() == requester.to_string():
return
try: try:
requester_rooms = yield self.store.get_rooms_for_user( requester_rooms = yield self.store.get_rooms_for_user(
requester.to_string() requester.to_string()

View File

@@ -61,6 +61,7 @@ class RegistrationHandler(BaseHandler):
self.profile_handler = hs.get_profile_handler() self.profile_handler = hs.get_profile_handler()
self.user_directory_handler = hs.get_user_directory_handler() self.user_directory_handler = hs.get_user_directory_handler()
self.captcha_client = CaptchaServerHttpClient(hs) self.captcha_client = CaptchaServerHttpClient(hs)
self.http_client = hs.get_simple_http_client()
self.identity_handler = self.hs.get_handlers().identity_handler self.identity_handler = self.hs.get_handlers().identity_handler
self.ratelimiter = hs.get_registration_ratelimiter() self.ratelimiter = hs.get_registration_ratelimiter()
@@ -73,6 +74,8 @@ class RegistrationHandler(BaseHandler):
) )
self._server_notices_mxid = hs.config.server_notices_mxid self._server_notices_mxid = hs.config.server_notices_mxid
self._show_in_user_directory = self.hs.config.show_users_in_user_directory
if hs.config.worker_app: if hs.config.worker_app:
self._register_client = ReplicationRegisterServlet.make_client(hs) self._register_client = ReplicationRegisterServlet.make_client(hs)
self._register_device_client = ( self._register_device_client = (
@@ -234,6 +237,11 @@ class RegistrationHandler(BaseHandler):
address=address, address=address,
) )
if default_display_name:
yield self.profile_handler.set_displayname(
user, None, default_display_name, by_admin=True,
)
if self.hs.config.user_directory_search_all_users: if self.hs.config.user_directory_search_all_users:
profile = yield self.store.get_profileinfo(localpart) profile = yield self.store.get_profileinfo(localpart)
yield self.user_directory_handler.handle_local_profile_change( yield self.user_directory_handler.handle_local_profile_change(
@@ -263,6 +271,11 @@ class RegistrationHandler(BaseHandler):
create_profile_with_displayname=default_display_name, create_profile_with_displayname=default_display_name,
address=address, address=address,
) )
yield self.profile_handler.set_displayname(
user, None, default_display_name, by_admin=True,
)
except SynapseError: except SynapseError:
# if user id is taken, just generate another # if user id is taken, just generate another
user = None user = None
@@ -287,6 +300,14 @@ class RegistrationHandler(BaseHandler):
user_id, threepid_dict, None, False, user_id, threepid_dict, None, False,
) )
# Prevent the new user from showing up in the user directory if the server
# mandates it.
if not self._show_in_user_directory:
yield self.store.add_account_data_for_user(
user_id, "im.vector.hide_profile", {'hide_profile': True},
)
yield self.profile_handler.set_active(user, False, True)
defer.returnValue((user_id, token)) defer.returnValue((user_id, token))
@defer.inlineCallbacks @defer.inlineCallbacks
@@ -356,7 +377,9 @@ class RegistrationHandler(BaseHandler):
yield self._auto_join_rooms(user_id) yield self._auto_join_rooms(user_id)
@defer.inlineCallbacks @defer.inlineCallbacks
def appservice_register(self, user_localpart, as_token): def appservice_register(self, user_localpart, as_token, password, display_name):
# FIXME: this should be factored out and merged with normal register()
user = UserID(user_localpart, self.hs.hostname) user = UserID(user_localpart, self.hs.hostname)
user_id = user.to_string() user_id = user.to_string()
service = self.store.get_app_service_by_token(as_token) service = self.store.get_app_service_by_token(as_token)
@@ -374,12 +397,29 @@ class RegistrationHandler(BaseHandler):
user_id, allowed_appservice=service user_id, allowed_appservice=service
) )
password_hash = ""
if password:
password_hash = yield self.auth_handler().hash(password)
display_name = display_name or user.localpart
yield self.register_with_store( yield self.register_with_store(
user_id=user_id, user_id=user_id,
password_hash="", password_hash=password_hash,
appservice_id=service_id, appservice_id=service_id,
create_profile_with_displayname=user.localpart, create_profile_with_displayname=display_name,
) )
yield self.profile_handler.set_displayname(
user, None, display_name, by_admin=True,
)
if self.hs.config.user_directory_search_all_users:
profile = yield self.store.get_profileinfo(user_localpart)
yield self.user_directory_handler.handle_local_profile_change(
user_id, profile
)
defer.returnValue(user_id) defer.returnValue(user_id)
@defer.inlineCallbacks @defer.inlineCallbacks
@@ -405,6 +445,39 @@ class RegistrationHandler(BaseHandler):
else: else:
logger.info("Valid captcha entered from %s", ip) logger.info("Valid captcha entered from %s", ip)
@defer.inlineCallbacks
def register_saml2(self, localpart):
"""
Registers email_id as SAML2 Based Auth.
"""
if types.contains_invalid_mxid_characters(localpart):
raise SynapseError(
400,
"User ID can only contain characters a-z, 0-9, or '=_-./'",
)
yield self.auth.check_auth_blocking()
user = UserID(localpart, self.hs.hostname)
user_id = user.to_string()
yield self.check_user_id_not_appservice_exclusive(user_id)
token = self.macaroon_gen.generate_access_token(user_id)
try:
yield self.register_with_store(
user_id=user_id,
token=token,
password_hash=None,
create_profile_with_displayname=user.localpart,
)
yield self.profile_handler.set_displayname(
user, None, user.localpart, by_admin=True,
)
except Exception as e:
yield self.store.add_access_token_to_user(user_id, token)
# Ignore Registration errors
logger.exception(e)
defer.returnValue((user_id, token))
@defer.inlineCallbacks @defer.inlineCallbacks
def register_email(self, threepidCreds): def register_email(self, threepidCreds):
""" """
@@ -427,7 +500,9 @@ class RegistrationHandler(BaseHandler):
logger.info("got threepid with medium '%s' and address '%s'", logger.info("got threepid with medium '%s' and address '%s'",
threepid['medium'], threepid['address']) threepid['medium'], threepid['address'])
if not check_3pid_allowed(self.hs, threepid['medium'], threepid['address']): if not (
yield check_3pid_allowed(self.hs, threepid['medium'], threepid['address'])
):
raise RegistrationError( raise RegistrationError(
403, "Third party identifier is not allowed" 403, "Third party identifier is not allowed"
) )
@@ -468,6 +543,39 @@ class RegistrationHandler(BaseHandler):
errcode=Codes.EXCLUSIVE errcode=Codes.EXCLUSIVE
) )
@defer.inlineCallbacks
def shadow_register(self, localpart, display_name, auth_result, params):
"""Invokes the current registration on another server, using
shared secret registration, passing in any auth_results from
other registration UI auth flows (e.g. validated 3pids)
Useful for setting up shadow/backup accounts on a parallel deployment.
"""
# TODO: retries
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
as_token = self.hs.config.shadow_server.get("as_token")
yield self.http_client.post_json_get_json(
"%s/_matrix/client/r0/register?access_token=%s" % (
shadow_hs_url, as_token,
),
{
# XXX: auth_result is an unspecified extension for shadow registration
'auth_result': auth_result,
# XXX: another unspecified extension for shadow registration to ensure
# that the displayname is correctly set by the masters erver
'display_name': display_name,
'username': localpart,
'password': params.get("password"),
'bind_email': params.get("bind_email"),
'bind_msisdn': params.get("bind_msisdn"),
'device_id': params.get("device_id"),
'initial_device_display_name': params.get("initial_device_display_name"),
'inhibit_login': False,
'access_token': as_token,
}
)
@defer.inlineCallbacks @defer.inlineCallbacks
def _generate_user_id(self, reseed=False): def _generate_user_id(self, reseed=False):
if reseed or self._next_generated_user_id is None: if reseed or self._next_generated_user_id is None:
@@ -556,18 +664,16 @@ class RegistrationHandler(BaseHandler):
user_id=user_id, user_id=user_id,
token=token, token=token,
password_hash=password_hash, password_hash=password_hash,
create_profile_with_displayname=user.localpart, create_profile_with_displayname=displayname or user.localpart,
) )
if displayname is not None:
yield self.profile_handler.set_displayname(
user, None, displayname or user.localpart, by_admin=True,
)
else: else:
yield self._auth_handler.delete_access_tokens_for_user(user_id) yield self._auth_handler.delete_access_tokens_for_user(user_id)
yield self.store.add_access_token_to_user(user_id=user_id, token=token) yield self.store.add_access_token_to_user(user_id=user_id, token=token)
if displayname is not None:
logger.info("setting user display name: %s -> %s", user_id, displayname)
yield self.profile_handler.set_displayname(
user, requester, displayname, by_admin=True,
)
defer.returnValue((user_id, token)) defer.returnValue((user_id, token))
@defer.inlineCallbacks @defer.inlineCallbacks

View File

@@ -49,12 +49,14 @@ class RoomCreationHandler(BaseHandler):
"history_visibility": "shared", "history_visibility": "shared",
"original_invitees_have_ops": False, "original_invitees_have_ops": False,
"guest_can_join": True, "guest_can_join": True,
"encryption_alg": "m.megolm.v1.aes-sha2",
}, },
RoomCreationPreset.TRUSTED_PRIVATE_CHAT: { RoomCreationPreset.TRUSTED_PRIVATE_CHAT: {
"join_rules": JoinRules.INVITE, "join_rules": JoinRules.INVITE,
"history_visibility": "shared", "history_visibility": "shared",
"original_invitees_have_ops": True, "original_invitees_have_ops": True,
"guest_can_join": True, "guest_can_join": True,
"encryption_alg": "m.megolm.v1.aes-sha2",
}, },
RoomCreationPreset.PUBLIC_CHAT: { RoomCreationPreset.PUBLIC_CHAT: {
"join_rules": JoinRules.PUBLIC, "join_rules": JoinRules.PUBLIC,
@@ -75,6 +77,10 @@ class RoomCreationHandler(BaseHandler):
# linearizer to stop two upgrades happening at once # linearizer to stop two upgrades happening at once
self._upgrade_linearizer = Linearizer("room_upgrade_linearizer") self._upgrade_linearizer = Linearizer("room_upgrade_linearizer")
self._server_notices_mxid = hs.config.server_notices_mxid
self.third_party_event_rules = hs.get_third_party_event_rules()
@defer.inlineCallbacks @defer.inlineCallbacks
def upgrade_room(self, requester, old_room_id, new_version): def upgrade_room(self, requester, old_room_id, new_version):
"""Replace a room with a new room with a different version """Replace a room with a new room with a different version
@@ -248,7 +254,22 @@ class RoomCreationHandler(BaseHandler):
""" """
user_id = requester.user.to_string() user_id = requester.user.to_string()
if not self.spam_checker.user_may_create_room(user_id): if (self._server_notices_mxid is not None and
requester.user.to_string() == self._server_notices_mxid):
# allow the server notices mxid to create rooms
is_requester_admin = True
else:
is_requester_admin = yield self.auth.is_server_admin(
requester.user,
)
if not is_requester_admin and not self.spam_checker.user_may_create_room(
user_id,
invite_list=[],
third_party_invite_list=[],
cloning=True,
):
raise SynapseError(403, "You are not permitted to create rooms") raise SynapseError(403, "You are not permitted to create rooms")
creation_content = { creation_content = {
@@ -470,7 +491,32 @@ class RoomCreationHandler(BaseHandler):
yield 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): if (self._server_notices_mxid is not None and
requester.user.to_string() == self._server_notices_mxid):
# allow the server notices mxid to create rooms
is_requester_admin = True
else:
is_requester_admin = yield self.auth.is_server_admin(
requester.user,
)
# Check whether the third party rules allows/changes the room create
# request.
yield self.third_party_event_rules.on_create_room(
requester,
config,
is_requester_admin=is_requester_admin,
)
invite_list = config.get("invite", [])
invite_3pid_list = config.get("invite_3pid", [])
if not is_requester_admin and not self.spam_checker.user_may_create_room(
user_id,
invite_list=invite_list,
third_party_invite_list=invite_3pid_list,
cloning=False,
):
raise SynapseError(403, "You are not permitted to create rooms") raise SynapseError(403, "You are not permitted to create rooms")
if ratelimit: if ratelimit:
@@ -517,7 +563,6 @@ class RoomCreationHandler(BaseHandler):
else: else:
room_alias = None room_alias = None
invite_list = config.get("invite", [])
for i in invite_list: for i in invite_list:
try: try:
UserID.from_string(i) UserID.from_string(i)
@@ -528,8 +573,6 @@ class RoomCreationHandler(BaseHandler):
requester, requester,
) )
invite_3pid_list = config.get("invite_3pid", [])
visibility = config.get("visibility", None) visibility = config.get("visibility", None)
is_public = visibility == "public" is_public = visibility == "public"
@@ -615,6 +658,7 @@ class RoomCreationHandler(BaseHandler):
"invite", "invite",
ratelimit=False, ratelimit=False,
content=content, content=content,
new_room=True,
) )
for invite_3pid in invite_3pid_list: for invite_3pid in invite_3pid_list:
@@ -629,6 +673,7 @@ class RoomCreationHandler(BaseHandler):
id_server, id_server,
requester, requester,
txn_id=None, txn_id=None,
new_room=True,
) )
result = {"room_id": room_id} result = {"room_id": room_id}
@@ -699,6 +744,7 @@ class RoomCreationHandler(BaseHandler):
"join", "join",
ratelimit=False, ratelimit=False,
content=creator_join_profile, content=creator_join_profile,
new_room=True,
) )
# We treat the power levels override specially as this needs to be one # We treat the power levels override specially as this needs to be one
@@ -774,6 +820,15 @@ class RoomCreationHandler(BaseHandler):
content=content, content=content,
) )
if "encryption_alg" in config:
yield send(
etype=EventTypes.Encryption,
state_key="",
content={
'algorithm': config["encryption_alg"],
}
)
@defer.inlineCallbacks @defer.inlineCallbacks
def _generate_room_id(self, creator_id, is_public): def _generate_room_id(self, creator_id, is_public):
# autogen room IDs and try to create it. We may clash, so just # autogen room IDs and try to create it. We may clash, so just

View File

@@ -20,16 +20,12 @@ import logging
from six.moves import http_client from six.moves import http_client
from signedjson.key import decode_verify_key_bytes
from signedjson.sign import verify_signed_json
from unpaddedbase64 import decode_base64
from twisted.internet import defer from twisted.internet import defer
import synapse.server import synapse.server
import synapse.types import synapse.types
from synapse.api.constants import EventTypes, Membership from synapse.api.constants import EventTypes, Membership
from synapse.api.errors import AuthError, Codes, SynapseError from synapse.api.errors import AuthError, Codes, ProxiedRequestError, SynapseError
from synapse.types import RoomID, UserID from synapse.types import RoomID, UserID
from synapse.util.async_helpers import Linearizer from synapse.util.async_helpers import Linearizer
from synapse.util.distributor import user_joined_room, user_left_room from synapse.util.distributor import user_joined_room, user_left_room
@@ -67,12 +63,15 @@ class RoomMemberHandler(object):
self.registration_handler = hs.get_registration_handler() self.registration_handler = hs.get_registration_handler()
self.profile_handler = hs.get_profile_handler() self.profile_handler = hs.get_profile_handler()
self.event_creation_handler = hs.get_event_creation_handler() self.event_creation_handler = hs.get_event_creation_handler()
self.identity_handler = hs.get_handlers().identity_handler
self.member_linearizer = Linearizer(name="member") self.member_linearizer = Linearizer(name="member")
self.clock = hs.get_clock() self.clock = hs.get_clock()
self.spam_checker = hs.get_spam_checker() self.spam_checker = hs.get_spam_checker()
self.third_party_event_rules = hs.get_third_party_event_rules()
self._server_notices_mxid = self.config.server_notices_mxid self._server_notices_mxid = self.config.server_notices_mxid
self.rewrite_identity_server_urls = self.config.rewrite_identity_server_urls
self._enable_lookup = hs.config.enable_3pid_lookup self._enable_lookup = hs.config.enable_3pid_lookup
self.allow_per_room_profiles = self.config.allow_per_room_profiles self.allow_per_room_profiles = self.config.allow_per_room_profiles
@@ -317,8 +316,31 @@ class RoomMemberHandler(object):
third_party_signed=None, third_party_signed=None,
ratelimit=True, ratelimit=True,
content=None, content=None,
new_room=False,
require_consent=True, require_consent=True,
): ):
"""Update a users membership in a room
Args:
requester (Requester)
target (UserID)
room_id (str)
action (str): The "action" the requester is performing against the
target. One of join/leave/kick/ban/invite/unban.
txn_id (str|None): The transaction ID associated with the request,
or None not provided.
remote_room_hosts (list[str]|None): List of remote servers to try
and join via if server isn't already in the room.
third_party_signed (dict|None): The signed object for third party
invites.
ratelimit (bool): Whether to apply ratelimiting to this request.
content (dict|None): Fields to include in the new events content.
new_room (bool): Whether these membership changes are happening
as part of a room creation (e.g. initial joins and invites)
Returns:
Deferred[FrozenEvent]
"""
key = (room_id,) key = (room_id,)
with (yield self.member_linearizer.queue(key)): with (yield self.member_linearizer.queue(key)):
@@ -332,6 +354,7 @@ class RoomMemberHandler(object):
third_party_signed=third_party_signed, third_party_signed=third_party_signed,
ratelimit=ratelimit, ratelimit=ratelimit,
content=content, content=content,
new_room=new_room,
require_consent=require_consent, require_consent=require_consent,
) )
@@ -349,6 +372,7 @@ class RoomMemberHandler(object):
third_party_signed=None, third_party_signed=None,
ratelimit=True, ratelimit=True,
content=None, content=None,
new_room=False,
require_consent=True, require_consent=True,
): ):
content_specified = bool(content) content_specified = bool(content)
@@ -416,8 +440,14 @@ class RoomMemberHandler(object):
) )
block_invite = True block_invite = True
is_published = yield self.store.is_room_published(room_id)
if not self.spam_checker.user_may_invite( if not self.spam_checker.user_may_invite(
requester.user.to_string(), target.to_string(), room_id, requester.user.to_string(), target.to_string(),
third_party_invite=None,
room_id=room_id,
new_room=new_room,
published_room=is_published,
): ):
logger.info("Blocking invite due to spam checker") logger.info("Blocking invite due to spam checker")
block_invite = True block_invite = True
@@ -496,8 +526,29 @@ class RoomMemberHandler(object):
# so don't really fit into the general auth process. # so don't really fit into the general auth process.
raise AuthError(403, "Guest access not allowed") raise AuthError(403, "Guest access not allowed")
if (self._server_notices_mxid is not None and
requester.user.to_string() == self._server_notices_mxid):
# allow the server notices mxid to join rooms
is_requester_admin = True
else:
is_requester_admin = yield self.auth.is_server_admin(
requester.user,
)
inviter = yield self._get_inviter(target.to_string(), room_id)
if not is_requester_admin:
# We assume that if the spam checker allowed the user to create
# a room then they're allowed to join it.
if not new_room and not self.spam_checker.user_may_join_room(
target.to_string(), room_id,
is_invited=inviter is not None,
):
raise SynapseError(
403, "Not allowed to join this room",
)
if not is_host_in_room: if not is_host_in_room:
inviter = yield self._get_inviter(target.to_string(), room_id)
if inviter and not self.hs.is_mine(inviter): if inviter and not self.hs.is_mine(inviter):
remote_room_hosts.append(inviter.domain) remote_room_hosts.append(inviter.domain)
@@ -707,7 +758,8 @@ class RoomMemberHandler(object):
address, address,
id_server, id_server,
requester, requester,
txn_id txn_id,
new_room=False,
): ):
if self.config.block_non_admin_invites: if self.config.block_non_admin_invites:
is_requester_admin = yield self.auth.is_server_admin( is_requester_admin = yield self.auth.is_server_admin(
@@ -723,10 +775,36 @@ class RoomMemberHandler(object):
# can't just rely on the standard ratelimiting of events. # can't just rely on the standard ratelimiting of events.
yield self.base_handler.ratelimit(requester) yield self.base_handler.ratelimit(requester)
can_invite = yield self.third_party_event_rules.check_threepid_can_be_invited(
medium, address, room_id,
)
if not can_invite:
raise SynapseError(
403, "This third-party identifier can not be invited in this room",
Codes.FORBIDDEN,
)
invitee = yield self._lookup_3pid( invitee = yield self._lookup_3pid(
id_server, medium, address id_server, medium, address
) )
is_published = yield self.store.is_room_published(room_id)
if not self.spam_checker.user_may_invite(
requester.user.to_string(), invitee,
third_party_invite={
"medium": medium,
"address": address,
},
room_id=room_id,
new_room=new_room,
published_room=is_published,
):
logger.info("Blocking invite due to spam checker")
raise SynapseError(
403, "Invites have been disabled on this server",
)
if invitee: if invitee:
yield self.update_membership( yield self.update_membership(
requester, requester,
@@ -746,6 +824,20 @@ class RoomMemberHandler(object):
txn_id=txn_id txn_id=txn_id
) )
def _get_id_server_target(self, id_server):
"""Looks up an id_server's actual http endpoint
Args:
id_server (str): the server name to lookup.
Returns:
the http endpoint to connect to.
"""
if id_server in self.rewrite_identity_server_urls:
return self.rewrite_identity_server_urls[id_server]
return id_server
@defer.inlineCallbacks @defer.inlineCallbacks
def _lookup_3pid(self, id_server, medium, address): def _lookup_3pid(self, id_server, medium, address):
"""Looks up a 3pid in the passed identity server. """Looks up a 3pid in the passed identity server.
@@ -759,48 +851,13 @@ class RoomMemberHandler(object):
Returns: Returns:
str: the matrix ID of the 3pid, or None if it is not recognized. str: the matrix ID of the 3pid, or None if it is not recognized.
""" """
if not self._enable_lookup:
raise SynapseError(
403, "Looking up third-party identifiers is denied from this server",
)
try: try:
data = yield self.simple_http_client.get_json( data = yield self.identity_handler.lookup_3pid(id_server, medium, address)
"%s%s/_matrix/identity/api/v1/lookup" % (id_server_scheme, id_server,), defer.returnValue(data.get("mxid"))
{ except ProxiedRequestError as e:
"medium": medium,
"address": address,
}
)
if "mxid" in data:
if "signatures" not in data:
raise AuthError(401, "No signatures on 3pid binding")
yield self._verify_any_signature(data, id_server)
defer.returnValue(data["mxid"])
except IOError as e:
logger.warn("Error from identity server lookup: %s" % (e,)) logger.warn("Error from identity server lookup: %s" % (e,))
defer.returnValue(None) defer.returnValue(None)
@defer.inlineCallbacks
def _verify_any_signature(self, data, server_hostname):
if server_hostname not in data["signatures"]:
raise AuthError(401, "No signature from server %s" % (server_hostname,))
for key_name, signature in data["signatures"][server_hostname].items():
key_data = yield self.simple_http_client.get_json(
"%s%s/_matrix/identity/api/v1/pubkey/%s" %
(id_server_scheme, server_hostname, key_name,),
)
if "public_key" not in key_data:
raise AuthError(401, "No public key named %s from %s" %
(key_name, server_hostname,))
verify_signed_json(
data,
server_hostname,
decode_verify_key_bytes(key_name, decode_base64(key_data["public_key"]))
)
return
@defer.inlineCallbacks @defer.inlineCallbacks
def _make_and_store_3pid_invite( def _make_and_store_3pid_invite(
self, self,
@@ -878,6 +935,7 @@ class RoomMemberHandler(object):
"sender": user.to_string(), "sender": user.to_string(),
"state_key": token, "state_key": token,
}, },
ratelimit=False,
txn_id=txn_id, txn_id=txn_id,
) )
@@ -926,8 +984,9 @@ class RoomMemberHandler(object):
user. user.
""" """
target = self._get_id_server_target(id_server)
is_url = "%s%s/_matrix/identity/api/v1/store-invite" % ( is_url = "%s%s/_matrix/identity/api/v1/store-invite" % (
id_server_scheme, id_server, id_server_scheme, target,
) )
invite_config = { invite_config = {
@@ -967,7 +1026,7 @@ class RoomMemberHandler(object):
fallback_public_key = { fallback_public_key = {
"public_key": data["public_key"], "public_key": data["public_key"],
"key_validity_url": "%s%s/_matrix/identity/api/v1/pubkey/isvalid" % ( "key_validity_url": "%s%s/_matrix/identity/api/v1/pubkey/isvalid" % (
id_server_scheme, id_server, id_server_scheme, target,
), ),
} }
else: else:

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- 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"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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) super(SetPasswordHandler, self).__init__(hs)
self._auth_handler = hs.get_auth_handler() self._auth_handler = hs.get_auth_handler()
self._device_handler = hs.get_device_handler() self._device_handler = hs.get_device_handler()
self._password_policy_handler = hs.get_password_policy_handler()
@defer.inlineCallbacks @defer.inlineCallbacks
def set_password(self, user_id, newpassword, requester=None): def set_password(self, user_id, newpassword, requester=None):
self._password_policy_handler.validate_password(newpassword)
password_hash = yield self._auth_handler.hash(newpassword) password_hash = yield self._auth_handler.hash(newpassword)
except_device_id = requester.device_id if requester else None except_device_id = requester.device_id if requester else None

View File

@@ -117,7 +117,7 @@ class Mailer(object):
link = ( link = (
self.hs.config.public_baseurl + self.hs.config.public_baseurl +
"_synapse/password_reset/email/submit_token" "_matrix/client/unstable/password_reset/email/submit_token"
"?token=%s&client_secret=%s&sid=%s" % "?token=%s&client_secret=%s&sid=%s" %
(token, client_secret, sid) (token, client_secret, sid)
) )

View File

@@ -67,7 +67,7 @@ REQUIREMENTS = [
"pymacaroons>=0.13.0", "pymacaroons>=0.13.0",
"msgpack>=0.5.0", "msgpack>=0.5.0",
"phonenumbers>=8.2.0", "phonenumbers>=8.2.0",
"six>=1.10", "six>=1.12",
# prometheus_client 0.4.0 changed the format of counter metrics # prometheus_client 0.4.0 changed the format of counter metrics
# (cf https://github.com/matrix-org/synapse/issues/4001) # (cf https://github.com/matrix-org/synapse/issues/4001)
"prometheus_client>=0.0.18,<0.4.0", "prometheus_client>=0.0.18,<0.4.0",
@@ -80,7 +80,7 @@ REQUIREMENTS = [
] ]
CONDITIONAL_REQUIREMENTS = { CONDITIONAL_REQUIREMENTS = {
"email": ["Jinja2>=2.9", "bleach>=1.4.2"], "email": ["Jinja2>=2.9", "bleach>=1.4.3"],
"matrix-synapse-ldap3": ["matrix-synapse-ldap3>=0.1"], "matrix-synapse-ldap3": ["matrix-synapse-ldap3>=0.1"],
# we use execute_batch, which arrived in psycopg 2.7. # we use execute_batch, which arrived in psycopg 2.7.

View File

@@ -0,0 +1 @@
<html><body>Your account has been successfully renewed.</body><html>

View File

@@ -0,0 +1 @@
<html><body>Invalid renewal token.</body><html>

View File

@@ -1,6 +1,6 @@
<html> <html>
<head></head> <head></head>
<body> <body>
<p>Your password was successfully reset. You may now close this window.</p> <p>Your email has now been validated, please return to your client to reset your password. You may now close this window.</p>
</body> </body>
</html> </html>

View File

@@ -41,6 +41,7 @@ from synapse.rest.client.v2_alpha import (
keys, keys,
notifications, notifications,
openid, openid,
password_policy,
read_marker, read_marker,
receipts, receipts,
register, register,
@@ -116,6 +117,7 @@ class ClientRestResource(JsonResource):
room_upgrade_rest_servlet.register_servlets(hs, client_resource) room_upgrade_rest_servlet.register_servlets(hs, client_resource)
capabilities.register_servlets(hs, client_resource) capabilities.register_servlets(hs, client_resource)
account_validity.register_servlets(hs, client_resource) account_validity.register_servlets(hs, client_resource)
password_policy.register_servlets(hs, client_resource)
relations.register_servlets(hs, client_resource) relations.register_servlets(hs, client_resource)
# moving to /_synapse/admin # moving to /_synapse/admin

View File

@@ -14,12 +14,16 @@
# limitations under the License. # limitations under the License.
""" This module contains REST servlets to do with profile: /profile/<paths> """ """ This module contains REST servlets to do with profile: /profile/<paths> """
import logging
from twisted.internet import defer from twisted.internet import defer
from synapse.http.servlet import RestServlet, parse_json_object_from_request from synapse.http.servlet import RestServlet, parse_json_object_from_request
from synapse.rest.client.v2_alpha._base import client_patterns from synapse.rest.client.v2_alpha._base import client_patterns
from synapse.types import UserID from synapse.types import UserID
logger = logging.getLogger(__name__)
class ProfileDisplaynameRestServlet(RestServlet): class ProfileDisplaynameRestServlet(RestServlet):
PATTERNS = client_patterns("/profile/(?P<user_id>[^/]*)/displayname", v1=True) PATTERNS = client_patterns("/profile/(?P<user_id>[^/]*)/displayname", v1=True)
@@ -28,6 +32,7 @@ class ProfileDisplaynameRestServlet(RestServlet):
super(ProfileDisplaynameRestServlet, self).__init__() super(ProfileDisplaynameRestServlet, self).__init__()
self.hs = hs self.hs = hs
self.profile_handler = hs.get_profile_handler() self.profile_handler = hs.get_profile_handler()
self.http_client = hs.get_simple_http_client()
self.auth = hs.get_auth() self.auth = hs.get_auth()
@defer.inlineCallbacks @defer.inlineCallbacks
@@ -66,11 +71,30 @@ class ProfileDisplaynameRestServlet(RestServlet):
yield self.profile_handler.set_displayname( yield self.profile_handler.set_displayname(
user, requester, new_name, is_admin) user, requester, new_name, is_admin)
if self.hs.config.shadow_server:
shadow_user = UserID(
user.localpart, self.hs.config.shadow_server.get("hs")
)
self.shadow_displayname(shadow_user.to_string(), content)
defer.returnValue((200, {})) defer.returnValue((200, {}))
def on_OPTIONS(self, request, user_id): def on_OPTIONS(self, request, user_id):
return (200, {}) return (200, {})
@defer.inlineCallbacks
def shadow_displayname(self, user_id, body):
# TODO: retries
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
as_token = self.hs.config.shadow_server.get("as_token")
yield self.http_client.put_json(
"%s/_matrix/client/r0/profile/%s/displayname?access_token=%s&user_id=%s" % (
shadow_hs_url, user_id, as_token, user_id
),
body
)
class ProfileAvatarURLRestServlet(RestServlet): class ProfileAvatarURLRestServlet(RestServlet):
PATTERNS = client_patterns("/profile/(?P<user_id>[^/]*)/avatar_url", v1=True) PATTERNS = client_patterns("/profile/(?P<user_id>[^/]*)/avatar_url", v1=True)
@@ -79,6 +103,7 @@ class ProfileAvatarURLRestServlet(RestServlet):
super(ProfileAvatarURLRestServlet, self).__init__() super(ProfileAvatarURLRestServlet, self).__init__()
self.hs = hs self.hs = hs
self.profile_handler = hs.get_profile_handler() self.profile_handler = hs.get_profile_handler()
self.http_client = hs.get_simple_http_client()
self.auth = hs.get_auth() self.auth = hs.get_auth()
@defer.inlineCallbacks @defer.inlineCallbacks
@@ -116,11 +141,30 @@ class ProfileAvatarURLRestServlet(RestServlet):
yield self.profile_handler.set_avatar_url( yield self.profile_handler.set_avatar_url(
user, requester, new_name, is_admin) user, requester, new_name, is_admin)
if self.hs.config.shadow_server:
shadow_user = UserID(
user.localpart, self.hs.config.shadow_server.get("hs")
)
self.shadow_avatar_url(shadow_user.to_string(), content)
defer.returnValue((200, {})) defer.returnValue((200, {}))
def on_OPTIONS(self, request, user_id): def on_OPTIONS(self, request, user_id):
return (200, {}) return (200, {})
@defer.inlineCallbacks
def shadow_avatar_url(self, user_id, body):
# TODO: retries
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
as_token = self.hs.config.shadow_server.get("as_token")
yield self.http_client.put_json(
"%s/_matrix/client/r0/profile/%s/avatar_url?access_token=%s&user_id=%s" % (
shadow_hs_url, user_id, as_token, user_id
),
body
)
class ProfileRestServlet(RestServlet): class ProfileRestServlet(RestServlet):
PATTERNS = client_patterns("/profile/(?P<user_id>[^/]*)", v1=True) PATTERNS = client_patterns("/profile/(?P<user_id>[^/]*)", v1=True)

View File

@@ -320,7 +320,7 @@ class PublicRoomListRestServlet(TransactionRestServlet):
# Option to allow servers to require auth when accessing # Option to allow servers to require auth when accessing
# /publicRooms via CS API. This is especially helpful in private # /publicRooms via CS API. This is especially helpful in private
# federations. # federations.
if self.hs.config.restrict_public_rooms_to_local_users: if not self.hs.config.allow_public_rooms_without_auth:
raise raise
# We allow people to not be authed if they're just looking at our # We allow people to not be authed if they're just looking at our
@@ -704,7 +704,8 @@ class RoomMembershipRestServlet(TransactionRestServlet):
content["address"], content["address"],
content["id_server"], content["id_server"],
requester, requester,
txn_id txn_id,
new_room=False,
) )
defer.returnValue((200, {})) defer.returnValue((200, {}))
return return

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2015, 2016 OpenMarket Ltd # Copyright 2015, 2016 OpenMarket Ltd
# Copyright 2017 Vector Creations Ltd # Copyright 2017 Vector Creations Ltd
# Copyright 2018 New Vector Ltd # Copyright 2018, 2019 New Vector Ltd
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -32,6 +32,7 @@ from synapse.http.servlet import (
parse_json_object_from_request, parse_json_object_from_request,
parse_string, parse_string,
) )
from synapse.types import UserID
from synapse.util.msisdn import phone_number_to_msisdn from synapse.util.msisdn import phone_number_to_msisdn
from synapse.util.stringutils import random_string from synapse.util.stringutils import random_string
from synapse.util.threepids import check_3pid_allowed from synapse.util.threepids import check_3pid_allowed
@@ -85,7 +86,7 @@ class EmailPasswordRequestTokenRestServlet(RestServlet):
if not check_3pid_allowed(self.hs, "email", email): if not check_3pid_allowed(self.hs, "email", email):
raise SynapseError( raise SynapseError(
403, 403,
"Your email domain is not authorized on this server", "Your email is not authorized on this server",
Codes.THREEPID_DENIED, Codes.THREEPID_DENIED,
) )
@@ -208,7 +209,7 @@ class MsisdnPasswordRequestTokenRestServlet(RestServlet):
msisdn = phone_number_to_msisdn(body['country'], body['phone_number']) msisdn = phone_number_to_msisdn(body['country'], body['phone_number'])
if not check_3pid_allowed(self.hs, "msisdn", msisdn): if not (yield check_3pid_allowed(self.hs, "msisdn", msisdn)):
raise SynapseError( raise SynapseError(
403, 403,
"Account phone numbers are not authorized on this server", "Account phone numbers are not authorized on this server",
@@ -228,9 +229,11 @@ class MsisdnPasswordRequestTokenRestServlet(RestServlet):
class PasswordResetSubmitTokenServlet(RestServlet): class PasswordResetSubmitTokenServlet(RestServlet):
"""Handles 3PID validation token submission""" """Handles 3PID validation token submission"""
PATTERNS = [ PATTERNS = client_patterns(
re.compile("^/_synapse/password_reset/(?P<medium>[^/]*)/submit_token/*$"), "/password_reset/(?P<medium>[^/]*)/submit_token/*$",
] releases=(),
unstable=True,
)
def __init__(self, hs): def __init__(self, hs):
""" """
@@ -348,6 +351,7 @@ class PasswordRestServlet(RestServlet):
self.auth_handler = hs.get_auth_handler() self.auth_handler = hs.get_auth_handler()
self.datastore = self.hs.get_datastore() self.datastore = self.hs.get_datastore()
self._set_password_handler = hs.get_set_password_handler() self._set_password_handler = hs.get_set_password_handler()
self.http_client = hs.get_simple_http_client()
@interactive_auth_handler @interactive_auth_handler
@defer.inlineCallbacks @defer.inlineCallbacks
@@ -366,9 +370,13 @@ class PasswordRestServlet(RestServlet):
if self.auth.has_access_token(request): if self.auth.has_access_token(request):
requester = yield self.auth.get_user_by_req(request) requester = yield self.auth.get_user_by_req(request)
params = yield self.auth_handler.validate_user_via_ui_auth( # blindly trust ASes without UI-authing them
requester, body, self.hs.get_ip_from_request(request), if requester.app_service:
) params = body
else:
params = yield self.auth_handler.validate_user_via_ui_auth(
requester, body, self.hs.get_ip_from_request(request),
)
user_id = requester.user.to_string() user_id = requester.user.to_string()
else: else:
requester = None requester = None
@@ -405,11 +413,30 @@ class PasswordRestServlet(RestServlet):
user_id, new_password, requester user_id, new_password, requester
) )
if self.hs.config.shadow_server:
shadow_user = UserID(
requester.user.localpart, self.hs.config.shadow_server.get("hs")
)
self.shadow_password(params, shadow_user.to_string())
defer.returnValue((200, {})) defer.returnValue((200, {}))
def on_OPTIONS(self, _): def on_OPTIONS(self, _):
return 200, {} return 200, {}
@defer.inlineCallbacks
def shadow_password(self, body, user_id):
# TODO: retries
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
as_token = self.hs.config.shadow_server.get("as_token")
yield self.http_client.post_json_get_json(
"%s/_matrix/client/r0/account/password?access_token=%s&user_id=%s" % (
shadow_hs_url, as_token, user_id,
),
body
)
class DeactivateAccountRestServlet(RestServlet): class DeactivateAccountRestServlet(RestServlet):
PATTERNS = client_patterns("/account/deactivate$") PATTERNS = client_patterns("/account/deactivate$")
@@ -476,10 +503,10 @@ class EmailThreepidRequestTokenRestServlet(RestServlet):
['id_server', 'client_secret', 'email', 'send_attempt'], ['id_server', 'client_secret', 'email', 'send_attempt'],
) )
if not check_3pid_allowed(self.hs, "email", body['email']): if not (yield check_3pid_allowed(self.hs, "email", body['email'])):
raise SynapseError( raise SynapseError(
403, 403,
"Your email domain is not authorized on this server", "Your email is not authorized on this server",
Codes.THREEPID_DENIED, Codes.THREEPID_DENIED,
) )
@@ -513,7 +540,7 @@ class MsisdnThreepidRequestTokenRestServlet(RestServlet):
msisdn = phone_number_to_msisdn(body['country'], body['phone_number']) msisdn = phone_number_to_msisdn(body['country'], body['phone_number'])
if not check_3pid_allowed(self.hs, "msisdn", msisdn): if not (yield check_3pid_allowed(self.hs, "msisdn", msisdn)):
raise SynapseError( raise SynapseError(
403, 403,
"Account phone numbers are not authorized on this server", "Account phone numbers are not authorized on this server",
@@ -540,7 +567,8 @@ class ThreepidRestServlet(RestServlet):
self.identity_handler = hs.get_handlers().identity_handler self.identity_handler = hs.get_handlers().identity_handler
self.auth = hs.get_auth() self.auth = hs.get_auth()
self.auth_handler = hs.get_auth_handler() self.auth_handler = hs.get_auth_handler()
self.datastore = self.hs.get_datastore() self.datastore = hs.get_datastore()
self.http_client = hs.get_simple_http_client()
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request): def on_GET(self, request):
@@ -554,27 +582,38 @@ class ThreepidRestServlet(RestServlet):
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request): def on_POST(self, request):
body = parse_json_object_from_request(request) if self.hs.config.disable_3pid_changes:
raise SynapseError(400, "3PID changes disabled on this server")
threePidCreds = body.get('threePidCreds') body = parse_json_object_from_request(request)
threePidCreds = body.get('three_pid_creds', threePidCreds)
if threePidCreds is None:
raise SynapseError(400, "Missing param", Codes.MISSING_PARAM)
requester = yield self.auth.get_user_by_req(request) requester = yield self.auth.get_user_by_req(request)
user_id = requester.user.to_string() user_id = requester.user.to_string()
threepid = yield self.identity_handler.threepid_from_creds(threePidCreds) # skip validation if this is a shadow 3PID from an AS
if not requester.app_service:
threePidCreds = body.get('threePidCreds')
threePidCreds = body.get('three_pid_creds', threePidCreds)
if threePidCreds is None:
raise SynapseError(400, "Missing param", Codes.MISSING_PARAM)
if not threepid: threepid = yield self.identity_handler.threepid_from_creds(threePidCreds)
raise SynapseError(
400, "Failed to auth 3pid", Codes.THREEPID_AUTH_FAILED
)
for reqd in ['medium', 'address', 'validated_at']: if not threepid:
if reqd not in threepid: raise SynapseError(
logger.warn("Couldn't add 3pid: invalid response from ID server") 400, "Failed to auth 3pid", Codes.THREEPID_AUTH_FAILED
raise SynapseError(500, "Invalid response from ID Server") )
for reqd in ['medium', 'address', 'validated_at']:
if reqd not in threepid:
logger.warn("Couldn't add 3pid: invalid response from ID server")
raise SynapseError(500, "Invalid response from ID Server")
else:
# XXX: ASes pass in a validated threepid directly to bypass the IS.
# This makes the API entirely change shape when we have an AS token;
# it really should be an entirely separate API - perhaps
# /account/3pid/replicate or something.
threepid = body.get('threepid')
yield self.auth_handler.add_threepid( yield self.auth_handler.add_threepid(
user_id, user_id,
@@ -583,7 +622,7 @@ class ThreepidRestServlet(RestServlet):
threepid['validated_at'], threepid['validated_at'],
) )
if 'bind' in body and body['bind']: if not requester.app_service and ('bind' in body and body['bind']):
logger.debug( logger.debug(
"Binding threepid %s to %s", "Binding threepid %s to %s",
threepid, user_id threepid, user_id
@@ -592,19 +631,43 @@ class ThreepidRestServlet(RestServlet):
threePidCreds, user_id threePidCreds, user_id
) )
if self.hs.config.shadow_server:
shadow_user = UserID(
requester.user.localpart, self.hs.config.shadow_server.get("hs")
)
self.shadow_3pid({'threepid': threepid}, shadow_user.to_string())
defer.returnValue((200, {})) defer.returnValue((200, {}))
@defer.inlineCallbacks
def shadow_3pid(self, body, user_id):
# TODO: retries
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
as_token = self.hs.config.shadow_server.get("as_token")
yield self.http_client.post_json_get_json(
"%s/_matrix/client/r0/account/3pid?access_token=%s&user_id=%s" % (
shadow_hs_url, as_token, user_id,
),
body
)
class ThreepidDeleteRestServlet(RestServlet): class ThreepidDeleteRestServlet(RestServlet):
PATTERNS = client_patterns("/account/3pid/delete$") PATTERNS = client_patterns("/account/3pid/delete$")
def __init__(self, hs): def __init__(self, hs):
super(ThreepidDeleteRestServlet, self).__init__() super(ThreepidDeleteRestServlet, self).__init__()
self.hs = hs
self.auth = hs.get_auth() self.auth = hs.get_auth()
self.auth_handler = hs.get_auth_handler() self.auth_handler = hs.get_auth_handler()
self.http_client = hs.get_simple_http_client()
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request): def on_POST(self, request):
if self.hs.config.disable_3pid_changes:
raise SynapseError(400, "3PID changes disabled on this server")
body = parse_json_object_from_request(request) body = parse_json_object_from_request(request)
assert_params_in_dict(body, ['medium', 'address']) assert_params_in_dict(body, ['medium', 'address'])
@@ -622,6 +685,12 @@ class ThreepidDeleteRestServlet(RestServlet):
logger.exception("Failed to remove threepid") logger.exception("Failed to remove threepid")
raise SynapseError(500, "Failed to remove threepid") raise SynapseError(500, "Failed to remove threepid")
if self.hs.config.shadow_server:
shadow_user = UserID(
requester.user.localpart, self.hs.config.shadow_server.get("hs")
)
self.shadow_3pid_delete(body, shadow_user.to_string())
if ret: if ret:
id_server_unbind_result = "success" id_server_unbind_result = "success"
else: else:
@@ -631,6 +700,78 @@ class ThreepidDeleteRestServlet(RestServlet):
"id_server_unbind_result": id_server_unbind_result, "id_server_unbind_result": id_server_unbind_result,
})) }))
@defer.inlineCallbacks
def shadow_3pid_delete(self, body, user_id):
# TODO: retries
shadow_hs_url = self.hs.config.shadow_server.get("hs_url")
as_token = self.hs.config.shadow_server.get("as_token")
yield self.http_client.post_json_get_json(
"%s/_matrix/client/r0/account/3pid/delete?access_token=%s&user_id=%s" % (
shadow_hs_url, as_token, user_id
),
body
)
class ThreepidLookupRestServlet(RestServlet):
PATTERNS = [re.compile("^/_matrix/client/unstable/account/3pid/lookup$")]
def __init__(self, hs):
super(ThreepidLookupRestServlet, self).__init__()
self.auth = hs.get_auth()
self.identity_handler = hs.get_handlers().identity_handler
@defer.inlineCallbacks
def on_GET(self, request):
"""Proxy a /_matrix/identity/api/v1/lookup request to an identity
server
"""
yield self.auth.get_user_by_req(request)
# Verify query parameters
query_params = request.args
assert_params_in_dict(query_params, [b"medium", b"address", b"id_server"])
# Retrieve needed information from query parameters
medium = parse_string(request, "medium")
address = parse_string(request, "address")
id_server = parse_string(request, "id_server")
# Proxy the request to the identity server. lookup_3pid handles checking
# if the lookup is allowed so we don't need to do it here.
ret = yield self.identity_handler.lookup_3pid(id_server, medium, address)
defer.returnValue((200, ret))
class ThreepidBulkLookupRestServlet(RestServlet):
PATTERNS = [re.compile("^/_matrix/client/unstable/account/3pid/bulk_lookup$")]
def __init__(self, hs):
super(ThreepidBulkLookupRestServlet, self).__init__()
self.auth = hs.get_auth()
self.identity_handler = hs.get_handlers().identity_handler
@defer.inlineCallbacks
def on_POST(self, request):
"""Proxy a /_matrix/identity/api/v1/bulk_lookup request to an identity
server
"""
yield self.auth.get_user_by_req(request)
body = parse_json_object_from_request(request)
assert_params_in_dict(body, ["threepids", "id_server"])
# Proxy the request to the identity server. lookup_3pid handles checking
# if the lookup is allowed so we don't need to do it here.
ret = yield self.identity_handler.bulk_lookup_3pid(
body["id_server"], body["threepids"],
)
defer.returnValue((200, ret))
class WhoamiRestServlet(RestServlet): class WhoamiRestServlet(RestServlet):
PATTERNS = client_patterns("/account/whoami$") PATTERNS = client_patterns("/account/whoami$")
@@ -656,4 +797,6 @@ def register_servlets(hs, http_server):
MsisdnThreepidRequestTokenRestServlet(hs).register(http_server) MsisdnThreepidRequestTokenRestServlet(hs).register(http_server)
ThreepidRestServlet(hs).register(http_server) ThreepidRestServlet(hs).register(http_server)
ThreepidDeleteRestServlet(hs).register(http_server) ThreepidDeleteRestServlet(hs).register(http_server)
ThreepidLookupRestServlet(hs).register(http_server)
ThreepidBulkLookupRestServlet(hs).register(http_server)
WhoamiRestServlet(hs).register(http_server) WhoamiRestServlet(hs).register(http_server)

View File

@@ -19,6 +19,7 @@ from twisted.internet import defer
from synapse.api.errors import AuthError, NotFoundError, SynapseError from synapse.api.errors import AuthError, NotFoundError, SynapseError
from synapse.http.servlet import RestServlet, parse_json_object_from_request from synapse.http.servlet import RestServlet, parse_json_object_from_request
from synapse.types import UserID
from ._base import client_patterns from ._base import client_patterns
@@ -39,6 +40,7 @@ class AccountDataServlet(RestServlet):
self.auth = hs.get_auth() self.auth = hs.get_auth()
self.store = hs.get_datastore() self.store = hs.get_datastore()
self.notifier = hs.get_notifier() self.notifier = hs.get_notifier()
self._profile_handler = hs.get_profile_handler()
@defer.inlineCallbacks @defer.inlineCallbacks
def on_PUT(self, request, user_id, account_data_type): def on_PUT(self, request, user_id, account_data_type):
@@ -48,6 +50,11 @@ class AccountDataServlet(RestServlet):
body = parse_json_object_from_request(request) body = parse_json_object_from_request(request)
if account_data_type == "im.vector.hide_profile":
user = UserID.from_string(user_id)
hide_profile = body.get('hide_profile')
yield self._profile_handler.set_active(user, not hide_profile, True)
max_id = yield self.store.add_account_data_for_user( max_id = yield self.store.add_account_data_for_user(
user_id, account_data_type, body user_id, account_data_type, body
) )

View File

@@ -15,6 +15,8 @@
import logging import logging
from six import ensure_binary
from twisted.internet import defer from twisted.internet import defer
from synapse.api.errors import AuthError, SynapseError from synapse.api.errors import AuthError, SynapseError
@@ -40,6 +42,8 @@ class AccountValidityRenewServlet(RestServlet):
self.hs = hs self.hs = hs
self.account_activity_handler = hs.get_account_validity_handler() self.account_activity_handler = hs.get_account_validity_handler()
self.auth = hs.get_auth() self.auth = hs.get_auth()
self.success_html = hs.config.account_validity.account_renewed_html_content
self.failure_html = hs.config.account_validity.invalid_token_html_content
@defer.inlineCallbacks @defer.inlineCallbacks
def on_GET(self, request): def on_GET(self, request):
@@ -47,14 +51,21 @@ class AccountValidityRenewServlet(RestServlet):
raise SynapseError(400, "Missing renewal token") raise SynapseError(400, "Missing renewal token")
renewal_token = request.args[b"token"][0] renewal_token = request.args[b"token"][0]
yield self.account_activity_handler.renew_account(renewal_token.decode('utf8')) token_valid = yield self.account_activity_handler.renew_account(
renewal_token.decode("utf8")
)
request.setResponseCode(200) if token_valid:
status_code = 200
response = self.success_html
else:
status_code = 404
response = self.failure_html
request.setResponseCode(status_code)
request.setHeader(b"Content-Type", b"text/html; charset=utf-8") request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
request.setHeader(b"Content-Length", b"%d" % ( request.setHeader(b"Content-Length", b"%d" % (len(response),))
len(AccountValidityRenewServlet.SUCCESS_HTML), request.write(ensure_binary(response))
))
request.write(AccountValidityRenewServlet.SUCCESS_HTML)
finish_request(request) finish_request(request)
defer.returnValue(None) defer.returnValue(None)
@@ -79,7 +90,7 @@ class AccountValiditySendMailServlet(RestServlet):
if not self.account_validity.renew_by_email_enabled: if not self.account_validity.renew_by_email_enabled:
raise AuthError(403, "Account renewal via email is disabled on this server.") raise AuthError(403, "Account renewal via email is disabled on this server.")
requester = yield self.auth.get_user_by_req(request) requester = yield self.auth.get_user_by_req(request, allow_expired=True)
user_id = requester.user.to_string() user_id = requester.user.to_string()
yield self.account_activity_handler.send_renewal_email_to_user(user_id) yield self.account_activity_handler.send_renewal_email_to_user(user_id)

View 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_patterns
logger = logging.getLogger(__name__)
class PasswordPolicyServlet(RestServlet):
PATTERNS = client_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)

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2015 - 2016 OpenMarket Ltd # Copyright 2015-2016 OpenMarket Ltd
# Copyright 2017 Vector Creations 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"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -16,7 +17,9 @@
import hmac import hmac
import logging import logging
import re
from hashlib import sha1 from hashlib import sha1
from string import capwords
from six import string_types from six import string_types
@@ -79,10 +82,10 @@ class EmailRegisterRequestTokenRestServlet(RestServlet):
'id_server', 'client_secret', 'email', 'send_attempt' 'id_server', 'client_secret', 'email', 'send_attempt'
]) ])
if not check_3pid_allowed(self.hs, "email", body['email']): if not (yield check_3pid_allowed(self.hs, "email", body['email'])):
raise SynapseError( raise SynapseError(
403, 403,
"Your email domain is not authorized to register on this server", "Your email is not authorized to register on this server",
Codes.THREEPID_DENIED, Codes.THREEPID_DENIED,
) )
@@ -121,7 +124,7 @@ class MsisdnRegisterRequestTokenRestServlet(RestServlet):
msisdn = phone_number_to_msisdn(body['country'], body['phone_number']) msisdn = phone_number_to_msisdn(body['country'], body['phone_number'])
if not check_3pid_allowed(self.hs, "msisdn", msisdn): if not (yield check_3pid_allowed(self.hs, "msisdn", msisdn)):
raise SynapseError( raise SynapseError(
403, 403,
"Phone numbers are not authorized to register on this server", "Phone numbers are not authorized to register on this server",
@@ -200,6 +203,7 @@ class RegisterRestServlet(RestServlet):
self.room_member_handler = hs.get_room_member_handler() self.room_member_handler = hs.get_room_member_handler()
self.macaroon_gen = hs.get_macaroon_generator() self.macaroon_gen = hs.get_macaroon_generator()
self.ratelimiter = hs.get_registration_ratelimiter() self.ratelimiter = hs.get_registration_ratelimiter()
self.password_policy_handler = hs.get_password_policy_handler()
self.clock = hs.get_clock() self.clock = hs.get_clock()
@interactive_auth_handler @interactive_auth_handler
@@ -243,6 +247,7 @@ class RegisterRestServlet(RestServlet):
if (not isinstance(body['password'], string_types) or if (not isinstance(body['password'], string_types) or
len(body['password']) > 512): len(body['password']) > 512):
raise SynapseError(400, "Invalid password") raise SynapseError(400, "Invalid password")
self.password_policy_handler.validate_password(body['password'])
desired_password = body["password"] desired_password = body["password"]
desired_username = None desired_username = None
@@ -252,6 +257,8 @@ class RegisterRestServlet(RestServlet):
raise SynapseError(400, "Invalid username") raise SynapseError(400, "Invalid username")
desired_username = body['username'] desired_username = body['username']
desired_display_name = body.get('display_name')
appservice = None appservice = None
if self.auth.has_access_token(request): if self.auth.has_access_token(request):
appservice = yield self.auth.get_appservice_by_req(request) appservice = yield self.auth.get_appservice_by_req(request)
@@ -275,7 +282,8 @@ class RegisterRestServlet(RestServlet):
if isinstance(desired_username, string_types): if isinstance(desired_username, string_types):
result = yield self._do_appservice_registration( result = yield self._do_appservice_registration(
desired_username, access_token, body desired_username, desired_password, desired_display_name,
access_token, body
) )
defer.returnValue((200, result)) # we throw for non 200 responses defer.returnValue((200, result)) # we throw for non 200 responses
return return
@@ -413,7 +421,7 @@ class RegisterRestServlet(RestServlet):
medium = auth_result[login_type]['medium'] medium = auth_result[login_type]['medium']
address = auth_result[login_type]['address'] address = auth_result[login_type]['address']
if not check_3pid_allowed(self.hs, medium, address): if not (yield check_3pid_allowed(self.hs, medium, address)):
raise SynapseError( raise SynapseError(
403, 403,
"Third party identifiers (email/phone numbers)" + "Third party identifiers (email/phone numbers)" +
@@ -421,6 +429,95 @@ class RegisterRestServlet(RestServlet):
Codes.THREEPID_DENIED, Codes.THREEPID_DENIED,
) )
existingUid = yield self.store.get_user_id_by_threepid(
medium, address,
)
if existingUid is not None:
raise SynapseError(
400,
"%s is already in use" % medium,
Codes.THREEPID_IN_USE,
)
if self.hs.config.register_mxid_from_3pid:
# override the desired_username based on the 3PID if any.
# reset it first to avoid folks picking their own username.
desired_username = None
# we should have an auth_result at this point if we're going to progress
# to register the user (i.e. we haven't picked up a registered_user_id
# from our session store), in which case get ready and gen the
# desired_username
if auth_result:
if (
self.hs.config.register_mxid_from_3pid == 'email' and
LoginType.EMAIL_IDENTITY in auth_result
):
address = auth_result[LoginType.EMAIL_IDENTITY]['address']
desired_username = synapse.types.strip_invalid_mxid_characters(
address.replace('@', '-').lower()
)
# find a unique mxid for the account, suffixing numbers
# if needed
while True:
try:
yield self.registration_handler.check_username(
desired_username,
guest_access_token=guest_access_token,
assigned_user_id=registered_user_id,
)
# if we got this far we passed the check.
break
except SynapseError as e:
if e.errcode == Codes.USER_IN_USE:
m = re.match(r'^(.*?)(\d+)$', desired_username)
if m:
desired_username = m.group(1) + str(
int(m.group(2)) + 1
)
else:
desired_username += "1"
else:
# something else went wrong.
break
if self.hs.config.register_just_use_email_for_display_name:
desired_display_name = address
else:
# XXX: a nasty heuristic to turn an email address into
# a displayname, as part of register_mxid_from_3pid
parts = address.replace('.', ' ').split('@')
org_parts = parts[1].split(' ')
if org_parts[-2] == "matrix" and org_parts[-1] == "org":
org = "Tchap Admin"
elif org_parts[-2] == "gouv" and org_parts[-1] == "fr":
org = org_parts[-3] if len(org_parts) > 2 else org_parts[-2]
else:
org = org_parts[-2]
desired_display_name = (
capwords(parts[0]) + " [" + capwords(org) + "]"
)
elif (
self.hs.config.register_mxid_from_3pid == 'msisdn' and
LoginType.MSISDN in auth_result
):
desired_username = auth_result[LoginType.MSISDN]['address']
else:
raise SynapseError(
400, "Cannot derive mxid from 3pid; no recognised 3pid"
)
if desired_username is not None:
yield self.registration_handler.check_username(
desired_username,
guest_access_token=guest_access_token,
assigned_user_id=registered_user_id,
)
if registered_user_id is not None: if registered_user_id is not None:
logger.info( logger.info(
"Already registered user ID %r for this session", "Already registered user ID %r for this session",
@@ -432,9 +529,16 @@ class RegisterRestServlet(RestServlet):
# NB: This may be from the auth handler and NOT from the POST # NB: This may be from the auth handler and NOT from the POST
assert_params_in_dict(params, ["password"]) assert_params_in_dict(params, ["password"])
desired_username = params.get("username", None) if not self.hs.config.register_mxid_from_3pid:
desired_username = params.get("username", None)
else:
# we keep the original desired_username derived from the 3pid above
pass
guest_access_token = params.get("guest_access_token", None) guest_access_token = params.get("guest_access_token", None)
new_password = params.get("password", None)
# XXX: don't we need to validate these for length etc like we did on
# the ones from the JSON body earlier on in the method?
if desired_username is not None: if desired_username is not None:
desired_username = desired_username.lower() desired_username = desired_username.lower()
@@ -467,9 +571,10 @@ class RegisterRestServlet(RestServlet):
(registered_user_id, _) = yield self.registration_handler.register( (registered_user_id, _) = yield self.registration_handler.register(
localpart=desired_username, localpart=desired_username,
password=new_password, password=params.get("password", None),
guest_access_token=guest_access_token, guest_access_token=guest_access_token,
generate_token=False, generate_token=False,
default_display_name=desired_display_name,
threepid=threepid, threepid=threepid,
address=client_addr, address=client_addr,
) )
@@ -481,6 +586,14 @@ class RegisterRestServlet(RestServlet):
): ):
yield self.store.upsert_monthly_active_user(registered_user_id) yield self.store.upsert_monthly_active_user(registered_user_id)
if self.hs.config.shadow_server:
yield self.registration_handler.shadow_register(
localpart=desired_username,
display_name=desired_display_name,
auth_result=auth_result,
params=params,
)
# remember that we've now registered that user account, and with # remember that we've now registered that user account, and with
# what user ID (since the user may not have specified) # what user ID (since the user may not have specified)
self.auth_handler.set_session_data( self.auth_handler.set_session_data(
@@ -508,11 +621,33 @@ class RegisterRestServlet(RestServlet):
return 200, {} return 200, {}
@defer.inlineCallbacks @defer.inlineCallbacks
def _do_appservice_registration(self, username, as_token, body): def _do_appservice_registration(
self, username, password, display_name, as_token, body
):
# FIXME: appservice_register() is horribly duplicated with register()
# and they should probably just be combined together with a config flag.
user_id = yield self.registration_handler.appservice_register( user_id = yield self.registration_handler.appservice_register(
username, as_token username, as_token, password, display_name
) )
defer.returnValue((yield self._create_registration_details(user_id, body))) result = yield self._create_registration_details(user_id, body)
auth_result = body.get('auth_result')
if auth_result and LoginType.EMAIL_IDENTITY in auth_result:
threepid = auth_result[LoginType.EMAIL_IDENTITY]
yield self._register_email_threepid(
user_id, threepid, result["access_token"],
body.get("bind_email")
)
if auth_result and LoginType.MSISDN in auth_result:
threepid = auth_result[LoginType.MSISDN]
yield self._register_msisdn_threepid(
user_id, threepid, result["access_token"],
body.get("bind_msisdn")
)
defer.returnValue(result)
@defer.inlineCallbacks @defer.inlineCallbacks
def _do_shared_secret_registration(self, username, password, body): def _do_shared_secret_registration(self, username, password, body):

View File

@@ -15,6 +15,8 @@
import logging import logging
from signedjson.sign import sign_json
from twisted.internet import defer from twisted.internet import defer
from synapse.api.errors import SynapseError from synapse.api.errors import SynapseError
@@ -37,6 +39,7 @@ class UserDirectorySearchRestServlet(RestServlet):
self.hs = hs self.hs = hs
self.auth = hs.get_auth() self.auth = hs.get_auth()
self.user_directory_handler = hs.get_user_directory_handler() self.user_directory_handler = hs.get_user_directory_handler()
self.http_client = hs.get_simple_http_client()
@defer.inlineCallbacks @defer.inlineCallbacks
def on_POST(self, request): def on_POST(self, request):
@@ -67,6 +70,14 @@ class UserDirectorySearchRestServlet(RestServlet):
body = parse_json_object_from_request(request) body = parse_json_object_from_request(request)
if self.hs.config.user_directory_defer_to_id_server:
signed_body = sign_json(body, self.hs.hostname, self.hs.config.signing_key[0])
url = "%s/_matrix/identity/api/v1/user_directory/search" % (
self.hs.config.user_directory_defer_to_id_server,
)
resp = yield self.http_client.post_json_get_json(url, signed_body)
defer.returnValue((200, resp))
limit = body.get("limit", 10) limit = body.get("limit", 10)
limit = min(limit, 50) limit = min(limit, 50)

View File

View File

@@ -0,0 +1,172 @@
# -*- coding: utf-8 -*-
# Copyright 2018 New Vector Ltd
#
# 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.config._base import ConfigError
logger = logging.getLogger(__name__)
class DomainRuleChecker(object):
"""
A re-implementation of the SpamChecker that prevents users in one domain from
inviting users in other domains to rooms, based on a configuration.
Takes a config in the format:
spam_checker:
module: "rulecheck.DomainRuleChecker"
config:
domain_mapping:
"inviter_domain": [ "invitee_domain_permitted", "other_domain_permitted" ]
"other_inviter_domain": [ "invitee_domain_permitted" ]
default: False
# Only let local users join rooms if they were explicitly invited.
can_only_join_rooms_with_invite: false
# Only let local users create rooms if they are inviting only one
# other user, and that user matches the rules above.
can_only_create_one_to_one_rooms: false
# Only let local users invite during room creation, regardless of the
# domain mapping rules above.
can_only_invite_during_room_creation: false
# Prevent local users from inviting users from certain domains to
# rooms published in the room directory.
domains_prevented_from_being_invited_to_published_rooms: []
# Allow third party invites
can_invite_by_third_party_id: true
Don't forget to consider if you can invite users from your own domain.
"""
def __init__(self, config):
self.domain_mapping = config["domain_mapping"] or {}
self.default = config["default"]
self.can_only_join_rooms_with_invite = config.get(
"can_only_join_rooms_with_invite", False,
)
self.can_only_create_one_to_one_rooms = config.get(
"can_only_create_one_to_one_rooms", False,
)
self.can_only_invite_during_room_creation = config.get(
"can_only_invite_during_room_creation", False,
)
self.can_invite_by_third_party_id = config.get(
"can_invite_by_third_party_id", True,
)
self.domains_prevented_from_being_invited_to_published_rooms = config.get(
"domains_prevented_from_being_invited_to_published_rooms", [],
)
def check_event_for_spam(self, event):
"""Implements synapse.events.SpamChecker.check_event_for_spam
"""
return False
def user_may_invite(self, inviter_userid, invitee_userid, third_party_invite,
room_id, new_room, published_room=False):
"""Implements synapse.events.SpamChecker.user_may_invite
"""
if self.can_only_invite_during_room_creation and not new_room:
return False
if not self.can_invite_by_third_party_id and third_party_invite:
return False
# This is a third party invite (without a bound mxid), so unless we have
# banned all third party invites (above) we allow it.
if not invitee_userid:
return True
inviter_domain = self._get_domain_from_id(inviter_userid)
invitee_domain = self._get_domain_from_id(invitee_userid)
if inviter_domain not in self.domain_mapping:
return self.default
if (
published_room and
invitee_domain in self.domains_prevented_from_being_invited_to_published_rooms
):
return False
return invitee_domain in self.domain_mapping[inviter_domain]
def user_may_create_room(self, userid, invite_list, third_party_invite_list,
cloning):
"""Implements synapse.events.SpamChecker.user_may_create_room
"""
if cloning:
return True
if not self.can_invite_by_third_party_id and third_party_invite_list:
return False
number_of_invites = len(invite_list) + len(third_party_invite_list)
if self.can_only_create_one_to_one_rooms and number_of_invites != 1:
return False
return True
def user_may_create_room_alias(self, userid, room_alias):
"""Implements synapse.events.SpamChecker.user_may_create_room_alias
"""
return True
def user_may_publish_room(self, userid, room_id):
"""Implements synapse.events.SpamChecker.user_may_publish_room
"""
return True
def user_may_join_room(self, userid, room_id, is_invited):
"""Implements synapse.events.SpamChecker.user_may_join_room
"""
if self.can_only_join_rooms_with_invite and not is_invited:
return False
return True
@staticmethod
def parse_config(config):
"""Implements synapse.events.SpamChecker.parse_config
"""
if "default" in config:
return config
else:
raise ConfigError("No default set for spam_config DomainRuleChecker")
@staticmethod
def _get_domain_from_id(mxid):
"""Parses a string and returns the domain part of the mxid.
Args:
mxid (str): a valid mxid
Returns:
str: the domain part of the mxid
"""
idx = mxid.find(":")
if idx == -1:
raise Exception("Invalid ID: %r" % (mxid,))
return mxid[idx + 1:]

View File

@@ -65,6 +65,7 @@ from synapse.handlers.groups_local import GroupsLocalHandler
from synapse.handlers.initial_sync import InitialSyncHandler from synapse.handlers.initial_sync import InitialSyncHandler
from synapse.handlers.message import EventCreationHandler, MessageHandler from synapse.handlers.message import EventCreationHandler, MessageHandler
from synapse.handlers.pagination import PaginationHandler from synapse.handlers.pagination import PaginationHandler
from synapse.handlers.password_policy import PasswordPolicyHandler
from synapse.handlers.presence import PresenceHandler from synapse.handlers.presence import PresenceHandler
from synapse.handlers.profile import BaseProfileHandler, MasterProfileHandler from synapse.handlers.profile import BaseProfileHandler, MasterProfileHandler
from synapse.handlers.read_marker import ReadMarkerHandler from synapse.handlers.read_marker import ReadMarkerHandler
@@ -193,6 +194,7 @@ class HomeServer(object):
'registration_handler', 'registration_handler',
'account_validity_handler', 'account_validity_handler',
'event_client_serializer', 'event_client_serializer',
'password_policy_handler',
] ]
REQUIRED_ON_MASTER_STARTUP = [ REQUIRED_ON_MASTER_STARTUP = [
@@ -529,6 +531,9 @@ class HomeServer(object):
def build_event_client_serializer(self): def build_event_client_serializer(self):
return EventClientSerializer(self) return EventClientSerializer(self)
def build_password_policy_handler(self):
return PasswordPolicyHandler(self)
def remove_pusher(self, app_id, push_key, user_id): def remove_pusher(self, app_id, push_key, user_id):
return self.get_pusherpool().remove_pusher(app_id, push_key, user_id) return self.get_pusherpool().remove_pusher(app_id, push_key, user_id)

View File

@@ -299,12 +299,12 @@ class SQLBaseStore(object):
def select_users_with_no_expiration_date_txn(txn): def select_users_with_no_expiration_date_txn(txn):
"""Retrieves the list of registered users with no expiration date from the """Retrieves the list of registered users with no expiration date from the
database. database, filtering out deactivated users.
""" """
sql = ( sql = (
"SELECT users.name FROM users" "SELECT users.name FROM users"
" LEFT JOIN account_validity ON (users.name = account_validity.user_id)" " LEFT JOIN account_validity ON (users.name = account_validity.user_id)"
" WHERE account_validity.user_id is NULL;" " WHERE account_validity.user_id is NULL AND users.deactivated = 0;"
) )
txn.execute(sql, []) txn.execute(sql, [])

View File

@@ -35,7 +35,7 @@ def _make_exclusive_regex(services_cache):
exclusive_user_regexes = [ exclusive_user_regexes = [
regex.pattern regex.pattern
for service in services_cache for service in services_cache
for regex in service.get_exlusive_user_regexes() for regex in service.get_exclusive_user_regexes()
] ]
if exclusive_user_regexes: if exclusive_user_regexes:
exclusive_user_regex = "|".join("(" + r + ")" for r in exclusive_user_regexes) exclusive_user_regex = "|".join("(" + r + ")" for r in exclusive_user_regexes)

View File

@@ -1405,6 +1405,9 @@ class EventsStore(
elif event.type == EventTypes.GuestAccess: elif event.type == EventTypes.GuestAccess:
# Insert into the event_search table. # Insert into the event_search table.
self._store_guest_access_txn(txn, event) self._store_guest_access_txn(txn, event)
elif event.type == EventTypes.Retention:
# Update the room_retention table.
self._store_retention_policy_for_room_txn(txn, event)
self._handle_event_relations(txn, event) self._handle_event_relations(txn, event)

View File

@@ -255,6 +255,26 @@ class EventsWorkerStore(SQLBaseStore):
# didn't have the redacted event at the time, so we recheck on read # didn't have the redacted event at the time, so we recheck on read
# instead. # instead.
if not allow_rejected and entry.event.type == EventTypes.Redaction: if not allow_rejected and entry.event.type == EventTypes.Redaction:
orig_event_info = yield self._simple_select_one(
table="events",
keyvalues={"event_id": entry.event.redacts},
retcols=["sender", "room_id", "type"],
allow_none=True,
)
if not orig_event_info:
# We don't have the event that is being redacted, so we
# assume that the event isn't authorized for now. (If we
# later receive the event, then we will always redact
# it anyway, since we have this redaction)
continue
if orig_event_info["room_id"] != entry.event.room_id:
# Don't process redactions if the redacted event doesn't belong to the
# redaction's room.
logger.info("Ignoring redation in another room.")
continue
if entry.event.internal_metadata.need_to_check_redaction(): if entry.event.internal_metadata.need_to_check_redaction():
# XXX: we need to avoid calling get_event here. # XXX: we need to avoid calling get_event here.
# #
@@ -277,27 +297,13 @@ class EventsWorkerStore(SQLBaseStore):
# 2. have _get_event_from_row just call the first half of # 2. have _get_event_from_row just call the first half of
# that # that
orig_sender = yield self._simple_select_one_onecol(
table="events",
keyvalues={"event_id": entry.event.redacts},
retcol="sender",
allow_none=True,
)
expected_domain = get_domain_from_id(entry.event.sender) expected_domain = get_domain_from_id(entry.event.sender)
if ( if (
orig_sender get_domain_from_id(orig_event_info["sender"]) == expected_domain
and get_domain_from_id(orig_sender) == expected_domain
): ):
# This redaction event is allowed. Mark as not needing a # This redaction event is allowed. Mark as not needing a
# recheck. # recheck.
entry.event.internal_metadata.recheck_redaction = False entry.event.internal_metadata.recheck_redaction = False
else:
# We don't have the event that is being redacted, so we
# assume that the event isn't authorized for now. (If we
# later receive the event, then we will always redact
# it anyway, since we have this redaction)
continue
if allow_rejected or not entry.event.rejected_reason: if allow_rejected or not entry.event.rejected_reason:
if check_redacted and entry.redacted_event: if check_redacted and entry.redacted_event:
@@ -532,7 +538,7 @@ class EventsWorkerStore(SQLBaseStore):
) )
redacted_event = None redacted_event = None
if redacted: if redacted and original_ev.type != EventTypes.Redaction:
redacted_event = prune_event(original_ev) redacted_event = prune_event(original_ev)
redaction_id = yield self._simple_select_one_onecol( redaction_id = yield self._simple_select_one_onecol(
@@ -564,9 +570,18 @@ class EventsWorkerStore(SQLBaseStore):
# recheck. # recheck.
because.internal_metadata.recheck_redaction = False because.internal_metadata.recheck_redaction = False
else: else:
# Senders don't match, so the event isn't actually redacted # Senders don't match, so the event isn't actually
# redacted
redacted_event = None redacted_event = None
if because.room_id != original_ev.room_id:
redacted_event = None
else:
# The lack of a redaction likely means that the redaction is invalid
# and therefore not returned by get_event, so it should be safe to
# just ignore it here.
redacted_event = None
cache_entry = _EventCacheEntry( cache_entry = _EventCacheEntry(
event=original_ev, redacted_event=redacted_event event=original_ev, redacted_event=redacted_event
) )

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright 2014-2016 OpenMarket Ltd # Copyright 2014-2016 OpenMarket Ltd
# Copyright 2018 New Vector Ltd
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -18,8 +19,11 @@ from twisted.internet import defer
from synapse.api.errors import StoreError from synapse.api.errors import StoreError
from synapse.storage.roommember import ProfileInfo from synapse.storage.roommember import ProfileInfo
from . import background_updates
from ._base import SQLBaseStore from ._base import SQLBaseStore
BATCH_SIZE = 100
class ProfileWorkerStore(SQLBaseStore): class ProfileWorkerStore(SQLBaseStore):
@defer.inlineCallbacks @defer.inlineCallbacks
@@ -61,6 +65,55 @@ class ProfileWorkerStore(SQLBaseStore):
desc="get_profile_avatar_url", desc="get_profile_avatar_url",
) )
def get_latest_profile_replication_batch_number(self):
def f(txn):
txn.execute("SELECT MAX(batch) as maxbatch FROM profiles")
rows = self.cursor_to_dict(txn)
return rows[0]['maxbatch']
return self.runInteraction(
"get_latest_profile_replication_batch_number", f,
)
def get_profile_batch(self, batchnum):
return self._simple_select_list(
table="profiles",
keyvalues={
"batch": batchnum,
},
retcols=("user_id", "displayname", "avatar_url", "active"),
desc="get_profile_batch",
)
def assign_profile_batch(self):
def f(txn):
sql = (
"UPDATE profiles SET batch = "
"(SELECT COALESCE(MAX(batch), -1) + 1 FROM profiles) "
"WHERE user_id in ("
" SELECT user_id FROM profiles WHERE batch is NULL limit ?"
")"
)
txn.execute(sql, (BATCH_SIZE,))
return txn.rowcount
return self.runInteraction("assign_profile_batch", f)
def get_replication_hosts(self):
def f(txn):
txn.execute("SELECT host, last_synced_batch FROM profile_replication_status")
rows = self.cursor_to_dict(txn)
return {r['host']: r['last_synced_batch'] for r in rows}
return self.runInteraction("get_replication_hosts", f)
def update_replication_batch_for_host(self, host, last_synced_batch):
return self._simple_upsert(
table="profile_replication_status",
keyvalues={"host": host},
values={
"last_synced_batch": last_synced_batch,
},
desc="update_replication_batch_for_host",
)
def get_from_remote_profile_cache(self, user_id): def get_from_remote_profile_cache(self, user_id):
return self._simple_select_one( return self._simple_select_one(
table="remote_profile_cache", table="remote_profile_cache",
@@ -70,29 +123,62 @@ class ProfileWorkerStore(SQLBaseStore):
desc="get_from_remote_profile_cache", desc="get_from_remote_profile_cache",
) )
def create_profile(self, user_localpart): def set_profile_displayname(self, user_localpart, new_displayname, batchnum):
return self._simple_insert( return self._simple_upsert(
table="profiles", values={"user_id": user_localpart}, desc="create_profile"
)
def set_profile_displayname(self, user_localpart, new_displayname):
return self._simple_update_one(
table="profiles", table="profiles",
keyvalues={"user_id": user_localpart}, keyvalues={"user_id": user_localpart},
updatevalues={"displayname": new_displayname}, values={
"displayname": new_displayname,
"batch": batchnum,
},
desc="set_profile_displayname", desc="set_profile_displayname",
lock=False # we can do this because user_id has a unique index
) )
def set_profile_avatar_url(self, user_localpart, new_avatar_url): def set_profile_avatar_url(self, user_localpart, new_avatar_url, batchnum):
return self._simple_update_one( return self._simple_upsert(
table="profiles", table="profiles",
keyvalues={"user_id": user_localpart}, keyvalues={"user_id": user_localpart},
updatevalues={"avatar_url": new_avatar_url}, values={
"avatar_url": new_avatar_url,
"batch": batchnum,
},
desc="set_profile_avatar_url", desc="set_profile_avatar_url",
lock=False # we can do this because user_id has a unique index
)
def set_profile_active(self, user_localpart, active, hide, batchnum):
values = {
"active": int(active),
"batch": batchnum,
}
if not active and not hide:
# we are deactivating for real (not in hide mode)
# so clear the profile.
values["avatar_url"] = None
values["displayname"] = None
return self._simple_upsert(
table="profiles",
keyvalues={"user_id": user_localpart},
values=values,
desc="set_profile_active",
lock=False # we can do this because user_id has a unique index
) )
class ProfileStore(ProfileWorkerStore): class ProfileStore(ProfileWorkerStore, background_updates.BackgroundUpdateStore):
def __init__(self, db_conn, hs):
super(ProfileStore, self).__init__(db_conn, hs)
self.register_background_index_update(
"profile_replication_status_host_index",
index_name="profile_replication_status_idx",
table="profile_replication_status",
columns=["host"],
unique=True,
)
def add_remote_profile_cache(self, user_id, displayname, avatar_url): def add_remote_profile_cache(self, user_id, displayname, avatar_url):
"""Ensure we are caching the remote user's profiles. """Ensure we are caching the remote user's profiles.

View File

@@ -15,6 +15,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import logging
import re import re
from six import iterkeys from six import iterkeys
@@ -31,6 +32,8 @@ from synapse.util.caches.descriptors import cached, cachedInlineCallbacks
THIRTY_MINUTES_IN_MS = 30 * 60 * 1000 THIRTY_MINUTES_IN_MS = 30 * 60 * 1000
logger = logging.getLogger(__name__)
class RegistrationWorkerStore(SQLBaseStore): class RegistrationWorkerStore(SQLBaseStore):
def __init__(self, db_conn, hs): def __init__(self, db_conn, hs):
@@ -248,6 +251,20 @@ class RegistrationWorkerStore(SQLBaseStore):
desc="set_renewal_mail_status", desc="set_renewal_mail_status",
) )
@defer.inlineCallbacks
def delete_account_validity_for_user(self, user_id):
"""Deletes the entry for the given user in the account validity table, removing
their expiration date and renewal token.
Args:
user_id (str): ID of the user to remove from the account validity table.
"""
yield self._simple_delete_one(
table="account_validity",
keyvalues={"user_id": user_id},
desc="delete_account_validity_for_user",
)
@defer.inlineCallbacks @defer.inlineCallbacks
def is_server_admin(self, user): def is_server_admin(self, user):
res = yield self._simple_select_one_onecol( res = yield self._simple_select_one_onecol(
@@ -598,11 +615,77 @@ class RegistrationStore(
"user_threepids_grandfather", self._bg_user_threepids_grandfather, "user_threepids_grandfather", self._bg_user_threepids_grandfather,
) )
self.register_background_update_handler(
"users_set_deactivated_flag", self._backgroud_update_set_deactivated_flag,
)
# Create a background job for culling expired 3PID validity tokens # Create a background job for culling expired 3PID validity tokens
hs.get_clock().looping_call( hs.get_clock().looping_call(
self.cull_expired_threepid_validation_tokens, THIRTY_MINUTES_IN_MS, self.cull_expired_threepid_validation_tokens, THIRTY_MINUTES_IN_MS,
) )
@defer.inlineCallbacks
def _backgroud_update_set_deactivated_flag(self, progress, batch_size):
"""Retrieves a list of all deactivated users and sets the 'deactivated' flag to 1
for each of them.
"""
last_user = progress.get("user_id", "")
def _backgroud_update_set_deactivated_flag_txn(txn):
txn.execute(
"""
SELECT
users.name,
COUNT(access_tokens.token) AS count_tokens,
COUNT(user_threepids.address) AS count_threepids
FROM users
LEFT JOIN access_tokens ON (access_tokens.user_id = users.name)
LEFT JOIN user_threepids ON (user_threepids.user_id = users.name)
WHERE (users.password_hash IS NULL OR users.password_hash = '')
AND (users.appservice_id IS NULL OR users.appservice_id = '')
AND users.is_guest = 0
AND users.name > ?
GROUP BY users.name
ORDER BY users.name ASC
LIMIT ?;
""",
(last_user, batch_size),
)
rows = self.cursor_to_dict(txn)
if not rows:
return True
rows_processed_nb = 0
for user in rows:
if not user["count_tokens"] and not user["count_threepids"]:
self.set_user_deactivated_status_txn(txn, user["name"], True)
rows_processed_nb += 1
logger.info("Marked %d rows as deactivated", rows_processed_nb)
self._background_update_progress_txn(
txn, "users_set_deactivated_flag", {"user_id": rows[-1]["name"]}
)
if batch_size > len(rows):
return True
else:
return False
end = yield self.runInteraction(
"users_set_deactivated_flag",
_backgroud_update_set_deactivated_flag_txn,
)
if end:
yield self._end_background_update("users_set_deactivated_flag")
defer.returnValue(batch_size)
@defer.inlineCallbacks @defer.inlineCallbacks
def add_access_token_to_user(self, user_id, token, device_id=None): def add_access_token_to_user(self, user_id, token, device_id=None):
"""Adds an access token for the given user. """Adds an access token for the given user.
@@ -1268,3 +1351,50 @@ class RegistrationStore(
"delete_threepid_session", "delete_threepid_session",
delete_threepid_session_txn, delete_threepid_session_txn,
) )
def set_user_deactivated_status_txn(self, txn, user_id, deactivated):
self._simple_update_one_txn(
txn=txn,
table="users",
keyvalues={"name": user_id},
updatevalues={"deactivated": 1 if deactivated else 0},
)
self._invalidate_cache_and_stream(
txn, self.get_user_deactivated_status, (user_id,),
)
@defer.inlineCallbacks
def set_user_deactivated_status(self, user_id, deactivated):
"""Set the `deactivated` property for the provided user to the provided value.
Args:
user_id (str): The ID of the user to set the status for.
deactivated (bool): The value to set for `deactivated`.
"""
yield self.runInteraction(
"set_user_deactivated_status",
self.set_user_deactivated_status_txn,
user_id, deactivated,
)
@cachedInlineCallbacks()
def get_user_deactivated_status(self, user_id):
"""Retrieve the value for the `deactivated` property for the provided user.
Args:
user_id (str): The ID of the user to retrieve the status for.
Returns:
defer.Deferred(bool): The requested value.
"""
res = yield self._simple_select_one_onecol(
table="users",
keyvalues={"name": user_id},
retcol="deactivated",
desc="get_user_deactivated_status",
)
# Convert the integer into a boolean.
defer.returnValue(res == 1)

View File

@@ -17,10 +17,13 @@ import collections
import logging import logging
import re import re
from six import integer_types
from canonicaljson import json from canonicaljson import json
from twisted.internet import defer from twisted.internet import defer
from synapse.api.constants import EventTypes
from synapse.api.errors import StoreError from synapse.api.errors import StoreError
from synapse.storage._base import SQLBaseStore from synapse.storage._base import SQLBaseStore
from synapse.storage.search import SearchStore from synapse.storage.search import SearchStore
@@ -171,6 +174,24 @@ class RoomWorkerStore(SQLBaseStore):
desc="is_room_blocked", desc="is_room_blocked",
) )
@defer.inlineCallbacks
def is_room_published(self, room_id):
"""Check whether a room has been published in the local public room
directory.
Args:
room_id (str)
Returns:
bool: Whether the room is currently published in the room directory
"""
# Get room information
room_info = yield self.get_room(room_id)
if not room_info:
defer.returnValue(False)
# Check the is_public value
defer.returnValue(room_info.get("is_public", False))
@cachedInlineCallbacks(max_entries=10000) @cachedInlineCallbacks(max_entries=10000)
def get_ratelimit_for_user(self, user_id): def get_ratelimit_for_user(self, user_id):
"""Check if there are any overrides for ratelimiting for the given """Check if there are any overrides for ratelimiting for the given
@@ -204,6 +225,85 @@ class RoomWorkerStore(SQLBaseStore):
class RoomStore(RoomWorkerStore, SearchStore): class RoomStore(RoomWorkerStore, SearchStore):
def __init__(self, db_conn, hs):
super(RoomStore, self).__init__(db_conn, hs)
self.config = hs.config
self.register_background_update_handler(
"insert_room_retention", self._background_insert_retention,
)
@defer.inlineCallbacks
def _background_insert_retention(self, progress, batch_size):
"""Retrieves a list of all rooms within a range and inserts an entry for each of
them into the room_retention table.
NULLs the property's columns if missing from the retention event in the room's
state (or NULLs all of them if there's no retention event in the room's state),
so that we fall back to the server's retention policy.
"""
last_room = progress.get("room_id", "")
def _background_insert_retention_txn(txn):
txn.execute(
"""
SELECT state.room_id, state.event_id, events.json
FROM current_state_events as state
LEFT JOIN event_json AS events ON (state.event_id = events.event_id)
WHERE state.room_id > ? AND state.type = '%s'
ORDER BY state.room_id ASC
LIMIT ?;
""" % EventTypes.Retention,
(last_room, batch_size)
)
rows = self.cursor_to_dict(txn)
if not rows:
return True
for row in rows:
if not row["json"]:
retention_policy = {}
else:
ev = json.loads(row["json"])
retention_policy = json.dumps(ev["content"])
self._simple_insert_txn(
txn=txn,
table="room_retention",
values={
"room_id": row["room_id"],
"event_id": row["event_id"],
"min_lifetime": retention_policy.get("min_lifetime"),
"max_lifetime": retention_policy.get("max_lifetime"),
}
)
logger.info("Inserted %d rows into room_retention", len(rows))
self._background_update_progress_txn(
txn, "insert_room_retention", {
"room_id": rows[-1]["room_id"],
}
)
if batch_size > len(rows):
return True
else:
return False
end = yield self.runInteraction(
"insert_room_retention",
_background_insert_retention_txn,
)
if end:
yield self._end_background_update("insert_room_retention")
defer.returnValue(batch_size)
@defer.inlineCallbacks @defer.inlineCallbacks
def store_room(self, room_id, room_creator_user_id, is_public): def store_room(self, room_id, room_creator_user_id, is_public):
"""Stores a room. """Stores a room.
@@ -439,6 +539,37 @@ class RoomStore(RoomWorkerStore, SearchStore):
) )
txn.execute(sql, (event.event_id, event.room_id, event.content[key])) txn.execute(sql, (event.event_id, event.room_id, event.content[key]))
def _store_retention_policy_for_room_txn(self, txn, event):
if (
hasattr(event, "content")
and ("min_lifetime" in event.content or "max_lifetime" in event.content)
):
if (
("min_lifetime" in event.content and not isinstance(
event.content.get("min_lifetime"), integer_types
))
or ("max_lifetime" in event.content and not isinstance(
event.content.get("max_lifetime"), integer_types
))
):
# Ignore the event if one of the value isn't an integer.
return
self._simple_insert_txn(
txn=txn,
table="room_retention",
values={
"room_id": event.room_id,
"event_id": event.event_id,
"min_lifetime": event.content.get("min_lifetime"),
"max_lifetime": event.content.get("max_lifetime"),
},
)
self._invalidate_cache_and_stream(
txn, self.get_retention_policy_for_room, (event.room_id,)
)
def add_event_report( def add_event_report(
self, room_id, event_id, user_id, reason, content, received_ts self, room_id, event_id, user_id, reason, content, received_ts
): ):
@@ -620,3 +751,142 @@ class RoomStore(RoomWorkerStore, SearchStore):
remote_media_mxcs.append((hostname, media_id)) remote_media_mxcs.append((hostname, media_id))
return local_media_mxcs, remote_media_mxcs return local_media_mxcs, remote_media_mxcs
@defer.inlineCallbacks
def get_rooms_for_retention_period_in_range(self, min_ms, max_ms, include_null=False):
"""Retrieves all of the rooms within the given retention range.
Optionally includes the rooms which don't have a retention policy.
Args:
min_ms (int|None): Duration in milliseconds that define the lower limit of
the range to handle (exclusive). If None, doesn't set a lower limit.
max_ms (int|None): Duration in milliseconds that define the upper limit of
the range to handle (inclusive). If None, doesn't set an upper limit.
include_null (bool): Whether to include rooms which retention policy is NULL
in the returned set.
Returns:
dict[str, dict]: The rooms within this range, along with their retention
policy. The key is "room_id", and maps to a dict describing the retention
policy associated with this room ID. The keys for this nested dict are
"min_lifetime" (int|None), and "max_lifetime" (int|None).
"""
def get_rooms_for_retention_period_in_range_txn(txn):
range_conditions = []
args = []
if min_ms is not None:
range_conditions.append("max_lifetime > ?")
args.append(min_ms)
if max_ms is not None:
range_conditions.append("max_lifetime <= ?")
args.append(max_ms)
# Do a first query which will retrieve the rooms that have a retention policy
# in their current state.
sql = """
SELECT room_id, min_lifetime, max_lifetime FROM room_retention
INNER JOIN current_state_events USING (event_id, room_id)
"""
if len(range_conditions):
sql += " WHERE (" + " AND ".join(range_conditions) + ")"
if include_null:
sql += " OR max_lifetime IS NULL"
txn.execute(sql, args)
rows = self.cursor_to_dict(txn)
rooms_dict = {}
for row in rows:
rooms_dict[row["room_id"]] = {
"min_lifetime": row["min_lifetime"],
"max_lifetime": row["max_lifetime"],
}
if include_null:
# If required, do a second query that retrieves all of the rooms we know
# of so we can handle rooms with no retention policy.
sql = "SELECT DISTINCT room_id FROM current_state_events"
txn.execute(sql)
rows = self.cursor_to_dict(txn)
# If a room isn't already in the dict (i.e. it doesn't have a retention
# policy in its state), add it with a null policy.
for row in rows:
if row["room_id"] not in rooms_dict:
rooms_dict[row["room_id"]] = {
"min_lifetime": None,
"max_lifetime": None,
}
return rooms_dict
rooms = yield self.runInteraction(
"get_rooms_for_retention_period_in_range",
get_rooms_for_retention_period_in_range_txn,
)
defer.returnValue(rooms)
@cachedInlineCallbacks()
def get_retention_policy_for_room(self, room_id):
"""Get the retention policy for a given room.
If no retention policy has been found for this room, returns a policy defined
by the configured default policy (which has None as both the 'min_lifetime' and
the 'max_lifetime' if no default policy has been defined in the server's
configuration).
Args:
room_id (str): The ID of the room to get the retention policy of.
Returns:
dict[int, int]: "min_lifetime" and "max_lifetime" for this room.
"""
def get_retention_policy_for_room_txn(txn):
txn.execute(
"""
SELECT min_lifetime, max_lifetime FROM room_retention
INNER JOIN current_state_events USING (event_id, room_id)
WHERE room_id = ?;
""",
(room_id,)
)
return self.cursor_to_dict(txn)
ret = yield self.runInteraction(
"get_retention_policy_for_room",
get_retention_policy_for_room_txn,
)
# If we don't know this room ID, ret will be None, in this case return the default
# policy.
if not ret:
defer.returnValue({
"min_lifetime": self.config.retention_default_min_lifetime,
"max_lifetime": self.config.retention_default_max_lifetime,
})
row = ret[0]
# If one of the room's policy's attributes isn't defined, use the matching
# attribute from the default policy.
# The default values will be None if no default policy has been defined, or if one
# of the attributes is missing from the default policy.
if row["min_lifetime"] is None:
row["min_lifetime"] = self.config.retention_default_min_lifetime
if row["max_lifetime"] is None:
row["max_lifetime"] = self.config.retention_default_max_lifetime
defer.returnValue(row)

View File

@@ -0,0 +1,36 @@
/* Copyright 2018 New Vector Ltd
*
* 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.
*/
/*
* Add a batch number to track changes to profiles and the
* order they're made in so we can replicate user profiles
* to other hosts as they change
*/
ALTER TABLE profiles ADD COLUMN batch BIGINT DEFAULT NULL;
/*
* Index on the batch number so we can get profiles
* by their batch
*/
CREATE INDEX profiles_batch_idx ON profiles(batch);
/*
* A table to track what batch of user profiles has been
* synced to what profile replication target.
*/
CREATE TABLE profile_replication_status (
host TEXT NOT NULL,
last_synced_batch BIGINT NOT NULL
);

View File

@@ -0,0 +1,23 @@
/* Copyright 2018 New Vector Ltd
*
* 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 flag saying whether the user owning the profile has been deactivated
* This really belongs on the users table, not here, but the users table
* stores users by their full user_id and profiles stores them by localpart,
* so we can't easily join between the two tables. Plus, the batch number
* realy ought to represent data in this table that has changed.
*/
ALTER TABLE profiles ADD COLUMN active SMALLINT DEFAULT 1 NOT NULL;

View File

@@ -0,0 +1,17 @@
/* Copyright 2019 New Vector Ltd
*
* 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.
*/
INSERT INTO background_updates (update_name, progress_json) VALUES
('profile_replication_status_host_index', '{}');

View File

@@ -0,0 +1,33 @@
/* Copyright 2019 New Vector Ltd
*
* 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.
*/
-- Tracks the retention policy of a room.
-- A NULL max_lifetime or min_lifetime means that the matching property is not defined in
-- the room's retention policy state event.
-- If a room doesn't have a retention policy state event in its state, both max_lifetime
-- and min_lifetime are NULL.
CREATE TABLE IF NOT EXISTS room_retention(
room_id TEXT,
event_id TEXT,
min_lifetime BIGINT,
max_lifetime BIGINT,
PRIMARY KEY(room_id, event_id)
);
CREATE INDEX room_retention_max_lifetime_idx on room_retention(max_lifetime);
INSERT INTO background_updates (update_name, progress_json) VALUES
('insert_room_retention', '{}');

View File

@@ -0,0 +1,19 @@
/* 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.
*/
ALTER TABLE users ADD deactivated SMALLINT DEFAULT 0 NOT NULL;
INSERT INTO background_updates (update_name, progress_json) VALUES
('users_set_deactivated_flag', '{}');

View File

@@ -667,10 +667,19 @@ CREATE TABLE presence_stream (
CREATE TABLE profile_replication_status (
host text NOT NULL,
last_synced_batch bigint NOT NULL
);
CREATE TABLE profiles ( CREATE TABLE profiles (
user_id text NOT NULL, user_id text NOT NULL,
displayname text, displayname text,
avatar_url text avatar_url text,
batch bigint,
active smallint DEFAULT 1 NOT NULL
); );
@@ -1842,6 +1851,10 @@ CREATE INDEX presence_stream_user_id ON presence_stream USING btree (user_id);
CREATE INDEX profiles_batch_idx ON profiles USING btree (batch);
CREATE INDEX public_room_index ON rooms USING btree (is_public); CREATE INDEX public_room_index ON rooms USING btree (is_public);

View File

@@ -6,7 +6,7 @@ CREATE TABLE presence_allow_inbound( observed_user_id TEXT NOT NULL, observer_us
CREATE TABLE users( name TEXT, password_hash TEXT, creation_ts BIGINT, admin SMALLINT DEFAULT 0 NOT NULL, upgrade_ts BIGINT, is_guest SMALLINT DEFAULT 0 NOT NULL, appservice_id TEXT, consent_version TEXT, consent_server_notice_sent TEXT, user_type TEXT DEFAULT NULL, UNIQUE(name) ); CREATE TABLE users( name TEXT, password_hash TEXT, creation_ts BIGINT, admin SMALLINT DEFAULT 0 NOT NULL, upgrade_ts BIGINT, is_guest SMALLINT DEFAULT 0 NOT NULL, appservice_id TEXT, consent_version TEXT, consent_server_notice_sent TEXT, user_type TEXT DEFAULT NULL, UNIQUE(name) );
CREATE TABLE access_tokens( id BIGINT PRIMARY KEY, user_id TEXT NOT NULL, device_id TEXT, token TEXT NOT NULL, last_used BIGINT, UNIQUE(token) ); CREATE TABLE access_tokens( id BIGINT PRIMARY KEY, user_id TEXT NOT NULL, device_id TEXT, token TEXT NOT NULL, last_used BIGINT, UNIQUE(token) );
CREATE TABLE user_ips ( user_id TEXT NOT NULL, access_token TEXT NOT NULL, device_id TEXT, ip TEXT NOT NULL, user_agent TEXT NOT NULL, last_seen BIGINT NOT NULL ); CREATE TABLE user_ips ( user_id TEXT NOT NULL, access_token TEXT NOT NULL, device_id TEXT, ip TEXT NOT NULL, user_agent TEXT NOT NULL, last_seen BIGINT NOT NULL );
CREATE TABLE profiles( user_id TEXT NOT NULL, displayname TEXT, avatar_url TEXT, UNIQUE(user_id) ); CREATE TABLE profiles( user_id TEXT NOT NULL, displayname TEXT, avatar_url TEXT, batch BIGINT DEFAULT NULL, active SMALLINT DEFAULT 1 NOT NULL, UNIQUE(user_id) );
CREATE TABLE received_transactions( transaction_id TEXT, origin TEXT, ts BIGINT, response_code INTEGER, response_json bytea, has_been_referenced smallint default 0, UNIQUE (transaction_id, origin) ); CREATE TABLE received_transactions( transaction_id TEXT, origin TEXT, ts BIGINT, response_code INTEGER, response_json bytea, has_been_referenced smallint default 0, UNIQUE (transaction_id, origin) );
CREATE TABLE destinations( destination TEXT PRIMARY KEY, retry_last_ts BIGINT, retry_interval INTEGER ); CREATE TABLE destinations( destination TEXT PRIMARY KEY, retry_last_ts BIGINT, retry_interval INTEGER );
CREATE TABLE events( stream_ordering INTEGER PRIMARY KEY, topological_ordering BIGINT NOT NULL, event_id TEXT NOT NULL, type TEXT NOT NULL, room_id TEXT NOT NULL, content TEXT, unrecognized_keys TEXT, processed BOOL NOT NULL, outlier BOOL NOT NULL, depth BIGINT DEFAULT 0 NOT NULL, origin_server_ts BIGINT, received_ts BIGINT, sender TEXT, contains_url BOOLEAN, UNIQUE (event_id) ); CREATE TABLE events( stream_ordering INTEGER PRIMARY KEY, topological_ordering BIGINT NOT NULL, event_id TEXT NOT NULL, type TEXT NOT NULL, room_id TEXT NOT NULL, content TEXT, unrecognized_keys TEXT, processed BOOL NOT NULL, outlier BOOL NOT NULL, depth BIGINT DEFAULT 0 NOT NULL, origin_server_ts BIGINT, received_ts BIGINT, sender TEXT, contains_url BOOLEAN, UNIQUE (event_id) );
@@ -208,6 +208,8 @@ CREATE INDEX group_users_u_idx ON group_users(user_id);
CREATE INDEX group_invites_u_idx ON group_invites(user_id); CREATE INDEX group_invites_u_idx ON group_invites(user_id);
CREATE UNIQUE INDEX group_rooms_g_idx ON group_rooms(group_id, room_id); CREATE UNIQUE INDEX group_rooms_g_idx ON group_rooms(group_id, room_id);
CREATE INDEX group_rooms_r_idx ON group_rooms(room_id); CREATE INDEX group_rooms_r_idx ON group_rooms(room_id);
CREATE INDEX profiles_batch_idx ON profiles(batch);
CREATE TABLE profile_replication_status ( host TEXT NOT NULL, last_synced_batch BIGINT NOT NULL );
CREATE TABLE user_daily_visits ( user_id TEXT NOT NULL, device_id TEXT, timestamp BIGINT NOT NULL ); CREATE TABLE user_daily_visits ( user_id TEXT NOT NULL, device_id TEXT, timestamp BIGINT NOT NULL );
CREATE INDEX user_daily_visits_uts_idx ON user_daily_visits(user_id, timestamp); CREATE INDEX user_daily_visits_uts_idx ON user_daily_visits(user_id, timestamp);
CREATE INDEX user_daily_visits_ts_idx ON user_daily_visits(timestamp); CREATE INDEX user_daily_visits_ts_idx ON user_daily_visits(timestamp);

View File

@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Copyright 2019 New Vector Ltd
#
# 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.

Some files were not shown because too many files have changed in this diff Show More