diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a76142..0bf0c89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Snikket Server changelog +## UNRELEASED + +- Increase shared file size limit from 16MB to 100MB +- Allow configurable storage quota for shared files +- Initial support for "limited" user accounts +- Support for group chat notifications on iOS +- Configurable port range for TURN service +- Ability to see basic server metrics in the web admin interface +- Support for advanced monitoring/alerting via Prometheus + +### Upgrading + +If you are using a reverse proxy in front of Snikket, ensure it can +handle the new upload limit (for example, in nginx the `client_max_body_size` +option). + ## beta.20210519 - Allow custom HTTP bind interface diff --git a/ansible/files/prosody.cfg.lua b/ansible/files/prosody.cfg.lua index be40cff..41a1aea 100644 --- a/ansible/files/prosody.cfg.lua +++ b/ansible/files/prosody.cfg.lua @@ -31,6 +31,13 @@ data_path = "/snikket/prosody" pidfile = "/var/run/prosody/prosody.pid" +admin_shell_prompt = ("prosody [%s]> "):format(DOMAIN) + +-- Aggressive GC to reduce resource consumption. These values are not +-- incredibly scientific, but should be good for a small private server. +-- They should be reviewed on the upgrade to Lua 5.4. +gc = { threshold = 100, speed = 750 } + modules_enabled = { -- Generally required @@ -77,8 +84,11 @@ modules_enabled = { "update_notify"; "turncredentials"; "admin_shell"; + "isolate_host"; "snikket_client_id"; "snikket_ios_preserve_push"; + "snikket_restricted_users"; + "lastlog2"; -- Spam/abuse management "spam_reporting"; -- Allow users to report spam/abuse @@ -119,6 +129,7 @@ modules_enabled = { -- Monitoring & maintenance "measure_process"; + "measure_active_users"; } registration_watchers = {} -- Disable by default @@ -142,6 +153,9 @@ registration_invite_only = true -- over what happens when a user invites someone. allow_contact_invites = false +-- Disallow restricted users to create invitations to the server +deny_user_invites_by_roles = { "prosody:restricted" } + invites_page = ENV_SNIKKET_INVITE_URL or ("https://"..DOMAIN.."/invite/{invite.token}/"); invites_page_external = true @@ -205,6 +219,9 @@ if ENV_SNIKKET_TWEAK_TURNSERVER ~= "0" or ENV_SNIKKET_TWEAK_TURNSERVER_DOMAIN th turncredentials_secret = ENV_SNIKKET_TWEAK_TURNSERVER_SECRET or assert(io.open("/snikket/prosody/turn-auth-secret-v2")):read("*l"); end +-- Allow restricted users access to push notification servers +isolate_except_domains = { "push.snikket.net", "push-ios.snikket.net" } + VirtualHost (DOMAIN) authentication = "internal_hashed" @@ -243,11 +260,20 @@ Component ("groups."..DOMAIN) "muc" "vcard_muc"; "muc_defaults"; "muc_offline_delivery"; + "snikket_restricted_users"; + "muc_auto_reserve_nicks"; } restrict_room_creation = "local" muc_local_only = { "general@groups."..DOMAIN } - muc_room_default_persistent = true + + -- Default configuration for rooms (typically overwritten by the client) muc_room_default_allow_member_invites = true + muc_room_default_persistent = true + muc_room_default_public = false + + -- Enable push notifications for offline group members by default + -- (this also requires mod_muc_auto_reserve_nicks in practice) + muc_offline_delivery_default = true default_mucs = { { @@ -272,8 +298,13 @@ Component ("share."..DOMAIN) "http_file_share" http_host = "share."..DOMAIN http_external_url = "https://share."..DOMAIN.."/" end - http_file_share_size_limit = 1024 * 1024 * 100 -- 100MB + + -- 128 bits (i.e. 16 bytes) is the maximum length of a GCM auth tag, which + -- is appended to encrypted uploads according to XEP-0454. This ensures we + -- allow files up to the size limit even if they are encrypted. + http_file_share_size_limit = (1024 * 1024 * 100) + 16 -- 100MB + 16 bytes http_file_share_expire_after = 60 * 60 * 24 * RETENTION_DAYS -- N days + if UPLOAD_STORAGE_GB then http_file_share_global_quota = 1024 * 1024 * 1024 * UPLOAD_STORAGE_GB end diff --git a/ansible/snikket.yml b/ansible/snikket.yml index 2d20a87..e5aa26d 100644 --- a/ansible/snikket.yml +++ b/ansible/snikket.yml @@ -7,9 +7,9 @@ vars: prosody: package: "prosody-trunk" - build: "1521" + build: "1544" prosody_modules: - revision: "8b3e91249cff" + revision: "4abb33a15897" tasks: - import_tasks: tasks/prosody.yml - import_tasks: tasks/supervisor.yml diff --git a/ansible/tasks/prosody.yml b/ansible/tasks/prosody.yml index 76a698b..26e9a79 100644 --- a/ansible/tasks/prosody.yml +++ b/ansible/tasks/prosody.yml @@ -79,6 +79,7 @@ loop: - mod_smacks - mod_cloud_notify + - mod_cloud_notify_extensions - mod_cloud_notify_encrypted - mod_cloud_notify_priority_tag - mod_cloud_notify_filters @@ -86,7 +87,7 @@ - mod_compact_resource - mod_conversejs - mod_migrate_http_upload - - mod_lastlog + - mod_lastlog2 - mod_limit_auth - mod_password_policy - mod_roster_allinall @@ -116,11 +117,15 @@ - mod_groups_muc_bookmarks - mod_muc_defaults - mod_muc_local_only + - mod_muc_offline_delivery - mod_http_host_status_check - mod_measure_process - mod_prometheus - mod_spam_reporting - mod_watch_spam_reports + - mod_isolate_host + - mod_muc_auto_reserve_nicks + - mod_measure_active_users - name: Enable wanted modules (snikket-modules) file: @@ -134,6 +139,7 @@ - mod_invites_bootstrap - mod_snikket_client_id - mod_snikket_ios_preserve_push + - mod_snikket_restricted_users - name: "Install lua-ossl for encrypted push notifications" apt: diff --git a/docs/features/user_roles.md b/docs/features/user_roles.md new file mode 100644 index 0000000..f6ec1e9 --- /dev/null +++ b/docs/features/user_roles.md @@ -0,0 +1,53 @@ +--- +title: User roles +--- + +# User roles + +Snikket allows you to select a role for users, each role granting different +permissions. + +Each user may have one of three roles: + +## Administrator + +This is the default role of the first user (if you're reading this, that's +probably you!). + +Administrators have full control over the server, settings, users and circles. +These features can be accessed primarily through the admin panel in the +Snikket web interface. + +## Normal + +This is the default role for most users. It gives access to all +non-administrative server functionality. + +## Limited + +Limited users have various restrictions. The purpose of this role is to +allow granting someone an account on the server, only for the purposes of +communicating with other people on that server. This can be useful to provide +a guest or child account, for example. + +In particular, limited users are not allowed to: + +- Communicate with users on other servers +- Join group chats on other servers +- Create public channels (including on the current server) +- Invite new users to the server (regardless of whether this is enabled for + normal users). + +### Caveats + +The current support for limited users has some known issues. It is designed to +prevent casual misuse of the server, but it is not intended to be a foolproof +security measure. For example, limited users are still able to *receive* +messages and contact requests from other servers, even though they cannot send +them to other servers. It is expected that we will restrict incoming traffic +for limited users in a future release, after further testing. + +Also note that limited accounts may have issues using non-Snikket mobile apps +that use push notifications, depending on the design of the app. This is +because the restrictions may prevent the app communicating with its' +developer's push notification services over XMPP. diff --git a/docs/setup/troubleshooting.md b/docs/setup/troubleshooting.md index 6ae7eb3..14c0697 100644 --- a/docs/setup/troubleshooting.md +++ b/docs/setup/troubleshooting.md @@ -110,16 +110,38 @@ for more information on correctly configuring reverse proxies. ### Certificate debugging commands -If everything looks okay and you're not sure what the problem could be, -you can get the error message from the debug log: +#### Checking for errors + +If you think you have everything set up correctly and you're not sure what the +problem could be, check the error log: + +``` +docker-compose exec snikket_certs cat /var/log/letsencrypt/errors.log +``` + +If you get a "No such file or directory" error when running the above command, +inspect the debug log instead: ``` docker-compose exec snikket_certs cat /var/log/letsencrypt/letsencrypt.log | grep detail ``` +#### Trying again + Once you have fixed any problems, you can force a new attempt with the following command: +``` +docker-compose exec snikket_certs /etc/cron.daily/certbot +``` + +If that command says that no certificates are due for renewal, but you need to +trigger a renewal anyway, run: + ``` docker-compose exec snikket_certs su letsencrypt -- -c "certbot renew --config-dir /snikket/letsencrypt --cert-path /etc/ssl/certbot --force-renew" ``` + +Note that Let's Encrypt has strict [rate limits](https://letsencrypt.org/docs/rate-limits/) - +do not run these commands more often than necessary, or you may find yourself +unable to get new certificates for a while. diff --git a/snikket-modules/mod_snikket_restricted_users/mod_snikket_restricted_users.lua b/snikket-modules/mod_snikket_restricted_users/mod_snikket_restricted_users.lua new file mode 100644 index 0000000..75c0396 --- /dev/null +++ b/snikket-modules/mod_snikket_restricted_users/mod_snikket_restricted_users.lua @@ -0,0 +1,55 @@ +local jid_bare = require "util.jid".bare; +local um_get_roles = require "core.usermanager".get_roles; + +local function load_main_host(module) + -- Check whether a user should be isolated from remote JIDs + -- If not, set a session flag that allows them to bypass mod_isolate_host + local function check_user_isolated(event) + local session = event.session; + if not session.no_host_isolation then + local bare_jid = jid_bare(session.full_jid); + local roles = um_get_roles(bare_jid, module.host); + if roles == false then return; end + if not roles or not roles["prosody:restricted"] then + -- Bypass isolation for all unrestricted users + session.no_host_isolation = true; + end + end + end + + -- Add low-priority hook to run after the check_user_isolated default + -- behaviour in mod_isolate_host + module:hook("resource-bind", check_user_isolated, -0.5); +end + +local function load_groups_host(module) + local primary_host = module.host:gsub("^%a+%.", ""); + + local function is_restricted(user_jid) + local roles = um_get_roles(user_jid, primary_host); + return not roles or roles["prosody:restricted"]; + end + + module:hook("muc-config-submitted/muc#roomconfig_publicroom", function (event) + if not is_restricted(event.actor) then return; end + -- Don't allow modification of this value by restricted users + return true; + end, 5); + + module:hook("muc-config-form", function (event) + if not is_restricted(event.actor) then return; end -- Don't restrict admins + -- Hide the option from the config form for restricted users + local form = event.form; + for i = #form, 1, -1 do + if form[i].name == "muc#roomconfig_publicroom" then + table.remove(form, i); + end + end + end); +end + +if module:get_host_type() == "component" and module:get_option_string("component_module") == "muc" then + load_groups_host(module); +else + load_main_host(module); +end