Changeset - ef35c565bb0a
[Not reviewed]
0 7 0
Branko Majic (branko) - 10 months ago 2025-02-13 22:37:03
branko@majic.rs
MAR-242: Added role parameters for xmpp_server role to configure HTTP file upload limits (XEP-0363):

- Refactor the daily quota tests to be a bit more flexible.
7 files changed with 52 insertions and 24 deletions:
0 comments (0 inline, 0 general)
docs/rolereference.rst
Show inline comments
 
@@ -807,260 +807,278 @@ Here is an example configuration for setting-up LDAP server:
 
.. code-block:: yaml
 

	
 
  ---
 

	
 
  ldap_server_domain: "example.com"
 
  ldap_server_organization: "Example Corporation"
 
  ldap_server_log_level: 256
 
  ldap_server_tls_certificate: "{{ lookup('file', '~/tls/ldap.example.com_ldap.pem') }}"
 
  ldap_server_tls_key: "{{ lookup('file', '~/tls/ldap.example.com_ldap.key') }}"
 
  ldap_server_ssf: 128
 

	
 
  ldap_permissions:
 
    - >
 
      to *
 
      by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage
 
      by * break
 
    - >
 
      to attrs=userPassword,shadowLastChange
 
      by self write
 
      by anonymous auth
 
      by dn="cn=admin,dc=example,dc=com" write
 
      by * none
 
    - >
 
      to dn.base=""
 
      by * read
 
    - >
 
      to *
 
      by self write
 
      by dn="cn=admin,dc=example,dc=com" write
 
      by users read
 
      by * none
 

	
 
  ldap_entries:
 
    - dn: ou=people,dc=example,dc=com
 
      attributes:
 
        objectClass: organizationalUnit
 
        ou: people
 
    - dn: ou=groups,dc=example,dc=com
 
      attributes:
 
        objectClass: organizationalUnit
 
        ou: groups
 
    - dn: uid=john,dc=example,dc=com
 
      attributes:
 
        objectClass:
 
          - inetOrgPerson
 
          - simpleSecurityObject
 
        userPassword: somepassword
 
        uid: john
 
        cn: John Doe
 
        sn: Doe
 

	
 

	
 
XMPP Server
 
-----------
 

	
 
The ``xmpp_server`` role can be used for setting-up Prosody, an XMPP server, on
 
destination machine.
 

	
 
The role implements the following:
 

	
 
* Deploys XMPP TLS private key and certificate.
 

	
 
  .. warning::
 
     The issued certificate must have multiple FQDNs listed as subject
 
     alternative names (DNS names) for each configured domain:
 

	
 
     - domain itself
 
     - ``conference.DOMAIN``
 
     - ``proxy.DOMAIN``
 

	
 
     A daily cron job is run to validate that all certificates have
 
     been configured and issued correctly.
 

	
 
* Installs Prosody.
 
* Configures Prosody.
 
* Configures firewall to allow incoming connections to the XMPP server.
 

	
 
Prosody is configured as follows:
 

	
 
* Modules enabled: roster, saslauth, tls, dialback, posix, private, vcard,
 
  version, uptime, time, ping, pep, register, admin_adhoc, announce,
 
  legacyauth, carbons, mam.
 
* Self-registration is not allowed.
 
* TLS is configured. Legacy TLS is available on port 5223.
 
* Client-to-server communication requires encryption (TLS).
 
* Uses 2048-bit Diffie-Hellman parameters for relevant TLS ciphers for
 
  incoming connections.
 
* Configures TLS versions and ciphers supported by Prosody (for
 
  *c2s*/client connections only).
 
* Authentication is done via LDAP. For setting the LDAP TLS truststore, see
 
  :ref:`LDAP Client <ldap_client>`.
 
* Internal storage is used.
 
* For each domain specified, a dedicated conference/multi-user chat (MUC)
 
  service is set-up, with FQDN set to ``conference.DOMAIN``.
 
* For each domain specified, a dedicated file proxy service will be set-up, with
 
  FQDN set to ``proxy.DOMAIN``.
 
* For each domain specified, a dedicated http file share service will be set-up,
 
  with FQDN set to ``upload.DOMAIN``.
 
* For each domain specified, a dedicated http file share service is
 
  set-up, with FQDN set to ``upload.DOMAIN``. Service is configured
 
  with maximum upload file size limit, as well as per-user daily
 
  quota. This allows clients to use `XEP-0363: HTTP File Upload
 
  <https://xmpp.org/extensions/xep-0363.html>`_ for exchanging files.
 

	
 
  .. warning::
 
     Due to `bug related to global quotas
 
     <https://issues.prosody.im/1891>`_, the role currently does not
 
     configure global quotas in any way. This might change in the
 
     future.
 

	
 
Prosody expects a specific directory structure in LDAP when doing look-ups:
 

	
 
* Prosody will log-in to LDAP as user
 
  ``cn=prosody,ou=services,XMPP_LDAP_BASE_DN``.
 
* User entries are read from sub-tree (first-level only)
 
  ``ou=people,XMPP_LDAP_BASE_DN``. Query filter used for finding users is
 
  ``(&(mail=$user@$host)(memberOf=cn=xmpp,ou=groups,XMPP_LDAP_BASE_DN))``. This
 
  allows group-based granting of XMPP service to users.
 

	
 

	
 
LDIF Templates
 
~~~~~~~~~~~~~~
 

	
 
For adding user to a group, use::
 

	
 
  dn: cn=xmpp,ou=groups,BASE_DN
 
  changetype: modify
 
  add: uniqueMember
 
  uniqueMember: uid=USERNAME,ou=people,BASE_DN
 

	
 

	
 
Role dependencies
 
~~~~~~~~~~~~~~~~~
 

	
 
Depends on the following roles:
 

	
 
* **common**
 
* **backup_client**
 

	
 

	
 
Backups
 
~~~~~~~
 

	
 
If the backup for this role has been enabled, the following paths are backed-up:
 

	
 
**/var/lib/prosody/**
 
  Roster information, as well as undelivered (offline) messages for all XMPP
 
  users. Keep in mind that list of available users and their credentials are
 
  stored in the LDAP directory (which is backed-up via LDAP server role).
 

	
 

	
 
Parameters
 
~~~~~~~~~~
 

	
 
**xmpp_administrators** (list, mandatory)
 
  List of Prosody users that should be granted administrator privileges over
 
  Prosody. Each item is a string with value equal to XMPP user ID
 
  (i.e. ``john.doe@example.com``).
 

	
 
**xmpp_domains** (list, mandatory)
 
  List of domains that are served by this Prosody instance. Each item is a
 
  string specifying a domain.
 

	
 
**xmpp_ldap_base_dn** (string, mandatory)
 
  Base DN on the LDAP server. A specific directory structure is expected under
 
  this entry (as explained above) in order to locate the available domains,
 
  users, aliases etc.
 

	
 
**xmpp_ldap_password** (string, mandatory)
 
  Password used for authenticating to the LDAP server.
 

	
 
**xmpp_ldap_server** (string, mandatory)
 
  Fully qualified domain name, hostname, or IP address of the LDAP server used
 
  for user authentication and listing.
 

	
 
**xmpp_http_file_share_daily_quota** (integer, optional, ``104857600``)
 
  Daily quota for individual users - maximum file size in bytes that a
 
  particular user can upload per day (`XEP-0363: HTTP File Upload
 
  <https://xmpp.org/extensions/xep-0363.html>`_).
 

	
 
**xmpp_http_file_share_size_limit** (integer, optional, ``10485760``)
 
  Maximum file size in bytes to allow for upload (`XEP-0363: HTTP File
 
  Upload <https://xmpp.org/extensions/xep-0363.html>`_).
 

	
 
**xmpp_server_archive_expiration** (string, optional, ``never``)
 
  Expiration period for messages stored server-side using `XEP-0313:
 
  Message Archive Management
 
  <https://xmpp.org/extensions/xep-0313.html>`_. The value should be
 
  compatible with `Prosody mod_mam
 
  <https://prosody.im/doc/modules/mod_mam>`_ configuration option
 
  ``archive_expires_after``.
 

	
 
**xmpp_server_tls_ciphers** (string, optional,  ``DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305:TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:!aNULL:!MD5:!EXPORT``)
 
  TLS ciphers to enable on the XMPP server. This should be an
 
  OpenSSL-compatible cipher specification. Value should be compatible
 
  with Prosody's option ``ciphers`` normally defined within the
 
  ``ssl`` section of configuration file (see `official documentation
 
  <https://prosody.im/doc/advanced_ssl_config#ciphers>`_ for details).
 
  Default value enables TLSv1.2-compatible strong PFS ciphers and RSA
 
  private keys. TLSv1.3-specific ciphers (``TLS_*``)
 
  are not configurable due to OpenSSL TLS context initialisation
 
  specifics/implementation details. They are included here for
 
  documentation/reference purposes only.
 

	
 
  .. warning::
 
     If the mail server minimum TLS version is set to TLSv1.3, it is
 
     still necessary to include at least one TLSv1.2-compatible cipher
 
     in the list in order to ensure TLSv1.3 initialisation. This is
 
     due to specifics of OpenSSL context initialisation/internals.
 

	
 
  The server-to-server communications are not affected by this
 
  configuration in order to ensure the widest possible compatibility
 
  with 3rd-party servers.
 

	
 
**xmpp_server_tls_protocol** (string, optional, ``tlsv1_2+``)
 
  TLS protocol versions the XMPP server should enable for client
 
  connections. The value specified should be compatible with Prosody's
 
  ``protocol`` option normally defined within the ``ssl`` section of
 
  configuration file (see `official documentation
 
  <https://prosody.im/doc/advanced_ssl_config#protocol>`__ for
 
  details). Older versions of TLS protocol (TLSv1.1 and lower) are not
 
  fully supported by the role, and additional configuration is
 
  required on the server that weakens the OpenSSL security policies in
 
  order to enable them.
 

	
 
  The server-to-server communications are not affected by this
 
  configuration in order to ensure the widest possible compatibility
 
  with 3rd-party servers.
 

	
 
**xmpp_tls_certificate** (string, mandatory)
 
  X.509 certificate used for TLS for XMPP service. The file will be stored in
 
  directory ``/etc/ssl/certs/`` under name ``{{ ansible_fqdn }}_xmpp.pem``.
 

	
 
**xmpp_tls_key** (string, mandatory)
 
  Private key used for TLS for XMPP service. The file will be stored in
 
  directory ``/etc/ssl/private/`` under name ``{{ ansible_fqdn }}_xmpp.key``.
 

	
 

	
 
Distribution compatibility
 
~~~~~~~~~~~~~~~~~~~~~~~~~~
 

	
 
Role is compatible with the following distributions:
 

	
 
- Debian 12 (Bookworm)
 

	
 

	
 
Examples
 
~~~~~~~~
 

	
 
Here is an example configuration for setting-up XMPP server using Prosody:
 

	
 
.. code-block:: yaml
 

	
 
  ---
 

	
 
  xmpp_administrators:
 
    - john.doe@example.com
 
  xmpp_domains:
 
    - example.com
 
  xmpp_ldap_base_dn: dc=example,dc=com
 
  xmpp_ldap_password: xmpp
 
  xmpp_ldap_server: ldap.example.com
 
  # These are default key and certificate that generated during Prosody
 
  # installation. Possibly you want to deploy your own.
 
  xmpp_tls_key: "{{ lookup('file', '/etc/prosody/certs/localhost.key') }}"
 
  xmpp_tls_certificate: "{{ lookup('file', '/etc/prosody/certs/localhost.crt') }}"
 

	
 

	
 
Mail Server
 
-----------
 

	
 
.. warning::
 
   It may happen that the ``clamav-freshclam`` service hasn't finished
 
   downloading the virus database before the ``clamav-daemon`` and
 
   ``clamav-milter`` services are enabled during the initial run. If mail server
 
   is not operational, you may need to wait for a little while for download to
 
   finish, and then restart the ``clamav-daemon`` and ``clamav-milter``
 
   services.
 

	
 
The ``mail_server`` role can be used for setting-up a complete mail server
roles/xmpp_server/defaults/main.yml
Show inline comments
 
---
 

	
 
enable_backup: false
 
xmpp_http_file_share_daily_quota: 104857600  # 100MiB
 
xmpp_http_file_share_size_limit: 10485760  # 10MiB
 
xmpp_server_archive_expiration: "never"
 
xmpp_server_tls_protocol: "tlsv1_2+"
 

	
 
# TLS_* ciphers are mandated by the TLSv1.3-related standards and
 
# cannot be disabled when TLSv1.3 is enabled on the server.
 
xmpp_server_tls_ciphers: "\
 
DHE-RSA-AES128-GCM-SHA256:\
 
DHE-RSA-AES256-GCM-SHA384:\
 
DHE-RSA-CHACHA20-POLY1305:\
 
ECDHE-RSA-AES128-GCM-SHA256:\
 
ECDHE-RSA-AES256-GCM-SHA384:\
 
ECDHE-RSA-CHACHA20-POLY1305:\
 
TLS_AES_128_GCM_SHA256:\
 
TLS_AES_256_GCM_SHA384:\
 
TLS_CHACHA20_POLY1305_SHA256:\
 
!aNULL:!MD5:!EXPORT"
roles/xmpp_server/molecule/default/group_vars/parameters-optional.yml
Show inline comments
 
---
 

	
 
xmpp_administrators:
 
  - jane.doe@domain2
 
  - mick.doe@domain3
 
xmpp_domains:
 
  - domain2
 
  - domain3
 
xmpp_http_file_share_daily_quota: 73400320  # 70MiB
 
xmpp_http_file_share_size_limit: 20971520  # 20MiB
 
xmpp_ldap_base_dn: dc=local
 
xmpp_ldap_password: prosodypassword
 
xmpp_ldap_server: ldap-server
 
xmpp_server_archive_expiration: "1w"
 
xmpp_tls_certificate: "{{ lookup('file', 'tests/data/x509/server/{{ ansible_fqdn }}_xmpp.cert.pem') }}"
 
xmpp_tls_key: "{{ lookup('file', 'tests/data/x509/server/{{ ansible_fqdn }}_xmpp.key.pem') }}"
 
xmpp_server_tls_protocol: "tlsv1_3+"
 
# At least one non-TLSv1.3 cipher has to be included in order to
 
# ensure TLSv1.3 gets initialised. TLSv1.3 ciphers (TLS_*) are not
 
# configurable and listed for documentation/reference purposes.
 
xmpp_server_tls_ciphers: "\
 
ECDHE-RSA-CHACHA20-POLY1305:\
 
TLS_AES_128_GCM_SHA256:\
 
TLS_AES_256_GCM_SHA384:\
 
TLS_CHACHA20_POLY1305_SHA256:\
 
!aNULL:!MD5:!EXPORT"
 

	
 
# common
 
ca_certificates:
 
  testca: "{{ lookup('file', 'tests/data/x509/ca/level1.cert.pem') }}"
 

	
 
# backup_client
 
enable_backup: true
 
backup_client_username: "bak-parameters-optional-{{ ansible_distribution_release }}"
 
backup_encryption_key: "{{ lookup('file', 'tests/data/gnupg/parameters-optional.asc') }}"
 
backup_server: backup-server
 
backup_server_host_ssh_public_keys:
 
  - "{{ lookup('file', 'tests/data/ssh/server_rsa.pub') }}"
 
  - "{{ lookup('file', 'tests/data/ssh/server_ed25519.pub') }}"
 
  - "{{ lookup('file', 'tests/data/ssh/server_ecdsa.pub') }}"
 
backup_ssh_key: "{{ lookup('file', 'tests/data/ssh/parameters-optional') }}"
roles/xmpp_server/molecule/default/tests/test_client.py
Show inline comments
 
@@ -52,160 +52,158 @@ def test_tls(host, username, password, domain):
 
    ["jane.doe", "janepassword", "domain2"],
 
])
 
def test_authentication_requires_tls(host, username, password, domain):
 
    """
 
    Tests if STARTTLS is required.
 
    """
 

	
 
    send = host.run(f"echo 'Hello' | go-sendxmpp --debug "
 
                    f"--username {username}@{domain} --password {password} --jserver {domain}:5222 "
 
                    f"{username}@{domain}")
 

	
 
    assert send.rc == 0
 
    assert "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'><required/></starttls>" in send.stderr
 

	
 

	
 
@pytest.mark.parametrize("username, password, domain", [
 
    ["john.doe", "johnpassword", "domain1"],
 
    ["jane.doe", "janepassword", "domain2"],
 
    ["mick.doe", "mickpassword", "domain3"],
 
])
 
def test_authentication(host, username, password, domain):
 
    """
 
    Tests if authentication works correctly.
 
    """
 

	
 
    send = host.run(f"echo 'Hello' | go-sendxmpp --debug "
 
                    f"--username {username}@{domain} --password {password} --jserver {domain}:5222 "
 
                    f"{username}@{domain}")
 
    assert send.rc == 0
 

	
 
    send = host.run(f"echo 'Hello' | go-sendxmpp --debug --tls "
 
                    f"--username {username}@{domain} --password {password} --jserver {domain}:5223 "
 
                    f"{username}@{domain}")
 
    assert send.rc == 0
 

	
 

	
 
@pytest.mark.parametrize("target_username, target_domain", [
 
    ["john.doe", "domain1"],
 
    ["jane.doe", "domain2"],
 
])
 
def test_unauthorized_users_rejected(host, target_username, target_domain):
 
    """
 
    Tests if unauthorized users (present in LDAP, but not member of correct
 
    group) are rejected from accessing the XMPP server.
 
    """
 

	
 
    send = host.run(f"echo 'Hello' | go-sendxmpp --debug "
 
                    f"--username noxmpp@{target_domain} --password noxmpppassword --jserver {target_domain}:5222 "
 
                    f"{target_username}@{target_domain}")
 
    assert send.rc != 0
 
    assert "Unable to authorize you with the authentication credentials you've sent" in send.stderr
 

	
 

	
 
@pytest.mark.parametrize("username, password, domain, server", [
 
    ["john.doe", "johnpassword", "domain1", "parameters-mandatory"],
 
    ["jane.doe", "janepassword", "domain2", "parameters-optional"],
 
    ["mick.doe", "mickpassword", "domain3", "parameters-optional"],
 
])
 
@pytest.mark.usefixtures("server_clean_domain_uploads")
 
def test_http_file_upload(host, server_host, username, password, domain):
 
    """
 
    Tests if http file upload works correctly.
 
    """
 

	
 
    upload_directory_path = f"/var/lib/prosody/upload%2e{domain}/http_file_share"
 

	
 
    # Prepare file for transfer.
 
    expected_content = str(uuid.uuid4())
 
    create_sample_file = host.run("echo -n %s > /tmp/http_file_upload_sample.txt", expected_content)
 
    assert create_sample_file.rc == 0
 

	
 
    # Upload the file.
 
    send = host.run(f"go-sendxmpp --debug --username {username}@{domain} --password {password} --jserver {domain}:5222 "
 
                    f"--http-upload /tmp/http_file_upload_sample.txt "
 
                    f"{username}@{domain}")
 
    assert send.rc == 0
 
    assert "No http upload component found." not in send.stderr
 

	
 
    # Verify content on server.
 
    with server_host.sudo():
 
        upload_directory = server_host.file(upload_directory_path)
 
        assert upload_directory.is_directory
 
        assert upload_directory.user == "prosody"
 
        assert upload_directory.group == "prosody"
 
        assert upload_directory.mode == 0o750
 
        assert len(upload_directory.listdir()) == 1
 

	
 
        uploaded_file_name = upload_directory.listdir()[0]
 
        uploaded_file = server_host.file(os.path.join(upload_directory_path, uploaded_file_name))
 
        assert uploaded_file.is_file
 
        assert uploaded_file.user == "prosody"
 
        assert uploaded_file.group == "prosody"
 
        assert uploaded_file.mode == 0o640
 
        assert uploaded_file.content_string == expected_content
 

	
 

	
 
@pytest.mark.parametrize("username, password, domain, server", [
 
    ["john.doe", "johnpassword", "domain1", "parameters-mandatory"],
 
    ["jane.doe", "janepassword", "domain2", "parameters-optional"],
 
    ["mick.doe", "mickpassword", "domain3", "parameters-optional"],
 
@pytest.mark.parametrize("username, password, domain, server, file_size_limit", [
 
    ["john.doe", "johnpassword", "domain1", "parameters-mandatory", 10 * 1024 * 1024],
 
    ["jane.doe", "janepassword", "domain2", "parameters-optional", 20 * 1024 * 1024],
 
    ["mick.doe", "mickpassword", "domain3", "parameters-optional", 20 * 1024 * 1024],
 
])
 
@pytest.mark.usefixtures("server_clean_domain_uploads")
 
def test_http_file_share_size_limit(host, username, password, domain):
 
def test_http_file_share_size_limit(host, username, password, domain, file_size_limit):
 
    """
 
    Tests the maximum file size for files uploaded via XEP-0363.
 
    """
 

	
 
    file_size_limit = 10 * 1024 * 1024
 

	
 
    # Test exact size limit.
 
    create_sample_file = host.run("dd if=/dev/zero of=/tmp/http_file_upload_sample.txt bs=%sB count=1", str(file_size_limit))
 
    assert create_sample_file.rc == 0
 

	
 
    send = host.run(f"go-sendxmpp --debug --username {username}@{domain} --password {password} --jserver {domain}:5222 "
 
                    f"--http-upload /tmp/http_file_upload_sample.txt "
 
                    f"{username}@{domain}")
 
    assert "file-too-large" not in send.stderr
 

	
 
    # Test exceeded size limit.
 
    create_sample_file = host.run("dd if=/dev/zero of=/tmp/http_file_upload_sample.txt bs=%sB count=1", str(file_size_limit + 1))
 
    assert create_sample_file.rc == 0
 

	
 
    send = host.run(f"go-sendxmpp --debug --username {username}@{domain} --password {password} --jserver {domain}:5222 "
 
                    f"--http-upload /tmp/http_file_upload_sample.txt "
 
                    f"{username}@{domain}")
 
    assert "file-too-large" in send.stderr
 

	
 

	
 
@pytest.mark.parametrize("username, password, domain, server", [
 
    ["john.doe", "johnpassword", "domain1", "parameters-mandatory"],
 
    ["jane.doe", "janepassword", "domain2", "parameters-optional"],
 
    ["mick.doe", "mickpassword", "domain3", "parameters-optional"],
 
@pytest.mark.parametrize("username, password, domain, server, file_size_limit, user_daily_quota", [
 
    ["john.doe", "johnpassword", "domain1", "parameters-mandatory", 10 * 1024 * 1024, 100 * 1024 * 1024],
 
    ["jane.doe", "janepassword", "domain2", "parameters-optional", 20 * 1024 * 1024, 70 * 1024 * 1024],
 
    ["mick.doe", "mickpassword", "domain3", "parameters-optional", 20 * 1024 * 1024, 70 * 1024 * 1024],
 
])
 
@pytest.mark.usefixtures("server_clean_domain_uploads")
 
def test_http_file_share_daily_quota(host, username, password, domain):
 
def test_http_file_share_daily_quota(host, username, password, domain, file_size_limit, user_daily_quota):
 
    """
 
    Tests the user's daily quota for files uploaded via XEP-0363.
 
    """
 

	
 
    # Equivalent of 100MiB.
 
    file_size_limit = 10 * 1024 * 1024
 
    file_count = 10
 
    remaining_quota = user_daily_quota
 
    while remaining_quota > 0:
 
        file_size = file_size_limit if file_size_limit < remaining_quota else remaining_quota
 
        create_sample_file = host.run("dd if=/dev/zero of=/tmp/http_file_upload_sample.txt bs=%sB count=1", str(file_size))
 
        assert create_sample_file.rc == 0
 

	
 
    # Fill-up the daily quota.
 
    create_sample_file = host.run("dd if=/dev/zero of=/tmp/http_file_upload_sample.txt bs=%sB count=1", str(file_size_limit))
 
    assert create_sample_file.rc == 0
 
    for _ in range(file_count):
 
        send = host.run(f"go-sendxmpp --debug --username {username}@{domain} --password {password} --jserver {domain}:5222 "
 
                        f"--http-upload /tmp/http_file_upload_sample.txt "
 
                        f"{username}@{domain}")
 
        assert send.rc == 0
 

	
 
        remaining_quota -= file_size
 

	
 
    # Test exceeded daily quota.
 
    create_sample_file = host.run("dd if=/dev/zero of=/tmp/http_file_upload_sample.txt bs=1B count=1")
 
    assert create_sample_file.rc == 0
 

	
 
    send = host.run(f"go-sendxmpp --debug --username {username}@{domain} --password {password} --jserver {domain}:5222 "
 
                    f"--http-upload /tmp/http_file_upload_sample.txt "
 
                    f"{username}@{domain}")
 
    assert "Daily quota reached" in send.stderr
roles/xmpp_server/molecule/default/tests/test_mandatory.py
Show inline comments
 
import os
 

	
 
import defusedxml.ElementTree as ElementTree
 

	
 
import pytest
 

	
 
import testinfra.utils.ansible_runner
 

	
 

	
 
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
 
    os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('parameters-mandatory')
 

	
 

	
 
def test_prosody_configuration_file_content(host):
 
    """
 
    Tests if Prosody configuration file has correct content.
 
    """
 

	
 
    hostname = host.run('hostname').stdout.strip()
 

	
 
    with host.sudo():
 

	
 
        config = host.file('/etc/prosody/prosody.cfg.lua')
 

	
 
        assert "admins = { \"john.doe@domain1\",  }" in config.content_string
 
        assert "key = \"/etc/ssl/private/%s_xmpp.key\";" % hostname in config.content_string
 
        assert "certificate = \"/etc/ssl/certs/%s_xmpp.pem\";" % hostname in config.content_string
 
        assert "ldap_server = \"ldap-server\"" in config.content_string
 
        assert "ldap_rootdn = \"cn=prosody,ou=services,dc=local\"" in config.content_string
 
        assert "ldap_password = \"prosodypassword\"" in config.content_string
 
        assert "ldap_filter = \"(&(mail=$user@$host)(memberOf=cn=xmpp,ou=groups,dc=local))\"" in config.content_string
 
        assert "ldap_base = \"ou=people,dc=local\"" in config.content_string
 
        assert "archive_expires_after = \"never\"" in config.content_string
 

	
 
        assert """VirtualHost "domain1"
 
Component "conference.domain1" "muc"
 
  restrict_room_creation = "local"
 
Component "proxy.domain1" "proxy65"
 
  proxy65_acl = { "domain1" }
 
Component "upload.domain1" "http_file_share"
 
  http_file_share_access = { "domain1" }""" in config.content_string
 
  http_file_share_access = { "domain1" }
 
  http_file_share_size_limit = 10485760
 
  http_file_share_daily_quota = 104857600""" in config.content_string
 

	
 

	
 
def test_xmpp_server_uses_correct_dh_parameters(host):
 
    """
 
    Tests if the HTTP server uses the generated Diffie-Hellman parameter.
 
    """
 

	
 
    fqdn = host.run('hostname -f').stdout.strip()
 

	
 
    # Use first defined domain for testing.
 
    domain = host.ansible.get_variables()['xmpp_domains'][0]
 

	
 
    with host.sudo():
 
        expected_dhparam = host.file('/etc/ssl/private/%s_xmpp.dh.pem' % fqdn).content_string.rstrip()
 

	
 
    connection = host.run("gnutls-cli --no-ca-verification --starttls-proto=xmpp --port 5222 "
 
                          "--priority 'NONE:+VERS-TLS1.2:+CTYPE-X509:+COMP-NULL:+SIGN-RSA-SHA384:+DHE-RSA:+SHA384:+AEAD:+AES-256-GCM' --verbose %s", domain)
 

	
 
    output = connection.stdout
 
    begin_marker = "-----BEGIN DH PARAMETERS-----"
 
    end_marker = "-----END DH PARAMETERS-----"
 
    used_dhparam = output[output.find(begin_marker):output.find(end_marker) + len(end_marker)]
 

	
 
    assert used_dhparam == expected_dhparam
 

	
 

	
 
@pytest.mark.parametrize("port", [
 
    5222,
 
    5223
 
])
 
def test_xmpp_c2s_tls_version_and_ciphers(host, port):
 
    """
 
    Tests if the correct TLS version and ciphers have been enabled for
 
    XMPP C2S ports.
 
    """
 

	
 
    expected_tls_versions = ["TLSv1.2", "TLSv1.3"]
 
    expected_tls_ciphers = [
 
        "TLS_AKE_WITH_AES_128_GCM_SHA256",
 
        "TLS_AKE_WITH_AES_256_GCM_SHA384",
 
        "TLS_AKE_WITH_CHACHA20_POLY1305_SHA256",
 
        "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256",
 
        "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384",
 
        "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
 
        "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
 
        "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
 
        "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
 
    ]
 

	
 
    # Run the nmap scanner against the server, and fetch the results.
 
    nmap = host.run("nmap -sV --script ssl-enum-ciphers -p %s domain1 -oX /tmp/report.xml", str(port))
 
    assert nmap.rc == 0
 
    report_content = host.file('/tmp/report.xml').content_string
 

	
 
    report_root = ElementTree.fromstring(report_content)
 

	
 
    tls_versions = []
 
    tls_ciphers = set()
 

	
 
    for child in report_root.findall("./host/ports/port/script[@id='ssl-enum-ciphers']/table"):
 
        tls_versions.append(child.attrib['key'])
 

	
 
    for child in report_root.findall(".//table[@key='ciphers']/table/elem[@key='name']"):
 
        tls_ciphers.add(child.text)
 

	
 
    tls_versions.sort()
 
    tls_ciphers = sorted(list(tls_ciphers))
 

	
 
    assert tls_versions == expected_tls_versions
 
    assert tls_ciphers == expected_tls_ciphers
 

	
 

	
 
def test_xmpp_s2s_tls_version_and_ciphers(host):
 
    """
 
    Tests if the correct TLS version and ciphers have been enabled for
 
    XMPP S2S port.
 
    """
 

	
 
    expected_tls_versions = ["TLSv1.2", "TLSv1.3"]
 
    # Seems like TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256 is off by default.
 
    expected_tls_ciphers = [
 
        "TLS_AKE_WITH_AES_128_GCM_SHA256",
 
        "TLS_AKE_WITH_AES_256_GCM_SHA384",
 
        "TLS_AKE_WITH_CHACHA20_POLY1305_SHA256",
 
        "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256",
 
        "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384",
 
        "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
 
        "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
 
        "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
 
    ]
 

	
 
    # Run the nmap scanner against the server, and fetch the results.
 
    nmap = host.run("nmap -sV --script ssl-enum-ciphers -p 5269 domain1 -oX /tmp/report.xml")
 
    assert nmap.rc == 0
 
    report_content = host.file('/tmp/report.xml').content_string
 

	
roles/xmpp_server/molecule/default/tests/test_optional.py
Show inline comments
 
import os
 

	
 
import defusedxml.ElementTree as ElementTree
 

	
 
import pytest
 

	
 
import testinfra.utils.ansible_runner
 

	
 

	
 
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
 
    os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('parameters-optional')
 

	
 

	
 
def test_prosody_configuration_file_content(host):
 
    """
 
    Tests if Prosody configuration file has correct content.
 
    """
 

	
 
    hostname = host.run('hostname').stdout.strip()
 

	
 
    with host.sudo():
 

	
 
        config = host.file('/etc/prosody/prosody.cfg.lua')
 

	
 
        assert "admins = { \"jane.doe@domain2\", \"mick.doe@domain3\",  }" in config.content_string
 
        assert "key = \"/etc/ssl/private/%s_xmpp.key\";" % hostname in config.content_string
 
        assert "certificate = \"/etc/ssl/certs/%s_xmpp.pem\";" % hostname in config.content_string
 
        assert "ldap_server = \"ldap-server\"" in config.content_string
 
        assert "ldap_rootdn = \"cn=prosody,ou=services,dc=local\"" in config.content_string
 
        assert "ldap_password = \"prosodypassword\"" in config.content_string
 
        assert "ldap_filter = \"(&(mail=$user@$host)(memberOf=cn=xmpp,ou=groups,dc=local))\"" in config.content_string
 
        assert "ldap_base = \"ou=people,dc=local\"" in config.content_string
 
        assert "archive_expires_after = \"1w\"" in config.content_string
 

	
 
        assert """VirtualHost "domain2"
 
Component "conference.domain2" "muc"
 
  restrict_room_creation = "local"
 
Component "proxy.domain2" "proxy65"
 
  proxy65_acl = { "domain2" }
 
Component "upload.domain2" "http_file_share"
 
  http_file_share_access = { "domain2" }""" in config.content_string
 
  http_file_share_access = { "domain2" }
 
  http_file_share_size_limit = 20971520
 
  http_file_share_daily_quota = 73400320""" in config.content_string
 

	
 
        assert """VirtualHost "domain3"
 
Component "conference.domain3" "muc"
 
  restrict_room_creation = "local"
 
Component "proxy.domain3" "proxy65"
 
  proxy65_acl = { "domain3" }
 
Component "upload.domain3" "http_file_share"
 
  http_file_share_access = { "domain3" }""" in config.content_string
 
  http_file_share_access = { "domain3" }
 
  http_file_share_size_limit = 20971520
 
  http_file_share_daily_quota = 73400320""" in config.content_string
 

	
 

	
 
@pytest.mark.parametrize("port", [
 
    5222,
 
    5223
 
])
 
def test_xmpp_c2s_tls_version_and_ciphers(host, port):
 
    """
 
    Tests if the correct TLS version and ciphers have been enabled for
 
    XMPP C2S ports.
 
    """
 

	
 
    expected_tls_versions = ["TLSv1.3"]
 
    expected_tls_ciphers = [
 
        "TLS_AKE_WITH_AES_128_GCM_SHA256",
 
        "TLS_AKE_WITH_AES_256_GCM_SHA384",
 
        "TLS_AKE_WITH_CHACHA20_POLY1305_SHA256",
 
    ]
 

	
 
    # Run the nmap scanner against the server, and fetch the results.
 
    nmap = host.run("nmap -sV --script ssl-enum-ciphers -p %s domain2 -oX /tmp/report.xml", str(port))
 
    assert nmap.rc == 0
 
    report_content = host.file('/tmp/report.xml').content_string
 

	
 
    report_root = ElementTree.fromstring(report_content)
 

	
 
    tls_versions = []
 
    tls_ciphers = set()
 

	
 
    for child in report_root.findall("./host/ports/port/script[@id='ssl-enum-ciphers']/table"):
 
        tls_versions.append(child.attrib['key'])
 

	
 
    for child in report_root.findall(".//table[@key='ciphers']/table/elem[@key='name']"):
 
        tls_ciphers.add(child.text)
 

	
 
    tls_versions.sort()
 
    tls_ciphers = sorted(list(tls_ciphers))
 

	
 
    assert tls_versions == expected_tls_versions
 
    assert tls_ciphers == expected_tls_ciphers
 

	
 

	
 
def test_xmpp_s2s_tls_version_and_ciphers(host):
 
    """
 
    Tests if the correct TLS version and ciphers have been enabled for
 
    XMPP S2S port.
 
    """
 

	
 
    expected_tls_versions = ["TLSv1.2", "TLSv1.3"]
 
    # Seems like TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256 is off by default.
 
    expected_tls_ciphers = [
 
        "TLS_AKE_WITH_AES_128_GCM_SHA256",
 
        "TLS_AKE_WITH_AES_256_GCM_SHA384",
 
        "TLS_AKE_WITH_CHACHA20_POLY1305_SHA256",
 
        "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256",
 
        "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384",
 
        "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
 
        "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
 
        "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
 
    ]
 

	
 
    # Run the nmap scanner against the server, and fetch the results.
 
    nmap = host.run("nmap -sV --script ssl-enum-ciphers -p 5269 domain2 -oX /tmp/report.xml")
 
    assert nmap.rc == 0
 
    report_content = host.file('/tmp/report.xml').content_string
 

	
 
    report_root = ElementTree.fromstring(report_content)
 

	
 
    tls_versions = []
 
    tls_ciphers = set()
 

	
 
    for child in report_root.findall("./host/ports/port/script[@id='ssl-enum-ciphers']/table"):
 
        tls_versions.append(child.attrib['key'])
 

	
 
    for child in report_root.findall(".//table[@key='ciphers']/table/elem[@key='name']"):
 
        tls_ciphers.add(child.text)
 

	
 
    tls_versions.sort()
 
    tls_ciphers = sorted(list(tls_ciphers))
 

	
 
    assert tls_versions == expected_tls_versions
 
    assert tls_ciphers == expected_tls_ciphers
roles/xmpp_server/templates/prosody.cfg.lua.j2
Show inline comments
 
@@ -19,97 +19,99 @@ modules_enabled = {
 
    "carbons"; -- Keep multiple clients in sync
 

	
 
  -- Nice to have
 
    "version"; -- Replies to server version requests
 
    "uptime"; -- Report how long server has been running
 
    "time"; -- Let others know the time here on this server
 
    "ping"; -- Replies to XMPP pings with pongs
 
    "pep"; -- Enables users to publish their mood, activity, playing music and more
 
    "register"; -- Allow users to register on this server using a client and change passwords
 
    "mam"; -- Store messages in an archive and allow users to access it
 

	
 
  -- Admin interfaces
 
    "admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands
 

	
 
  -- Other specific functionality
 
    "announce"; -- Send announcement to all online users
 
    "legacyauth"; -- Allow legacy authentication and SSL
 
};
 

	
 
-- Disable account creation by default, for security
 
-- For more information see http://prosody.im/doc/creating_accounts
 
allow_registration = false;
 

	
 
-- Set global settings for SSL/TLS.
 
ssl = {
 
  key = "/etc/ssl/private/{{ ansible_fqdn }}_xmpp.key";
 
  certificate = "/etc/ssl/certs/{{ ansible_fqdn }}_xmpp.pem";
 
  dhparam = "/etc/ssl/private/{{ ansible_fqdn }}_xmpp.dh.pem";
 
}
 

	
 
-- Configure TLS protocol and ciphers for client-to-server
 
-- connections (STARTTLS).
 
c2s_ssl = {
 
  protocol = "{{ xmpp_server_tls_protocol }}";
 
  ciphers = "{{ xmpp_server_tls_ciphers }}";
 
}
 

	
 
-- Configure TLS protocol and ciphers for client-to-server
 
-- connections (direct TLS).
 
c2s_direct_tls_ssl = {
 
  protocol = "{{ xmpp_server_tls_protocol }}";
 
  ciphers = "{{ xmpp_server_tls_ciphers }}";
 
  -- @WORKAROUND: No DHE ciphers because dhparam is getting reset
 
  --
 
  --    There is a bug in Prosody 0.12.3 resulting in dhparam value
 
  --    from from global config getting ignored when domain SNI
 
  --    context is initalised on TCP port 5223. Define the parameter
 
  --    in within this configuration context as well to fix the issue.
 
  dhparam = "/etc/ssl/private/{{ ansible_fqdn }}_xmpp.dh.pem";
 
}
 

	
 
-- Ports on which to have direct TLS/SSL.
 
c2s_direct_tls_ports = { 5223 }
 

	
 
-- Force clients to use encrypted connection.
 
c2s_require_encryption = true
 

	
 
-- Disable certificate validation for server-to-server connections.
 
s2s_secure_auth = false
 

	
 
-- Path to Prosody's PID file.
 
pidfile = "/run/prosody/prosody.pid"
 

	
 
-- Authentication backend.
 
authentication = "ldap"
 
ldap_server = "{{ xmpp_ldap_server }}"
 
ldap_rootdn = "cn=prosody,ou=services,{{ xmpp_ldap_base_dn }}"
 
ldap_password = "{{ xmpp_ldap_password }}"
 
ldap_filter = "(&(mail=$user@$host)(memberOf=cn=xmpp,ou=groups,{{xmpp_ldap_base_dn}}))"
 
ldap_scope = "onelevel"
 
ldap_tls = true
 
ldap_base = "ou=people,{{ xmpp_ldap_base_dn }}"
 

	
 
-- Message Archives (mod_mam) configuration.
 
archive_expires_after = "{{ xmpp_server_archive_expiration }}"
 

	
 
-- Storage backend.
 
storage = "internal"
 

	
 
-- Logging configuration.
 
log = {
 
  info = "/var/log/prosody/prosody.log"; -- Change 'info' to 'debug' for verbose logging
 
  error = "/var/log/prosody/prosody.err";
 
  "*syslog";
 
}
 

	
 
-- Domains which should be handled by Prosody, with dedicated MUC and file
 
-- proxying components.
 
{% for domain in xmpp_domains -%}
 
VirtualHost "{{ domain }}"
 
Component "conference.{{ domain }}" "muc"
 
  restrict_room_creation = "local"
 
Component "proxy.{{ domain }}" "proxy65"
 
  proxy65_acl = { "{{ domain }}" }
 
Component "upload.{{ domain }}" "http_file_share"
 
  http_file_share_access = { "{{ domain }}" }
 
  http_file_share_size_limit = {{ xmpp_http_file_share_size_limit }}
 
  http_file_share_daily_quota = {{ xmpp_http_file_share_daily_quota }}
 
{% endfor -%}
0 comments (0 inline, 0 general)