From 37a33e12cc580a054bcc50ae7f7556710e1992c8 Mon Sep 17 00:00:00 2001 From: jschaul Date: Mon, 5 Dec 2022 15:58:11 +0100 Subject: [PATCH 01/38] Add sphinx-copybutton plugin to make copying snippets of code from docs.wire.com easier. --- changelog.d/4-docs/copybutton | 1 + docs/src/conf.py | 1 + nix/default.nix | 1 + 3 files changed, 3 insertions(+) create mode 100644 changelog.d/4-docs/copybutton diff --git a/changelog.d/4-docs/copybutton b/changelog.d/4-docs/copybutton new file mode 100644 index 0000000000..cef39536e5 --- /dev/null +++ b/changelog.d/4-docs/copybutton @@ -0,0 +1 @@ +Add sphinx-copybutton plugin to make copying snippets of code from docs.wire.com easier. diff --git a/docs/src/conf.py b/docs/src/conf.py index e7c36d04e6..a37c1a8dd9 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -46,6 +46,7 @@ 'rst2pdf.pdfbuilder', 'sphinx_multiversion', 'sphinx_reredirects', + 'sphinx_copybutton', ] # Grouping the document tree into PDF files. List of tuples diff --git a/nix/default.nix b/nix/default.nix index 34f37250e0..bd35e4aaf2 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -36,6 +36,7 @@ let sphinx-multiversion sphinx_rtd_theme sphinx_reredirects + sphinx-copybutton sphinxcontrib-fulltoc sphinxcontrib-kroki ])) From 36507cf671063c0bdea120378c4c629dcec0d730 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Thu, 12 Jan 2023 17:22:31 +0100 Subject: [PATCH 02/38] Convert docs from reStructuredText to MyST markdown (#2980) --- docs/convert/compare_screenshots.py | 16 + docs/convert/config.yaml | 1 + docs/convert/conversions.yaml | 1 + docs/convert/convert.sh | 12 + docs/convert/revert.sh | 4 + docs/convert/screenshots.py | 47 + docs/convert/shell.nix | 17 + docs/src/conf.py | 3 +- docs/src/developer/developer/index.md | 10 + docs/src/developer/developer/index.rst | 10 - docs/src/developer/{index.rst => index.md} | 20 +- docs/src/developer/reference/index.md | 10 + docs/src/developer/reference/index.rst | 10 - .../src/developer/reference/spar-braindump.md | 2 +- docs/src/how-to/administrate/cassandra.md | 63 + docs/src/how-to/administrate/cassandra.rst | 65 - docs/src/how-to/administrate/elasticsearch.md | 127 ++ .../src/how-to/administrate/elasticsearch.rst | 134 -- docs/src/how-to/administrate/etcd.md | 261 ++++ docs/src/how-to/administrate/etcd.rst | 264 ---- docs/src/how-to/administrate/general-linux.md | 67 + .../src/how-to/administrate/general-linux.rst | 69 - docs/src/how-to/administrate/index.md | 12 + docs/src/how-to/administrate/index.rst | 14 - .../kubernetes/certificate-renewal/index.md | 10 + .../kubernetes/certificate-renewal/index.rst | 10 - .../scenario-1_k8s-v1.14-kubespray.md | 241 ++++ .../scenario-1_k8s-v1.14-kubespray.rst | 244 ---- .../how-to/administrate/kubernetes/index.md | 20 + .../how-to/administrate/kubernetes/index.rst | 21 - .../kubernetes/restart-machines/index.md | 42 + .../kubernetes/restart-machines/index.rst | 45 - .../kubernetes/upgrade-cluster/index.md | 75 ++ .../kubernetes/upgrade-cluster/index.rst | 82 -- .../administrate/{minio.rst => minio.md} | 256 ++-- docs/src/how-to/administrate/operations.md | 139 +++ docs/src/how-to/administrate/operations.rst | 144 --- docs/src/how-to/administrate/restund.md | 293 +++++ docs/src/how-to/administrate/restund.rst | 301 ----- docs/src/how-to/administrate/users.md | 590 +++++++++ docs/src/how-to/administrate/users.rst | 609 --------- .../custom-backend-for-desktop-client.md | 79 ++ .../custom-backend-for-desktop-client.rst | 90 -- ...ertificates.rst => custom-certificates.md} | 5 +- docs/src/how-to/associate/deeplink.md | 174 +++ docs/src/how-to/associate/deeplink.rst | 174 --- docs/src/how-to/associate/index.md | 10 + docs/src/how-to/associate/index.rst | 10 - docs/src/how-to/index.md | 20 + docs/src/how-to/index.rst | 21 - docs/src/how-to/install/ansible-VMs.md | 275 ++++ docs/src/how-to/install/ansible-VMs.rst | 277 ----- .../how-to/install/ansible-authentication.md | 63 + .../how-to/install/ansible-authentication.rst | 66 - docs/src/how-to/install/ansible-tinc.md | 54 + docs/src/how-to/install/ansible-tinc.rst | 54 - docs/src/how-to/install/aws-prod.md | 36 + docs/src/how-to/install/aws-prod.rst | 39 - .../how-to/install/configuration-options.md | 1048 ++++++++++++++++ .../how-to/install/configuration-options.rst | 1106 ----------------- docs/src/how-to/install/dependencies.md | 69 + docs/src/how-to/install/dependencies.rst | 74 -- docs/src/how-to/install/helm-prod.md | 208 ++++ docs/src/how-to/install/helm-prod.rst | 225 ---- docs/src/how-to/install/helm.md | 145 +++ docs/src/how-to/install/helm.rst | 154 --- .../install/includes/dns-federation.rst | 43 - .../helm_dns-ingress-troubleshooting.inc.rst | 2 - docs/src/how-to/install/index.md | 30 + docs/src/how-to/install/index.rst | 30 - docs/src/how-to/install/kubernetes.md | 85 ++ docs/src/how-to/install/kubernetes.rst | 83 -- .../install/{logging.rst => logging.md} | 160 ++- .../install/{monitoring.rst => monitoring.md} | 14 +- .../install/{planning.rst => planning.md} | 57 +- docs/src/how-to/install/prod-intro.md | 58 + docs/src/how-to/install/prod-intro.rst | 60 - docs/src/how-to/install/restund.md | 80 ++ docs/src/how-to/install/restund.rst | 88 -- docs/src/how-to/install/{sft.rst => sft.md} | 162 ++- docs/src/how-to/install/tls.md | 52 + docs/src/how-to/install/tls.rst | 60 - docs/src/how-to/install/troubleshooting.md | 265 ++++ docs/src/how-to/install/troubleshooting.rst | 255 ---- .../how-to/install/version-requirements.md | 28 + .../how-to/install/version-requirements.rst | 35 - .../post-install/{index.rst => index.md} | 16 +- .../how-to/post-install/logrotation-check.md | 81 ++ .../how-to/post-install/logrotation-check.rst | 79 -- docs/src/how-to/post-install/ntp-check.md | 44 + docs/src/how-to/post-install/ntp-check.rst | 48 - docs/src/how-to/single-sign-on/adfs/main.md | 41 + docs/src/how-to/single-sign-on/adfs/main.rst | 19 - docs/src/how-to/single-sign-on/azure/main.md | 92 ++ docs/src/how-to/single-sign-on/azure/main.rst | 82 -- .../centrify/{main.rst => main.md} | 66 +- .../how-to/single-sign-on/generic-setup.md | 37 + .../how-to/single-sign-on/generic-setup.rst | 42 - docs/src/how-to/single-sign-on/index.md | 15 + .../single-sign-on/okta/{main.rst => main.md} | 62 +- ...ouble-shooting.rst => trouble-shooting.md} | 221 ++-- .../Wire_SAML_Flow (lucidchart).svg | 0 .../understand}/Wire_SAML_Flow.png | Bin .../how-to/single-sign-on/understand/main.md | 561 +++++++++ .../understand}/token-step-01.png | Bin .../understand}/token-step-02.png | Bin .../understand}/token-step-03.png | Bin .../understand}/token-step-04.png | Bin .../understand}/token-step-05.png | Bin .../understand}/token-step-06.png | Bin docs/src/index.md | 46 + docs/src/index.rst | 43 - .../{release-notes.rst => release-notes.md} | 13 +- .../2021-12-15_log4shell.md | 90 ++ .../2021-12-15_log4shell.rst | 103 -- docs/src/security-responses/index.md | 14 + docs/src/security-responses/index.rst | 16 - .../api-client-perspective/authentication.md | 435 +++++++ .../api-client-perspective/authentication.rst | 476 ------- .../api-client-perspective/index.md | 15 + .../api-client-perspective/index.rst | 14 - .../{swagger.rst => swagger.md} | 17 +- docs/src/understand/federation/index.md | 24 + docs/src/understand/federation/index.rst | 25 - docs/src/understand/helm.md | 61 + docs/src/understand/helm.rst | 64 - docs/src/understand/index.md | 17 + docs/src/understand/index.rst | 17 - docs/src/understand/{minio.rst => minio.md} | 13 +- docs/src/understand/notes/port-ranges.md | 36 + docs/src/understand/notes/port-ranges.rst | 36 - docs/src/understand/overview.md | 143 +++ docs/src/understand/overview.rst | 148 --- .../understand/{restund.rst => restund.md} | 110 +- docs/src/understand/{sft.rst => sft.md} | 97 +- docs/src/understand/single-sign-on/design.rst | 3 - docs/src/understand/single-sign-on/main.rst | 560 --------- 137 files changed, 7202 insertions(+), 7424 deletions(-) create mode 100644 docs/convert/compare_screenshots.py create mode 100644 docs/convert/config.yaml create mode 100644 docs/convert/conversions.yaml create mode 100644 docs/convert/convert.sh create mode 100644 docs/convert/revert.sh create mode 100644 docs/convert/screenshots.py create mode 100644 docs/convert/shell.nix create mode 100644 docs/src/developer/developer/index.md delete mode 100644 docs/src/developer/developer/index.rst rename docs/src/developer/{index.rst => index.md} (52%) create mode 100644 docs/src/developer/reference/index.md delete mode 100644 docs/src/developer/reference/index.rst create mode 100644 docs/src/how-to/administrate/cassandra.md delete mode 100644 docs/src/how-to/administrate/cassandra.rst create mode 100644 docs/src/how-to/administrate/elasticsearch.md delete mode 100644 docs/src/how-to/administrate/elasticsearch.rst create mode 100644 docs/src/how-to/administrate/etcd.md delete mode 100644 docs/src/how-to/administrate/etcd.rst create mode 100644 docs/src/how-to/administrate/general-linux.md delete mode 100644 docs/src/how-to/administrate/general-linux.rst create mode 100644 docs/src/how-to/administrate/index.md delete mode 100644 docs/src/how-to/administrate/index.rst create mode 100644 docs/src/how-to/administrate/kubernetes/certificate-renewal/index.md delete mode 100644 docs/src/how-to/administrate/kubernetes/certificate-renewal/index.rst create mode 100644 docs/src/how-to/administrate/kubernetes/certificate-renewal/scenario-1_k8s-v1.14-kubespray.md delete mode 100644 docs/src/how-to/administrate/kubernetes/certificate-renewal/scenario-1_k8s-v1.14-kubespray.rst create mode 100644 docs/src/how-to/administrate/kubernetes/index.md delete mode 100644 docs/src/how-to/administrate/kubernetes/index.rst create mode 100644 docs/src/how-to/administrate/kubernetes/restart-machines/index.md delete mode 100644 docs/src/how-to/administrate/kubernetes/restart-machines/index.rst create mode 100644 docs/src/how-to/administrate/kubernetes/upgrade-cluster/index.md delete mode 100644 docs/src/how-to/administrate/kubernetes/upgrade-cluster/index.rst rename docs/src/how-to/administrate/{minio.rst => minio.md} (58%) create mode 100644 docs/src/how-to/administrate/operations.md delete mode 100644 docs/src/how-to/administrate/operations.rst create mode 100644 docs/src/how-to/administrate/restund.md delete mode 100644 docs/src/how-to/administrate/restund.rst create mode 100644 docs/src/how-to/administrate/users.md delete mode 100644 docs/src/how-to/administrate/users.rst create mode 100644 docs/src/how-to/associate/custom-backend-for-desktop-client.md delete mode 100644 docs/src/how-to/associate/custom-backend-for-desktop-client.rst rename docs/src/how-to/associate/{custom-certificates.rst => custom-certificates.md} (80%) create mode 100644 docs/src/how-to/associate/deeplink.md delete mode 100644 docs/src/how-to/associate/deeplink.rst create mode 100644 docs/src/how-to/associate/index.md delete mode 100644 docs/src/how-to/associate/index.rst create mode 100644 docs/src/how-to/index.md delete mode 100644 docs/src/how-to/index.rst create mode 100644 docs/src/how-to/install/ansible-VMs.md delete mode 100644 docs/src/how-to/install/ansible-VMs.rst create mode 100644 docs/src/how-to/install/ansible-authentication.md delete mode 100644 docs/src/how-to/install/ansible-authentication.rst create mode 100644 docs/src/how-to/install/ansible-tinc.md delete mode 100644 docs/src/how-to/install/ansible-tinc.rst create mode 100644 docs/src/how-to/install/aws-prod.md delete mode 100644 docs/src/how-to/install/aws-prod.rst create mode 100644 docs/src/how-to/install/configuration-options.md delete mode 100644 docs/src/how-to/install/configuration-options.rst create mode 100644 docs/src/how-to/install/dependencies.md delete mode 100644 docs/src/how-to/install/dependencies.rst create mode 100644 docs/src/how-to/install/helm-prod.md delete mode 100644 docs/src/how-to/install/helm-prod.rst create mode 100644 docs/src/how-to/install/helm.md delete mode 100644 docs/src/how-to/install/helm.rst delete mode 100644 docs/src/how-to/install/includes/dns-federation.rst create mode 100644 docs/src/how-to/install/index.md delete mode 100644 docs/src/how-to/install/index.rst create mode 100644 docs/src/how-to/install/kubernetes.md delete mode 100644 docs/src/how-to/install/kubernetes.rst rename docs/src/how-to/install/{logging.rst => logging.md} (60%) rename docs/src/how-to/install/{monitoring.rst => monitoring.md} (58%) rename docs/src/how-to/install/{planning.rst => planning.md} (55%) create mode 100644 docs/src/how-to/install/prod-intro.md delete mode 100644 docs/src/how-to/install/prod-intro.rst create mode 100644 docs/src/how-to/install/restund.md delete mode 100644 docs/src/how-to/install/restund.rst rename docs/src/how-to/install/{sft.rst => sft.md} (67%) create mode 100644 docs/src/how-to/install/tls.md delete mode 100644 docs/src/how-to/install/tls.rst create mode 100644 docs/src/how-to/install/troubleshooting.md delete mode 100644 docs/src/how-to/install/troubleshooting.rst create mode 100644 docs/src/how-to/install/version-requirements.md delete mode 100644 docs/src/how-to/install/version-requirements.rst rename docs/src/how-to/post-install/{index.rst => index.md} (53%) create mode 100644 docs/src/how-to/post-install/logrotation-check.md delete mode 100644 docs/src/how-to/post-install/logrotation-check.rst create mode 100644 docs/src/how-to/post-install/ntp-check.md delete mode 100644 docs/src/how-to/post-install/ntp-check.rst create mode 100644 docs/src/how-to/single-sign-on/adfs/main.md delete mode 100644 docs/src/how-to/single-sign-on/adfs/main.rst create mode 100644 docs/src/how-to/single-sign-on/azure/main.md delete mode 100644 docs/src/how-to/single-sign-on/azure/main.rst rename docs/src/how-to/single-sign-on/centrify/{main.rst => main.md} (55%) create mode 100644 docs/src/how-to/single-sign-on/generic-setup.md delete mode 100644 docs/src/how-to/single-sign-on/generic-setup.rst create mode 100644 docs/src/how-to/single-sign-on/index.md rename docs/src/how-to/single-sign-on/okta/{main.rst => main.md} (73%) rename docs/src/how-to/single-sign-on/{trouble-shooting.rst => trouble-shooting.md} (60%) rename docs/src/{understand/single-sign-on => how-to/single-sign-on/understand}/Wire_SAML_Flow (lucidchart).svg (100%) rename docs/src/{understand/single-sign-on => how-to/single-sign-on/understand}/Wire_SAML_Flow.png (100%) create mode 100644 docs/src/how-to/single-sign-on/understand/main.md rename docs/src/{understand/single-sign-on => how-to/single-sign-on/understand}/token-step-01.png (100%) rename docs/src/{understand/single-sign-on => how-to/single-sign-on/understand}/token-step-02.png (100%) rename docs/src/{understand/single-sign-on => how-to/single-sign-on/understand}/token-step-03.png (100%) rename docs/src/{understand/single-sign-on => how-to/single-sign-on/understand}/token-step-04.png (100%) rename docs/src/{understand/single-sign-on => how-to/single-sign-on/understand}/token-step-05.png (100%) rename docs/src/{understand/single-sign-on => how-to/single-sign-on/understand}/token-step-06.png (100%) create mode 100644 docs/src/index.md delete mode 100644 docs/src/index.rst rename docs/src/{release-notes.rst => release-notes.md} (51%) create mode 100644 docs/src/security-responses/2021-12-15_log4shell.md delete mode 100644 docs/src/security-responses/2021-12-15_log4shell.rst create mode 100644 docs/src/security-responses/index.md delete mode 100644 docs/src/security-responses/index.rst create mode 100644 docs/src/understand/api-client-perspective/authentication.md delete mode 100644 docs/src/understand/api-client-perspective/authentication.rst create mode 100644 docs/src/understand/api-client-perspective/index.md delete mode 100644 docs/src/understand/api-client-perspective/index.rst rename docs/src/understand/api-client-perspective/{swagger.rst => swagger.md} (67%) create mode 100644 docs/src/understand/federation/index.md delete mode 100644 docs/src/understand/federation/index.rst create mode 100644 docs/src/understand/helm.md delete mode 100644 docs/src/understand/helm.rst create mode 100644 docs/src/understand/index.md delete mode 100644 docs/src/understand/index.rst rename docs/src/understand/{minio.rst => minio.md} (86%) create mode 100644 docs/src/understand/notes/port-ranges.md delete mode 100644 docs/src/understand/notes/port-ranges.rst create mode 100644 docs/src/understand/overview.md delete mode 100644 docs/src/understand/overview.rst rename docs/src/understand/{restund.rst => restund.md} (61%) rename docs/src/understand/{sft.rst => sft.md} (77%) delete mode 100644 docs/src/understand/single-sign-on/design.rst delete mode 100644 docs/src/understand/single-sign-on/main.rst diff --git a/docs/convert/compare_screenshots.py b/docs/convert/compare_screenshots.py new file mode 100644 index 0000000000..c5b4d9eca1 --- /dev/null +++ b/docs/convert/compare_screenshots.py @@ -0,0 +1,16 @@ +#!/usr/bin/env sh + +import subprocess +import os + +output = subprocess.check_output(['find', 'screenshots', '-name', '*_dev.png']).decode('utf8') + +for dev in output.splitlines(): + ref = dev.replace('_dev.png', '_ref.png') + if os.path.exists(dev) and os.path.exists(ref): + print(dev) + cmd = ['compare', '-compose', 'src', dev, ref, dev.replace('_dev.png', '_diff.png')] + print(cmd) + subprocess.run(cmd) + else: + print(f'Cannot compare {dev}') diff --git a/docs/convert/config.yaml b/docs/convert/config.yaml new file mode 100644 index 0000000000..78f2c64c8f --- /dev/null +++ b/docs/convert/config.yaml @@ -0,0 +1 @@ +colon_fences: false diff --git a/docs/convert/conversions.yaml b/docs/convert/conversions.yaml new file mode 100644 index 0000000000..cbeb844cb6 --- /dev/null +++ b/docs/convert/conversions.yaml @@ -0,0 +1 @@ +sphinx.domains.std.Glossary: eval_rst diff --git a/docs/convert/convert.sh b/docs/convert/convert.sh new file mode 100644 index 0000000000..11a4a33fe7 --- /dev/null +++ b/docs/convert/convert.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# +set -e +# shellcheck disable=SC2044,SC3010 +for f in $(find . -type f -name '*.rst'); do + if [[ "$f" == */includes/* ]]; then + echo skipping "$f" + continue + fi + rst2myst convert -c convert/conversions.yaml --no-colon-fences "$f" + rm -f "$f" +done diff --git a/docs/convert/revert.sh b/docs/convert/revert.sh new file mode 100644 index 0000000000..df5cf912b3 --- /dev/null +++ b/docs/convert/revert.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env sh + +git checkout src +git clean src -f diff --git a/docs/convert/screenshots.py b/docs/convert/screenshots.py new file mode 100644 index 0000000000..ff172710d5 --- /dev/null +++ b/docs/convert/screenshots.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 + +from selenium import webdriver +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.common.by import By +import subprocess +import os.path + +def sanitize_name(name): + r = '' + for c in name: + if c.isalpha(): + r += c + else: + r += '_' + return r + +driver = webdriver.Firefox() + +output = subprocess.check_output(['find', 'build', '-name', '*.html']).decode('utf8') +for i, p in enumerate(output.splitlines()): + n = os.path.relpath(p, 'build') + url_dev = f'http://localhost:3000/{n}' + url_ref = f'https://docs.wire.com/{n}' + img_basename = sanitize_name(n) + '_' + str(i) + + try: + print(f'./screenshots/{i:03}-{img_basename}_dev.png') + driver.get(url_dev) + driver.get_full_page_screenshot_as_file(f'./screenshots/{i:03}-{img_basename}_dev.png') + print(url_ref) + driver.get(url_ref) + driver.get_full_page_screenshot_as_file(f'./screenshots/{i:03}-{img_basename}_ref.png') + except: + pass + +driver.close() + + + +# assert "Python" in driver.title +# elem = driver.find_element(By.NAME, "q") +# elem.clear() +# elem.send_keys("pycon") +# elem.send_keys(Keys.RETURN) +# assert "No results found." not in driver.page_source +# diff --git a/docs/convert/shell.nix b/docs/convert/shell.nix new file mode 100644 index 0000000000..130c456c6d --- /dev/null +++ b/docs/convert/shell.nix @@ -0,0 +1,17 @@ +{ pkgs ? import {} }: +(pkgs.buildFHSUserEnv { + name = "pipzone"; + targetPkgs = pkgs: (with pkgs; [ + python3 + python3Packages.pip + python3Packages.virtualenv + ]); + runScript = "bash"; +}).env + +# then +# virtualenv venv +# pip install rst-to-myst +# Fix this bug locally: https://github.com/executablebooks/rst-to-myst/issues/49 +# pip install sphinx-reredirects +# pip install sphinx-multiversion diff --git a/docs/src/conf.py b/docs/src/conf.py index e7c36d04e6..1d2c7fa490 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -128,6 +128,5 @@ "security-responses/log4shell": "2021-12-15_log4shell.html", "security-responses/cve-2021-44521": "2022-02-21_cve-2021-44521.html", "security-responses/2022-05_website_outage": "2022-05-23_website_outage.html", - "how-to/single-sign-on/index": "../../understand/single-sign-on/main.html#setting-up-sso-externally", - "how-to/scim/index": "../../understand/single-sign-on/main.html#user-provisioning", + "how-to/scim/index": "../../understand/single-sign-on/main.html#user-provisioning" } diff --git a/docs/src/developer/developer/index.md b/docs/src/developer/developer/index.md new file mode 100644 index 0000000000..77e35760cf --- /dev/null +++ b/docs/src/developer/developer/index.md @@ -0,0 +1,10 @@ +# Developer + +```{toctree} +:caption: 'Contents:' +:glob: true +:numbered: true +:titlesonly: true + +** +``` diff --git a/docs/src/developer/developer/index.rst b/docs/src/developer/developer/index.rst deleted file mode 100644 index a8fefaa770..0000000000 --- a/docs/src/developer/developer/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -Developer -========= - -.. toctree:: - :titlesonly: - :numbered: - :caption: Contents: - :glob: - - ** diff --git a/docs/src/developer/index.rst b/docs/src/developer/index.md similarity index 52% rename from docs/src/developer/index.rst rename to docs/src/developer/index.md index b48dbecae0..59cf4fd92e 100644 --- a/docs/src/developer/index.rst +++ b/docs/src/developer/index.md @@ -1,19 +1,19 @@ -Notes for developers -==================== +# Notes for developers -If you are an on-premise operator (administrating your own self-hosted installation of wire-server), you may want to go back to `docs.wire.com `_ and ignore this section of the docs. +If you are an on-premise operator (administrating your own self-hosted installation of wire-server), you may want to go back to [docs.wire.com](https://docs.wire.com/) and ignore this section of the docs. -If you are a wire end-user, please check out our `support pages `_. +If you are a wire end-user, please check out our [support pages](https://support.wire.com/). What you need to know as a user of the Wire backend: concepts, features, and API. We want to keep these up to date. They could benefit from some re-ordering, and they are far from complete, but we hope they will still help you. -.. toctree:: - :titlesonly: - :caption: Contents: - :glob: +```{toctree} +:caption: 'Contents:' +:glob: true +:titlesonly: true - developer/index.rst - reference/index.rst +developer/index.rst +reference/index.rst +``` diff --git a/docs/src/developer/reference/index.md b/docs/src/developer/reference/index.md new file mode 100644 index 0000000000..4b6e82f195 --- /dev/null +++ b/docs/src/developer/reference/index.md @@ -0,0 +1,10 @@ +# Reference + +```{toctree} +:caption: 'Contents:' +:glob: true +:numbered: true +:titlesonly: true + +** +``` diff --git a/docs/src/developer/reference/index.rst b/docs/src/developer/reference/index.rst deleted file mode 100644 index 1eb9feedba..0000000000 --- a/docs/src/developer/reference/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -Reference -========= - -.. toctree:: - :titlesonly: - :numbered: - :caption: Contents: - :glob: - - ** diff --git a/docs/src/developer/reference/spar-braindump.md b/docs/src/developer/reference/spar-braindump.md index f32532108b..dcee5847e9 100644 --- a/docs/src/developer/reference/spar-braindump.md +++ b/docs/src/developer/reference/spar-braindump.md @@ -113,7 +113,7 @@ export IDP_ID=... Copy the new metadata file to one of your spar instances. -Ssh into it. If you can't, [the sso docs](../../understand/single-sign-on/main.rst) explain how you can create a +Ssh into it. If you can't, [the sso docs](../../how-to/single-sign-on/understand/main.rst) explain how you can create a bearer token if you have the admin's login credentials. If you follow that approach, you need to replace all mentions of `-H'Z-User ...'` with `-H'Authorization: Bearer ...'` in the following, and you won't need diff --git a/docs/src/how-to/administrate/cassandra.md b/docs/src/how-to/administrate/cassandra.md new file mode 100644 index 0000000000..c75439d626 --- /dev/null +++ b/docs/src/how-to/administrate/cassandra.md @@ -0,0 +1,63 @@ +# Cassandra + +```{eval-rst} +.. include:: includes/intro.rst +``` + +This section only covers the bare minimum, for more information, see the [cassandra +documentation](https://cassandra.apache.org/doc/latest/) + +(check-the-health-of-a-cassandra-node)= + +## Check the health of a Cassandra node + +To check the health of a Cassandra node, run the following command: + +```sh +ssh /opt/cassandra/bin/nodetool status +``` + +or if you are running a newer version of wire-server (altough it should be backwards compatibile) + +```sh +ssh /opt/cassandra/bin/nodetool -h ::FFFF:127.0.0.1 status +``` + +You should see a list of nodes like this: + +```sh +Datacenter: datacenter1 +======================= +Status=Up/Down +|/ State=Normal/Leaving/Joining/Moving +-- Address Load Tokens Owns (effective) Host ID Rack +UN 192.168.220.13 9.51MiB 256 100.0% 3dba71c8-eea7-4e35-8f35-4386e7944894 rack1 +UN 192.168.220.23 9.53MiB 256 100.0% 3af56f1f-7685-4b5b-b73f-efdaa371e96e rack1 +UN 192.168.220.33 9.55MiB 256 100.0% RANDOMLY-MADE-UUID-GOES-INTHISPLACE! rack1 +``` + +A `UN` at the begginng of the line, refers to a node that is `Up` and `Normal`. + +## How to inspect tables and data manually + +```sh +cqlsh +# from the cqlsh shell +describe keyspaces +use ; +describe tables; +select * from WHERE = LIMIT 10; +``` + +## How to rolling-restart a cassandra cluster + +For maintenance you may need to restart the cluster. + +On each server one by one: + +1. check your cluster is healthy: `nodetool status` or `nodetool -h ::FFFF:127.0.0.1 status` (in newer versions) +2. `nodetool drain && systemctl stop cassandra` (to stop accepting writes and flush data to disk; then stop the process) +3. do any operation you need, if any +4. Start the cassandra daemon process: `systemctl start cassandra` +5. Wait for your cluster to be healthy again. +6. Do the same on the next server. diff --git a/docs/src/how-to/administrate/cassandra.rst b/docs/src/how-to/administrate/cassandra.rst deleted file mode 100644 index 180a8f2a8c..0000000000 --- a/docs/src/how-to/administrate/cassandra.rst +++ /dev/null @@ -1,65 +0,0 @@ -Cassandra --------------------------- - -.. include:: includes/intro.rst - -This section only covers the bare minimum, for more information, see the `cassandra -documentation `__ - -Check the health of a Cassandra node -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To check the health of a Cassandra node, run the following command: - -.. code:: sh - - ssh /opt/cassandra/bin/nodetool status - -or if you are running a newer version of wire-server (altough it should be backwards compatibile) - -.. code:: sh - - ssh /opt/cassandra/bin/nodetool -h ::FFFF:127.0.0.1 status - -You should see a list of nodes like this: - -.. code:: sh - - Datacenter: datacenter1 - ======================= - Status=Up/Down - |/ State=Normal/Leaving/Joining/Moving - -- Address Load Tokens Owns (effective) Host ID Rack - UN 192.168.220.13 9.51MiB 256 100.0% 3dba71c8-eea7-4e35-8f35-4386e7944894 rack1 - UN 192.168.220.23 9.53MiB 256 100.0% 3af56f1f-7685-4b5b-b73f-efdaa371e96e rack1 - UN 192.168.220.33 9.55MiB 256 100.0% RANDOMLY-MADE-UUID-GOES-INTHISPLACE! rack1 - -A ``UN`` at the begginng of the line, refers to a node that is ``Up`` and ``Normal``. - -How to inspect tables and data manually -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code:: sh - - cqlsh - # from the cqlsh shell - describe keyspaces - use ; - describe tables; - select * from WHERE = LIMIT 10; - -How to rolling-restart a cassandra cluster -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -For maintenance you may need to restart the cluster. - -On each server one by one: - -1. check your cluster is healthy: ``nodetool status`` or ``nodetool -h ::FFFF:127.0.0.1 status`` (in newer versions) -2. ``nodetool drain && systemctl stop cassandra`` (to stop accepting writes and flush data to disk; then stop the process) -3. do any operation you need, if any -4. Start the cassandra daemon process: ``systemctl start cassandra`` -5. Wait for your cluster to be healthy again. -6. Do the same on the next server. - - diff --git a/docs/src/how-to/administrate/elasticsearch.md b/docs/src/how-to/administrate/elasticsearch.md new file mode 100644 index 0000000000..f128a0c1d6 --- /dev/null +++ b/docs/src/how-to/administrate/elasticsearch.md @@ -0,0 +1,127 @@ +# Elasticsearch + +```{eval-rst} +.. include:: includes/intro.rst +``` + +For more information, see the [elasticsearch +documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) + +(restart-elasticsearch)= + +## How to rolling-restart an elasticsearch cluster + +For maintenance you may need to restart the cluster. + +On each server one by one: + +1. check your cluster is healthy (see above) +2. stop shard allocation: + +```sh +ES_IP= +curl -sSf -XPUT http://localhost:9200/_cluster/settings -H 'Content-Type: application/json' -d "{ \"transient\" : {\"cluster.routing.allocation.exclude._ip\": \"$ES_IP\" }}"; echo; +``` + +You should expect some output like this: + +```sh +{"acknowledged":true,"persistent":{},"transient":{"cluster":{"routing":{"allocation":{"exclude":{"_ip":""}}}}}} +``` + +3. Stop the elasticsearch daemon process: `systemctl stop elasticsearch` +4. do any operation you need, if any +5. Start the elasticsearch daemon process: `systemctl start elasticsearch` +6. re-enable shard allocation: + +```sh +curl -sSf -XPUT http://localhost:9200/_cluster/settings -H 'Content-Type: application/json' -d "{ \"transient\" : {\"cluster.routing.allocation.exclude._ip\": null }}"; echo; +``` + +You should expect some output like this from the above command: + +```sh +{"acknowledged":true,"persistent":{},"transient":{}} +``` + +6. Wait for your cluster to be healthy again. +7. Do the same on the next server. + +## How to manually look into what is stored in elasticsearch + +See also the elasticsearch sections in {ref}`investigative-tasks`. + +(check-the-health-of-an-elasticsearch-node)= + +## Check the health of an elasticsearch node + +To check the health of an elasticsearch node, run the following command: + +```sh +ssh curl localhost:9200/_cat/health +``` + +You should see output looking like this: + +``` +1630250355 15:18:55 elasticsearch-directory green 3 3 17 6 0 0 0 - 100.0% +``` + +Here, the `green` denotes good node health, and the `3 3` denotes 3 running nodes. + +## Check cluster health + +This is the command to check the health of the entire cluster: + +```sh +ssh curl 'http://localhost:9200/_cluster/health?pretty' +``` + +## List cluster nodes + +This is the command to list the nodes in the cluster: + +```sh +ssh curl 'http://localhost:9200/_cat/nodes?v&h=id,ip,name' +``` + +## Troubleshooting + +Description: +**ES nodes ran out of disk space** and error message says: `"blocked by: [FORBIDDEN/12/index read-only / allow delete (api)];"` + +Solution: + +1. Connect to the node: + +```sh +ssh +``` + +2. Clean up disk (e.g. `apt autoremove` on all nodes), then restart machines and/or the elasticsearch process + +```sh +sudo apt autoremove +sudo reboot +``` + +As always make sure you {ref}`check the health of the process `. before and after the reboot. + +3. Get the elastichsearch cluster out of *read-only* mode, run: + +```sh +curl -X PUT -H 'Content-Type: application/json' http://localhost:9200/_all/_settings -d '{"index.blocks.read_only_allow_delete": null}' +``` + +4. Trigger reindexing: From a kubernetes machine, in one terminal: + +```sh +# The following depends on your namespace where you installed wire-server. By default the namespace is called 'wire'. +kubectl --namespace wire port-forward svc/brig 9999:8080 +``` + +And in a second terminal trigger the reindex: + +```sh +curl -v -X POST localhost:9999/i/index/reindex +``` diff --git a/docs/src/how-to/administrate/elasticsearch.rst b/docs/src/how-to/administrate/elasticsearch.rst deleted file mode 100644 index 3a101a7645..0000000000 --- a/docs/src/how-to/administrate/elasticsearch.rst +++ /dev/null @@ -1,134 +0,0 @@ -Elasticsearch ------------------------------- - -.. include:: includes/intro.rst - -For more information, see the `elasticsearch -documentation `__ - - -.. _restart-elasticsearch: - -How to rolling-restart an elasticsearch cluster -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -For maintenance you may need to restart the cluster. - -On each server one by one: - -1. check your cluster is healthy (see above) -2. stop shard allocation: - -.. code:: sh - - ES_IP= - curl -sSf -XPUT http://localhost:9200/_cluster/settings -H 'Content-Type: application/json' -d "{ \"transient\" : {\"cluster.routing.allocation.exclude._ip\": \"$ES_IP\" }}"; echo; - -You should expect some output like this: - -.. code:: sh - - {"acknowledged":true,"persistent":{},"transient":{"cluster":{"routing":{"allocation":{"exclude":{"_ip":""}}}}}} - -3. Stop the elasticsearch daemon process: ``systemctl stop elasticsearch`` -4. do any operation you need, if any -5. Start the elasticsearch daemon process: ``systemctl start elasticsearch`` -6. re-enable shard allocation: - -.. code:: sh - - curl -sSf -XPUT http://localhost:9200/_cluster/settings -H 'Content-Type: application/json' -d "{ \"transient\" : {\"cluster.routing.allocation.exclude._ip\": null }}"; echo; - -You should expect some output like this from the above command: - -.. code:: sh - - {"acknowledged":true,"persistent":{},"transient":{}} - -6. Wait for your cluster to be healthy again. -7. Do the same on the next server. - -How to manually look into what is stored in elasticsearch -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -See also the elasticsearch sections in :ref:`investigative_tasks`. - - -Check the health of an elasticsearch node -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To check the health of an elasticsearch node, run the following command: - -.. code:: sh - - ssh curl localhost:9200/_cat/health - -You should see output looking like this: - -.. code:: - - 1630250355 15:18:55 elasticsearch-directory green 3 3 17 6 0 0 0 - 100.0% - -Here, the ``green`` denotes good node health, and the ``3 3`` denotes 3 running nodes. - -Check cluster health -~~~~~~~~~~~~~~~~~~~~ - -This is the command to check the health of the entire cluster: - -.. code:: sh - - ssh curl 'http://localhost:9200/_cluster/health?pretty' - - -List cluster nodes -~~~~~~~~~~~~~~~~~~ - -This is the command to list the nodes in the cluster: - -.. code:: sh - - ssh curl 'http://localhost:9200/_cat/nodes?v&h=id,ip,name' - - -Troubleshooting -~~~~~~~~~~~~~~~ - -Description: -**ES nodes ran out of disk space** and error message says: ``"blocked by: [FORBIDDEN/12/index read-only / allow delete (api)];"`` - -Solution: - -1. Connect to the node: - -.. code:: sh - - ssh - -2. Clean up disk (e.g. ``apt autoremove`` on all nodes), then restart machines and/or the elasticsearch process - -.. code:: sh - - sudo apt autoremove - sudo reboot - -As always, and as explained in the `operations/procedures page `__, make sure you `check the health of the process `__. before and after the reboot. - -3. Get the elastichsearch cluster out of *read-only* mode, run: - -.. code:: sh - - curl -X PUT -H 'Content-Type: application/json' http://localhost:9200/_all/_settings -d '{"index.blocks.read_only_allow_delete": null}' - -4. Trigger reindexing: From a kubernetes machine, in one terminal: - -.. code:: sh - - # The following depends on your namespace where you installed wire-server. By default the namespace is called 'wire'. - kubectl --namespace wire port-forward svc/brig 9999:8080 - -And in a second terminal trigger the reindex: - -.. code:: sh - - curl -v -X POST localhost:9999/i/index/reindex diff --git a/docs/src/how-to/administrate/etcd.md b/docs/src/how-to/administrate/etcd.md new file mode 100644 index 0000000000..a18c801f87 --- /dev/null +++ b/docs/src/how-to/administrate/etcd.md @@ -0,0 +1,261 @@ +# Etcd + +```{eval-rst} +.. include:: includes/intro.rst +``` + +This section only covers the bare minimum, for more information, see the [etcd documentation](https://etcd.io/) + +(how-to-see-cluster-health)= + +## How to see cluster health + +If the file `/usr/local/bin/etcd-health.sh` is available, you can run + +```sh +etcd-health.sh +``` + +which should produce an output similar to: + +``` +Cluster-Endpoints: https://127.0.0.1:2379 +cURL Command: curl -X GET https://127.0.0.1:2379/v2/members +member 7c37f7dc10558fae is healthy: got healthy result from https://10.10.1.11:2379 +member cca4e6f315097b3b is healthy: got healthy result from https://10.10.1.10:2379 +member e767162297c84b1e is healthy: got healthy result from https://10.10.1.12:2379 +cluster is healthy +``` + +If that helper file is not available, create it with the following contents: + +```bash +#!/usr/bin/env bash + +HOST=$(hostname) + +etcdctl --endpoints https://127.0.0.1:2379 --ca-file=/etc/ssl/etcd/ssl/ca.pem --cert-file=/etc/ssl/etcd/ssl/member-$HOST.pem --key-file=/etc/ssl/etcd/ssl/member-$HOST-key.pem --debug cluster-health +``` + +and then make it executable: `chmod +x /usr/local/bin/etcd-health.sh` + +## How to inspect tables and data manually + +```sh +TODO +``` + +(how-to-rolling-restart-an-etcd-cluster)= + +## How to rolling-restart an etcd cluster + +Etcd is a consistent and partition tolerant key-value store. This means that +Etcd nodes can be restarted (one by one) with no impact to the consistency of +data, but there might a small time in which the database can not process +writes. Etcd has a designated leader which decides ordering of events (and thus +writes) in the cluster. When the leader crashes, a leadership election takes +place. During the leadership election, the cluster might be briefly +unavailable for writes. Writes during this period are queued up until a new +leader is elected. Any writes that were happening during the crash of the +leader that were not acknowledged by the leader and the followers yet will be +'lost'. The client that performed this write will experience this as a write +timeout. (Source: ). Client +applications (like kubernetes) are expected to deal with this failure scenario +gracefully. + +Etcd can be restarted in a rolling fashion, by cleanly shutting down and +starting up etcd servers one by one. In Etcd 3.1 and up, when the leader is +cleanly shut down, it will hand over leadership gracefully to another node, +which will minimize the impact of write-availability as election time is +reduced. (Source : +) +Restarting follower nodes has no impact to availability. + +Etcd does load-balancing between servrvers on the client-side. This means that +if a server you were talking to is being restarted, etcd will transparently +redirect the request to another server. It's is thus safe to shut them down at +any point. + +Now to perform a rolling restart of the cluster, do the following steps: + +1. Check your cluster is healthy (see above) +2. Stop the process with `systemctl stop etcd` (this should be safe since etcd clients retry their operation if one endpoint becomes unavailable, see [this page](https://etcd.io/docs/v3.3.12/learning/client-architecture/)) +3. Do any operation you need, if any. Like rebooting +4. `systemctl start etcd` +5. Wait for your cluster to be healthy again. +6. Do the same on the next server. + +*For more details please refer to the official documentation:* [Replacing a failed etcd member](https://kubernetes.io/docs/tasks/administer-cluster/configure-upgrade-etcd/#replacing-a-failed-etcd-member) + +(etcd-backup-and-restore)= + +## Backing up and restoring + +Though as long as quorum is maintained in etcd there will be no dataloss, it is +still good to prepare for the worst. If a disaster takes out too many nodes, then +you might have to restore from an old backup. + +Luckily, etcd can take periodic snapshots of your cluster and these can be used +in cases of disaster recovery. Information about how to do snapshots and +restores can be found here: + + +*For more details please refer to the official documentation:* [Backing up an etcd cluster](https://kubernetes.io/docs/tasks/administer-cluster/configure-upgrade-etcd/#backing-up-an-etcd-cluster) + +## Troubleshooting + +### How to recover from a single unhealthy etcd node after virtual machine snapshot restore + +After restoring an etcd machine from an earlier snapshot of the machine disk, etcd members may become unable to join. + +Symptoms: That etcd process is unable to start and crashes, and other etcd nodes can't reach it: + +``` +failed to check the health of member e767162297c84b1e on https://10.10.1.12:2379: Get https://10.10.1.12:2379/health: dial tcp 10.10.1.12:2379: getsockopt: connection refused +member e767162297c84b1e is unreachable: [https://10.10.1.12:2379] are all unreachable +``` + +Logs from the crashing etcd: + +``` +(...) +Sep 25 09:27:05 node2 etcd[20288]: 2019-09-25 07:27:05.691409 I | raft: e767162297c84b1e [term: 28] received a MsgHeartbeat message with higher term from cca4e6f315097b3b [term: 30] +Sep 25 09:27:05 node2 etcd[20288]: 2019-09-25 07:27:05.691620 I | raft: e767162297c84b1e became follower at term 30 +Sep 25 09:27:05 node2 etcd[20288]: 2019-09-25 07:27:05.692423 C | raft: tocommit(16152654) is out of range [lastIndex(16061986)]. Was the raft log corrupted, truncated, or lost? +Sep 25 09:27:05 node2 etcd[20288]: panic: tocommit(16152654) is out of range [lastIndex(16061986)]. Was the raft log corrupted, truncated, or lost? +Sep 25 09:27:05 node2 etcd[20288]: goroutine 90 [running]: +(...) +``` + +Etcd will refuse nodes that run behind to join the cluster. If a node has +committed to a certain version of the raft log, it is expected not to jump back +in time after that. In this scenario, we turned an etcd server off, made a +snapshot of the virtual machine, brought it back online, and then restored the +snapshot. What went wrong is is that if you bring up a VM snapshot, it means +the etcd node will now have an older raft log than it had before; even though +it already gossiped to all other nodes that it has knowledge of newer entries. + +As a safety precaution, the other nodes will reject the node that is travelling +back in time, to avoid data corruption. A node could get corrupted for other +reasons as well. Perhaps a disk is faulty and is serving wrong data. Either +way, if you end up in a scenario where a node is unhealthy and will refuse to +rejoin the cluster, it is time to do some operations to get the cluster back in +a healthy state. + +It is not recommended to restore an etcd node from a vm snapshot, as that will +cause these kind of time-travelling behaviours which will make the node +unhealthy. To recover from this situation anyway, +I quote from the etcdv2 admin guide + +> If a member’s data directory is ever lost or corrupted then the user should +> remove the etcd member from the cluster using etcdctl tool. A user should +> avoid restarting an etcd member with a data directory from an out-of-date +> backup. Using an out-of-date data directory can lead to inconsistency as the +> member had agreed to store information via raft then re-joins saying it +> needs that information again. For maximum safety, if an etcd member suffers +> any sort of data corruption or loss, it must be removed from the cluster. +> Once removed the member can be re-added with an empty data directory. + +Note that this piece of documentation is from etcdv2 and not etcdv3. However +the etcdv3 docs describe a similar procedure here + + +The procedure to remove and add a member is documented here: + + +It is also documented in the kubernetes documentation: + + +So following the above guides step by step, we can recover our cluster to be +healthy again. + +First let us make sure our broken member is stopped by runnning this on `node`: + +```sh +systemctl stop etcd +``` + +Now from a healthy node, e.g. `node0` remove the broken node + +```sh +etcdctl3.sh member remove e767162297c84b1e +``` + +And we expect the output to be something like + +```sh +Member e767162297c84b1e removed from cluster 432c10551aa096af +``` + +By removing the member from the cluster, you signal the other nodes to not +expect it to come back with the right state. It will be considered dead and +removed from the peers. This will allow the node to come up with an empty data +directory and it not getting kicked out of the cluster. The cluster should now +be healthy, but only have 2 members, and so it is not to resistent to crashes +at the moment! As we can see if we run the health check from a healthy node. + +```sh +etcd-health.sh +``` + +And we expect only two nodes to be in the cluster: + +``` +Cluster-Endpoints: https://127.0.0.1:2379 +cURL Command: curl -X GET https://127.0.0.1:2379/v2/members +member 7c37f7dc10558fae is healthy: got healthy result from https://10.10.1.11:2379 +member cca4e6f315097b3b is healthy: got healthy result from https://10.10.1.10:2379 +cluster is healthy +``` + +Now from a healthy node, re-add the node you just removed. Make sure +to replace the IP in the snippet below with the IP of the node you just removed. + +```sh +etcdctl3.sh member add etcd_2 --peer-urls https://10.10.1.12:2380 +``` + +And it should report that it has been added: + +``` +Member e13b1d076b2f9344 added to cluster 432c10551aa096af + +ETCD_NAME="etcd_2" +ETCD_INITIAL_CLUSTER="etcd_1=https://10.10.1.11:2380,etcd_0=https://10.10.1.10:2380,etcd_2=https://10.10.1.12:2380" +ETCD_INITIAL_CLUSTER_STATE="existing" +``` + +it should now be in the list as "unstarted" instead of it not being in the list at all. + +```sh +etcdctl3.sh member list + + +7c37f7dc10558fae, started, etcd_1, https://10.10.1.11:2380, https://10.10.1.11:2379 +cca4e6f315097b3b, started, etcd_0, https://10.10.1.10:2380, https://10.10.1.10:2379 +e13b1d076b2f9344, unstarted, , https://10.10.1.12:2380, +``` + +Now on the broken node, remove the on-disk state, which was corrupted, and start etcd + +```sh +mv /var/lib/etcd /var/lib/etcd.bak +sudo systemctl start etcd +``` + +If we run the health check now, the cluster should report its healthy now again. + +```sh +etcd-health.sh +``` + +And indeed it outputs so: + +``` +Cluster-Endpoints: https://127.0.0.1:2379 +cURL Command: curl -X GET https://127.0.0.1:2379/v2/members +member 7c37f7dc10558fae is healthy: got healthy result from https://10.10.1.11:2379 +member cca4e6f315097b3b is healthy: got healthy result from https://10.10.1.10:2379 +member e13b1d076b2f9344 is healthy: got healthy result from https://10.10.1.12:2379 +cluster is healthy +``` diff --git a/docs/src/how-to/administrate/etcd.rst b/docs/src/how-to/administrate/etcd.rst deleted file mode 100644 index 47bce63d70..0000000000 --- a/docs/src/how-to/administrate/etcd.rst +++ /dev/null @@ -1,264 +0,0 @@ -Etcd --------------------------- - -.. include:: includes/intro.rst - -This section only covers the bare minimum, for more information, see the `etcd documentation `__ - -How to see cluster health -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If the file `/usr/local/bin/etcd-health.sh` is available, you can run - -.. code:: sh - - etcd-health.sh - -which should produce an output similar to:: - - Cluster-Endpoints: https://127.0.0.1:2379 - cURL Command: curl -X GET https://127.0.0.1:2379/v2/members - member 7c37f7dc10558fae is healthy: got healthy result from https://10.10.1.11:2379 - member cca4e6f315097b3b is healthy: got healthy result from https://10.10.1.10:2379 - member e767162297c84b1e is healthy: got healthy result from https://10.10.1.12:2379 - cluster is healthy - -If that helper file is not available, create it with the following contents: - -.. code:: bash - - #!/usr/bin/env bash - - HOST=$(hostname) - - etcdctl --endpoints https://127.0.0.1:2379 --ca-file=/etc/ssl/etcd/ssl/ca.pem --cert-file=/etc/ssl/etcd/ssl/member-$HOST.pem --key-file=/etc/ssl/etcd/ssl/member-$HOST-key.pem --debug cluster-health - -and then make it executable: ``chmod +x /usr/local/bin/etcd-health.sh`` - -How to inspect tables and data manually -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code:: sh - - TODO - - -.. _how-to-rolling-restart-an-etcd-cluster: - -How to rolling-restart an etcd cluster -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Etcd is a consistent and partition tolerant key-value store. This means that -Etcd nodes can be restarted (one by one) with no impact to the consistency of -data, but there might a small time in which the database can not process -writes. Etcd has a designated leader which decides ordering of events (and thus -writes) in the cluster. When the leader crashes, a leadership election takes -place. During the leadership election, the cluster might be briefly -unavailable for writes. Writes during this period are queued up until a new -leader is elected. Any writes that were happening during the crash of the -leader that were not acknowledged by the leader and the followers yet will be -'lost'. The client that performed this write will experience this as a write -timeout. (Source: https://etcd.io/docs/v3.4.0/op-guide/failures/). Client -applications (like kubernetes) are expected to deal with this failure scenario -gracefully. - -Etcd can be restarted in a rolling fashion, by cleanly shutting down and -starting up etcd servers one by one. In Etcd 3.1 and up, when the leader is -cleanly shut down, it will hand over leadership gracefully to another node, -which will minimize the impact of write-availability as election time is -reduced. (Source : -https://kubernetes.io/blog/2018/12/11/etcd-current-status-and-future-roadmap/) -Restarting follower nodes has no impact to availability. - -Etcd does load-balancing between servrvers on the client-side. This means that -if a server you were talking to is being restarted, etcd will transparently -redirect the request to another server. It's is thus safe to shut them down at -any point. - -Now to perform a rolling restart of the cluster, do the following steps: - -1. Check your cluster is healthy (see above) -2. Stop the process with ``systemctl stop etcd`` (this should be safe since etcd clients retry their operation if one endpoint becomes unavailable, see `this page `__) -3. Do any operation you need, if any. Like rebooting -4. ``systemctl start etcd`` -5. Wait for your cluster to be healthy again. -6. Do the same on the next server. - -*For more details please refer to the official documentation:* `Replacing a failed etcd member `__ - - -.. _etcd_backup-and-restore: - -Backing up and restoring -~~~~~~~~~~~~~~~~~~~~~~~~~ -Though as long as quorum is maintained in etcd there will be no dataloss, it is -still good to prepare for the worst. If a disaster takes out too many nodes, then -you might have to restore from an old backup. - -Luckily, etcd can take periodic snapshots of your cluster and these can be used -in cases of disaster recovery. Information about how to do snapshots and -restores can be found here: -https://github.com/etcd-io/etcd/blob/master/Documentation/op-guide/recovery.md - -*For more details please refer to the official documentation:* `Backing up an etcd cluster `__ - - -Troubleshooting -~~~~~~~~~~~~~~~~~~~~~~~~~~ - - -How to recover from a single unhealthy etcd node after virtual machine snapshot restore -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -After restoring an etcd machine from an earlier snapshot of the machine disk, etcd members may become unable to join. - -Symptoms: That etcd process is unable to start and crashes, and other etcd nodes can't reach it:: - - failed to check the health of member e767162297c84b1e on https://10.10.1.12:2379: Get https://10.10.1.12:2379/health: dial tcp 10.10.1.12:2379: getsockopt: connection refused - member e767162297c84b1e is unreachable: [https://10.10.1.12:2379] are all unreachable - -Logs from the crashing etcd:: - - (...) - Sep 25 09:27:05 node2 etcd[20288]: 2019-09-25 07:27:05.691409 I | raft: e767162297c84b1e [term: 28] received a MsgHeartbeat message with higher term from cca4e6f315097b3b [term: 30] - Sep 25 09:27:05 node2 etcd[20288]: 2019-09-25 07:27:05.691620 I | raft: e767162297c84b1e became follower at term 30 - Sep 25 09:27:05 node2 etcd[20288]: 2019-09-25 07:27:05.692423 C | raft: tocommit(16152654) is out of range [lastIndex(16061986)]. Was the raft log corrupted, truncated, or lost? - Sep 25 09:27:05 node2 etcd[20288]: panic: tocommit(16152654) is out of range [lastIndex(16061986)]. Was the raft log corrupted, truncated, or lost? - Sep 25 09:27:05 node2 etcd[20288]: goroutine 90 [running]: - (...) - - -Etcd will refuse nodes that run behind to join the cluster. If a node has -committed to a certain version of the raft log, it is expected not to jump back -in time after that. In this scenario, we turned an etcd server off, made a -snapshot of the virtual machine, brought it back online, and then restored the -snapshot. What went wrong is is that if you bring up a VM snapshot, it means -the etcd node will now have an older raft log than it had before; even though -it already gossiped to all other nodes that it has knowledge of newer entries. - -As a safety precaution, the other nodes will reject the node that is travelling -back in time, to avoid data corruption. A node could get corrupted for other -reasons as well. Perhaps a disk is faulty and is serving wrong data. Either -way, if you end up in a scenario where a node is unhealthy and will refuse to -rejoin the cluster, it is time to do some operations to get the cluster back in -a healthy state. - -It is not recommended to restore an etcd node from a vm snapshot, as that will -cause these kind of time-travelling behaviours which will make the node -unhealthy. To recover from this situation anyway, -I quote from the etcdv2 admin guide https://github.com/etcd-io/etcd/blob/master/Documentation/v2/admin_guide.md - - If a member’s data directory is ever lost or corrupted then the user should - remove the etcd member from the cluster using etcdctl tool. A user should - avoid restarting an etcd member with a data directory from an out-of-date - backup. Using an out-of-date data directory can lead to inconsistency as the - member had agreed to store information via raft then re-joins saying it - needs that information again. For maximum safety, if an etcd member suffers - any sort of data corruption or loss, it must be removed from the cluster. - Once removed the member can be re-added with an empty data directory. - - -Note that this piece of documentation is from etcdv2 and not etcdv3. However -the etcdv3 docs describe a similar procedure here -https://github.com/etcd-io/etcd/blob/master/Documentation/op-guide/runtime-configuration.md#replace-a-failed-machine - - -The procedure to remove and add a member is documented here: -https://github.com/etcd-io/etcd/blob/master/Documentation/op-guide/runtime-configuration.md#remove-a-member - -It is also documented in the kubernetes documentation: -https://kubernetes.io/docs/tasks/administer-cluster/configure-upgrade-etcd/#replacing-a-failed-etcd-member - -So following the above guides step by step, we can recover our cluster to be -healthy again. - -First let us make sure our broken member is stopped by runnning this on ``node``: - -.. code:: sh - - systemctl stop etcd - -Now from a healthy node, e.g. ``node0`` remove the broken node - -.. code:: sh - - etcdctl3.sh member remove e767162297c84b1e - -And we expect the output to be something like - -.. code:: sh - - Member e767162297c84b1e removed from cluster 432c10551aa096af - - -By removing the member from the cluster, you signal the other nodes to not -expect it to come back with the right state. It will be considered dead and -removed from the peers. This will allow the node to come up with an empty data -directory and it not getting kicked out of the cluster. The cluster should now -be healthy, but only have 2 members, and so it is not to resistent to crashes -at the moment! As we can see if we run the health check from a healthy node. - -.. code:: sh - - etcd-health.sh - -And we expect only two nodes to be in the cluster:: - - Cluster-Endpoints: https://127.0.0.1:2379 - cURL Command: curl -X GET https://127.0.0.1:2379/v2/members - member 7c37f7dc10558fae is healthy: got healthy result from https://10.10.1.11:2379 - member cca4e6f315097b3b is healthy: got healthy result from https://10.10.1.10:2379 - cluster is healthy - -Now from a healthy node, re-add the node you just removed. Make sure -to replace the IP in the snippet below with the IP of the node you just removed. - -.. code:: sh - - etcdctl3.sh member add etcd_2 --peer-urls https://10.10.1.12:2380 - -And it should report that it has been added:: - - Member e13b1d076b2f9344 added to cluster 432c10551aa096af - - ETCD_NAME="etcd_2" - ETCD_INITIAL_CLUSTER="etcd_1=https://10.10.1.11:2380,etcd_0=https://10.10.1.10:2380,etcd_2=https://10.10.1.12:2380" - ETCD_INITIAL_CLUSTER_STATE="existing" - - -it should now be in the list as "unstarted" instead of it not being in the list at all. - -.. code:: sh - - etcdctl3.sh member list - - - 7c37f7dc10558fae, started, etcd_1, https://10.10.1.11:2380, https://10.10.1.11:2379 - cca4e6f315097b3b, started, etcd_0, https://10.10.1.10:2380, https://10.10.1.10:2379 - e13b1d076b2f9344, unstarted, , https://10.10.1.12:2380, - - -Now on the broken node, remove the on-disk state, which was corrupted, and start etcd - -.. code:: sh - - mv /var/lib/etcd /var/lib/etcd.bak - sudo systemctl start etcd - -If we run the health check now, the cluster should report its healthy now again. - -.. code:: sh - - etcd-health.sh - -And indeed it outputs so:: - - Cluster-Endpoints: https://127.0.0.1:2379 - cURL Command: curl -X GET https://127.0.0.1:2379/v2/members - member 7c37f7dc10558fae is healthy: got healthy result from https://10.10.1.11:2379 - member cca4e6f315097b3b is healthy: got healthy result from https://10.10.1.10:2379 - member e13b1d076b2f9344 is healthy: got healthy result from https://10.10.1.12:2379 - cluster is healthy - - - diff --git a/docs/src/how-to/administrate/general-linux.md b/docs/src/how-to/administrate/general-linux.md new file mode 100644 index 0000000000..e0f6b694fe --- /dev/null +++ b/docs/src/how-to/administrate/general-linux.md @@ -0,0 +1,67 @@ +# General - Linux + +```{eval-rst} +.. include:: includes/intro.rst +``` + +## Which ports and network interface is my process running on? + +The following shows open TCP ports, and the related processes. + +```sh +sudo netstat -antlp | grep LISTEN +``` + +which may yield output like this: + +```sh +tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1536/sshd +``` + +(how-to-see-tls-certs)= + +## How can I see if my TLS certificates are configured the way I expect? + +```{note} +The following assumes you're querying a server from outside (e.g. your laptop). See the next section if operating on a server from an SSH session. +``` + +You can use openssl to check, with e.g. + +```sh +DOMAIN=example.com +PORT=443 +echo Q | openssl s_client -showcerts -connect $DOMAIN:$PORT +``` + +or + +```sh +DOMAIN=example.com +PORT=443 +echo Q | openssl s_client -showcerts -connect $DOMAIN:$PORT 2>/dev/null | openssl x509 -inform pem -noout -text +``` + +To see only the validity (expiration): + +```sh +DOMAIN=example.com +PORT=443 +echo Q | openssl s_client -showcerts -connect $DOMAIN:$PORT 2>/dev/null | openssl x509 -inform pem -noout -text | grep Validity -A 2 +``` + +## How can I see if my TLS certificates are configured the way I expect (special case kubernetes from a kubernetes machine) + +When you first SSH to a kubernetes node, depending on the setup, DNS may not resolve, in which case you can use the `-servername` parameter: + +```sh +# the IP of the network interface that kubernetes is listening on. 127.0.0.1 may or may not work depending on the installation. It's one of those from +# ifconfig | grep "inet addr" +IP=1.2.3.4 +# PORT can be 443 or 31773, depending on the installation +PORT=443 +# not the root domain, but one of the 5 subdomains for which kubernetes is serving traffic +DOMAIN=app.example.com + +echo Q | openssl s_client -showcerts -servername $DOMAIN -connect $IP:$PORT 2>/dev/null | openssl x509 -inform pem -noout -text | grep Validity -A 2 +``` diff --git a/docs/src/how-to/administrate/general-linux.rst b/docs/src/how-to/administrate/general-linux.rst deleted file mode 100644 index a2c8d81d1d..0000000000 --- a/docs/src/how-to/administrate/general-linux.rst +++ /dev/null @@ -1,69 +0,0 @@ -General - Linux --------------------------- - -.. include:: includes/intro.rst - -Which ports and network interface is my process running on? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The following shows open TCP ports, and the related processes. - -.. code:: sh - - sudo netstat -antlp | grep LISTEN - -which may yield output like this: - -.. code:: sh - - tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1536/sshd - -.. _how-to-see-tls-certs: - -How can I see if my TLS certificates are configured the way I expect? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. note:: - The following assumes you're querying a server from outside (e.g. your laptop). See the next section if operating on a server from an SSH session. - -You can use openssl to check, with e.g. - -.. code:: sh - - DOMAIN=example.com - PORT=443 - echo Q | openssl s_client -showcerts -connect $DOMAIN:$PORT - -or - -.. code:: sh - - DOMAIN=example.com - PORT=443 - echo Q | openssl s_client -showcerts -connect $DOMAIN:$PORT 2>/dev/null | openssl x509 -inform pem -noout -text - -To see only the validity (expiration): - -.. code:: sh - - DOMAIN=example.com - PORT=443 - echo Q | openssl s_client -showcerts -connect $DOMAIN:$PORT 2>/dev/null | openssl x509 -inform pem -noout -text | grep Validity -A 2 - - -How can I see if my TLS certificates are configured the way I expect (special case kubernetes from a kubernetes machine) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When you first SSH to a kubernetes node, depending on the setup, DNS may not resolve, in which case you can use the ``-servername`` parameter: - -.. code:: sh - - # the IP of the network interface that kubernetes is listening on. 127.0.0.1 may or may not work depending on the installation. It's one of those from - # ifconfig | grep "inet addr" - IP=1.2.3.4 - # PORT can be 443 or 31773, depending on the installation - PORT=443 - # not the root domain, but one of the 5 subdomains for which kubernetes is serving traffic - DOMAIN=app.example.com - - echo Q | openssl s_client -showcerts -servername $DOMAIN -connect $IP:$PORT 2>/dev/null | openssl x509 -inform pem -noout -text | grep Validity -A 2 diff --git a/docs/src/how-to/administrate/index.md b/docs/src/how-to/administrate/index.md new file mode 100644 index 0000000000..5f6dd1ab72 --- /dev/null +++ b/docs/src/how-to/administrate/index.md @@ -0,0 +1,12 @@ +# Administrate components after successful installation + +```{toctree} +:glob: true +:maxdepth: 2 + +Kubernetes + +* +``` + +% TODO: .. include:: administration/redis.rst diff --git a/docs/src/how-to/administrate/index.rst b/docs/src/how-to/administrate/index.rst deleted file mode 100644 index 5995a82a3c..0000000000 --- a/docs/src/how-to/administrate/index.rst +++ /dev/null @@ -1,14 +0,0 @@ -Administrate components after successful installation -===================================================== - -.. toctree:: - :maxdepth: 2 - :glob: - - Kubernetes - - * - -.. - TODO: .. include:: administration/redis.rst - diff --git a/docs/src/how-to/administrate/kubernetes/certificate-renewal/index.md b/docs/src/how-to/administrate/kubernetes/certificate-renewal/index.md new file mode 100644 index 0000000000..ae9323d55f --- /dev/null +++ b/docs/src/how-to/administrate/kubernetes/certificate-renewal/index.md @@ -0,0 +1,10 @@ +# Certificate renewal + +```{toctree} +:glob: true +:maxdepth: 1 + +* +``` + +% diff --git a/docs/src/how-to/administrate/kubernetes/certificate-renewal/index.rst b/docs/src/how-to/administrate/kubernetes/certificate-renewal/index.rst deleted file mode 100644 index b782d3b107..0000000000 --- a/docs/src/how-to/administrate/kubernetes/certificate-renewal/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -Certificate renewal -=================== - -.. toctree:: - :maxdepth: 1 - :glob: - - * - -.. \ No newline at end of file diff --git a/docs/src/how-to/administrate/kubernetes/certificate-renewal/scenario-1_k8s-v1.14-kubespray.md b/docs/src/how-to/administrate/kubernetes/certificate-renewal/scenario-1_k8s-v1.14-kubespray.md new file mode 100644 index 0000000000..316b644cd0 --- /dev/null +++ b/docs/src/how-to/administrate/kubernetes/certificate-renewal/scenario-1_k8s-v1.14-kubespray.md @@ -0,0 +1,241 @@ +# How to renew certificates on kubernetes 1.14.x + +Kubernetes-internal certificates by default (see assumptions) expire after one year. Without renewal, your installation will cease to function. +This page explains how to renew certificates. + +## Assumptions + +- Kubernetes version 1.14.x + +- installed with the help of [Kubespray](https://github.com/kubernetes-sigs/kubespray) + + - This page was tested using kubespray release 2.10 branch from 2019-05-20, i.e. commit `e2f5a9748e4dbfe2fdba7931198b0b5f1f4bdc7e`. + +- setup: 3 scheduled nodes, each hosting master (control plane) + + worker (kubelet) + etcd (cluster state, key-value database) + +*NOTE: due to Kubernetes being installed with Kubespray, the Kubernetes +CAs (expire after 10yr) as well as certificates involved in etcd +communication (expire after 100yr) are not required to be renewed (any +time soon).* + +**Official documentation:** + +- [Certificate Management with kubeadm (v1.14)](https://v1-14.docs.kubernetes.io/docs/tasks/administer-cluster/kubeadm/kubeadm-certs/) +- [PKI certificates and requirements (v1.14)](https://v1-14.docs.kubernetes.io/docs/setup/best-practices/certificates/) + +## High-level description + +1. verify current expiration date +2. issue new certificates +3. generate new client configuration (aka kubeconfig file) +4. restart control plane +5. drain node - restart kubelet - uncordon node again +6. repeat 3-5 on all other nodes + +## Step-by-step instructions + +*Please note, that the following instructions may require privileged +execution. So, either switch to a privileged user or prepend following +statements with \`\`sudo\`\`. In any case, it is most likely that every +newly created file has to be owned by \`\`root\`\`, depending on kow +Kubernetes was installed.* + +1. Verify current expiration date on each node + +```bash +export K8S_CERT_DIR=/etc/kubernetes/pki +export ETCD_CERT_DIR=/etc/ssl/etcd/ssl +export KUBELET_CERT_DIR=/var/lib/kubelet/pki + + +for crt in ${K8S_CERT_DIR}/*.crt; do + expirationDate=$(openssl x509 -noout -text -in ${crt} | grep After | sed -e 's/^[[:space:]]*//') + echo "$(basename ${crt}) -- ${expirationDate}" +done + + +for crt in $(ls ${ETCD_CERT_DIR}/*.pem | grep -v 'key'); do + expirationDate=$(openssl x509 -noout -text -in ${crt} | grep After | sed -e 's/^[[:space:]]*//') + echo "$(basename ${crt}) -- ${expirationDate}" +done + +echo "kubelet-client-current.pem -- $(openssl x509 -noout -text -in ${KUBELET_CERT_DIR}/kubelet-client-current.pem | grep After | sed -e 's/^[[:space:]]*//')" +echo "kubelet.crt -- $(openssl x509 -noout -text -in ${KUBELET_CERT_DIR}/kubelet.crt | grep After | sed -e 's/^[[:space:]]*//')" + + +# MASTER: api-server cert +echo -n | openssl s_client -connect localhost:6443 2>&1 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | openssl x509 -text -noout | grep Not +# MASTER: controller-manager cert +echo -n | openssl s_client -connect localhost:10257 2>&1 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | openssl x509 -text -noout | grep Not +# MASTER: scheduler cert +echo -n | openssl s_client -connect localhost:10259 2>&1 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | openssl x509 -text -noout | grep Not + +# WORKER: kubelet cert +echo -n | openssl s_client -connect localhost:10250 2>&1 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | openssl x509 -text -noout | grep Not +``` + +2. Allocate a terminal session on one node and backup existing + certificates & configurations + +```bash +cd /etc/kubernetes + +cp -r ./ssl ./ssl.bkp + +cp admin.conf admin.conf.bkp +cp controller-manager.conf controller-manager.conf.bkp +cp scheduler.conf scheduler.conf.bkp +cp kubelet.conf kubelet.conf.bkp +``` + +3. Renew certificates on that very node + +```bash +kubeadm alpha certs renew apiserver +kubeadm alpha certs renew apiserver-kubelet-client +kubeadm alpha certs renew front-proxy-client +``` + +*Looking at the timestamps of the certificates, it is indicated, that apicerver, kubelet & proxy-client have been +renewed. This can be confirmed, by executing parts of (1).* + +``` +root@kubenode01:/etc/kubernetes$ ls -al ./ssl +total 56 +drwxr-xr-x 2 kube root 4096 Mar 20 17:09 . +drwxr-xr-x 5 kube root 4096 Mar 20 17:08 .. +-rw-r--r-- 1 root root 1517 Mar 20 15:12 apiserver.crt +-rw------- 1 root root 1675 Mar 20 15:12 apiserver.key +-rw-r--r-- 1 root root 1099 Mar 20 15:13 apiserver-kubelet-client.crt +-rw------- 1 root root 1675 Mar 20 15:13 apiserver-kubelet-client.key +-rw-r--r-- 1 root root 1025 Sep 23 14:53 ca.crt +-rw------- 1 root root 1679 Sep 23 14:53 ca.key +-rw-r--r-- 1 root root 1038 Sep 23 14:53 front-proxy-ca.crt +-rw------- 1 root root 1679 Sep 23 14:53 front-proxy-ca.key +-rw-r--r-- 1 root root 1058 Mar 20 15:13 front-proxy-client.crt +-rw------- 1 root root 1675 Mar 20 15:13 front-proxy-client.key +-rw------- 1 root root 1679 Sep 23 14:53 sa.key +-rw------- 1 root root 451 Sep 23 14:53 sa.pub +``` + +4. Based on those renewed certificates, generate new kubeconfig files + +The first command assumes it's being executed on a master node. You may need to swap `masters` with `nodes` in +case you are on a different sort of machines. + +```bash +kubeadm alpha kubeconfig user --org system:masters --client-name kubernetes-admin > /etc/kubernetes/admin.conf +kubeadm alpha kubeconfig user --client-name system:kube-controller-manager > /etc/kubernetes/controller-manager.conf +kubeadm alpha kubeconfig user --client-name system:kube-scheduler > /etc/kubernetes/scheduler.conf +``` + +*Again, check if ownership and permission for these files are the same +as all the others around them.* + +And, in case you are operating the cluster from the current node, you may want to replace the user's kubeconfig. +Afterwards, compare the backup version with the new one, to see if any configuration (e.g. pre-configured *namespace*) +might need to be moved over, too. + +```bash +mv ~/.kube/config ~/.kube/config.bkp +cp /etc/kubernetes/admin.conf ~/.kube/config +chown $(id -u):$(id -g) ~/.kube/config +chmod 770 ~/.kube/config +``` + +5. Now that certificates and configuration files are in place, the + control plane must be restarted. They typically run in containers, so + the easiest way to trigger a restart, is to kill the processes + running in there. Use (1) to verify, that the expiration dates indeed + have been changed. + +```bash +kill -s SIGHUP $(pidof kube-apiserver) +kill -s SIGHUP $(pidof kube-controller-manager) +kill -s SIGHUP $(pidof kube-scheduler) +``` + +6. Make *kubelet* aware of the new certificate + +1) Drain the node + +``` +kubectl drain --delete-local-data --ignore-daemonsets $(hostname) +``` + +2. Stop the kubelet process + +``` +systemctl stop kubelet +``` + +3. Remove old certificates and configuration + +``` +mv /var/lib/kubelet/pki{,old} +mkdir /var/lib/kubelet/pki +``` + +4. Generate new kubeconfig file for the kubelet + +``` +kubeadm alpha kubeconfig user --org system:nodes --client-name system:node:$(hostname) > /etc/kubernetes/kubelet.conf +``` + +5. Start kubelet again + +``` +systemctl start kubelet +``` + +6. \[Optional\] Verify kubelet has recognized certificate rotation + +``` +sleep 5 && systemctl status kubelet +``` + +7. Allow workload to be scheduled again on the node + +``` +kubectl uncordon $(hostname) +``` + +7. Copy certificates over to all the other nodes + +Option A - you can ssh from one kubernetes node to another + +```bash +# set the ip or hostname: +export NODE2=root@ip-or-hostname +export NODE3=... + +scp ./ssl/apiserver.* "${NODE2}:/etc/kubernetes/ssl/" +scp ./ssl/apiserver.* "${NODE3}:/etc/kubernetes/ssl/" + +scp ./ssl/apiserver-kubelet-client.* "${NODE2}:/etc/kubernetes/ssl/" +scp ./ssl/apiserver-kubelet-client.* "${NODE3}:/etc/kubernetes/ssl/" + +scp ./ssl/front-proxy-client.* "${NODE2}:/etc/kubernetes/ssl/" +scp ./ssl/front-proxy-client.* "${NODE3}:/etc/kubernetes/ssl/" +``` + +Option B - copy via local administrator's machine + +```bash +# set the ip or hostname: +export NODE1=root@ip-or-hostname +export NODE2= +export NODE3= + +scp -3 "${NODE1}:/etc/kubernetes/ssl/apiserver.*" "${NODE2}:/etc/kubernetes/ssl/" +scp -3 "${NODE1}:/etc/kubernetes/ssl/apiserver.*" "${NODE3}:/etc/kubernetes/ssl/" + +scp -3 "${NODE1}:/etc/kubernetes/ssl/apiserver-kubelet-client.*" "${NODE2}:/etc/kubernetes/ssl/" +scp -3 "${NODE1}:/etc/kubernetes/ssl/apiserver-kubelet-client.*" "${NODE3}:/etc/kubernetes/ssl/" + +scp -3 "${NODE1}:/etc/kubernetes/ssl/front-proxy-client.*" "${NODE2}:/etc/kubernetes/ssl/" +scp -3 "${NODE1}:/etc/kubernetes/ssl/front-proxy-client.*" "${NODE3}:/etc/kubernetes/ssl/" +``` + +8. Continue again with (4) for each node that is left diff --git a/docs/src/how-to/administrate/kubernetes/certificate-renewal/scenario-1_k8s-v1.14-kubespray.rst b/docs/src/how-to/administrate/kubernetes/certificate-renewal/scenario-1_k8s-v1.14-kubespray.rst deleted file mode 100644 index 2db6f4f178..0000000000 --- a/docs/src/how-to/administrate/kubernetes/certificate-renewal/scenario-1_k8s-v1.14-kubespray.rst +++ /dev/null @@ -1,244 +0,0 @@ -How to renew certificates on kubernetes 1.14.x -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Kubernetes-internal certificates by default (see assumptions) expire after one year. Without renewal, your installation will cease to function. -This page explains how to renew certificates. - -Assumptions ------------ - -- Kubernetes version 1.14.x -- installed with the help of `Kubespray `__ - - - This page was tested using kubespray release 2.10 branch from 2019-05-20, i.e. commit ``e2f5a9748e4dbfe2fdba7931198b0b5f1f4bdc7e``. -- setup: 3 scheduled nodes, each hosting master (control plane) + - worker (kubelet) + etcd (cluster state, key-value database) - -*NOTE: due to Kubernetes being installed with Kubespray, the Kubernetes -CAs (expire after 10yr) as well as certificates involved in etcd -communication (expire after 100yr) are not required to be renewed (any -time soon).* - -**Official documentation:** - -* `Certificate Management with kubeadm (v1.14) `__ -* `PKI certificates and requirements (v1.14) `__ - -High-level description ----------------------- - -1. verify current expiration date -2. issue new certificates -3. generate new client configuration (aka kubeconfig file) -4. restart control plane -5. drain node - restart kubelet - uncordon node again -6. repeat 3-5 on all other nodes - -Step-by-step instructions -------------------------- - -*Please note, that the following instructions may require privileged -execution. So, either switch to a privileged user or prepend following -statements with ``sudo``. In any case, it is most likely that every -newly created file has to be owned by ``root``, depending on kow -Kubernetes was installed.* - -1. Verify current expiration date on each node - -.. code:: bash - - - export K8S_CERT_DIR=/etc/kubernetes/pki - export ETCD_CERT_DIR=/etc/ssl/etcd/ssl - export KUBELET_CERT_DIR=/var/lib/kubelet/pki - - - for crt in ${K8S_CERT_DIR}/*.crt; do - expirationDate=$(openssl x509 -noout -text -in ${crt} | grep After | sed -e 's/^[[:space:]]*//') - echo "$(basename ${crt}) -- ${expirationDate}" - done - - - for crt in $(ls ${ETCD_CERT_DIR}/*.pem | grep -v 'key'); do - expirationDate=$(openssl x509 -noout -text -in ${crt} | grep After | sed -e 's/^[[:space:]]*//') - echo "$(basename ${crt}) -- ${expirationDate}" - done - - echo "kubelet-client-current.pem -- $(openssl x509 -noout -text -in ${KUBELET_CERT_DIR}/kubelet-client-current.pem | grep After | sed -e 's/^[[:space:]]*//')" - echo "kubelet.crt -- $(openssl x509 -noout -text -in ${KUBELET_CERT_DIR}/kubelet.crt | grep After | sed -e 's/^[[:space:]]*//')" - - - # MASTER: api-server cert - echo -n | openssl s_client -connect localhost:6443 2>&1 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | openssl x509 -text -noout | grep Not - # MASTER: controller-manager cert - echo -n | openssl s_client -connect localhost:10257 2>&1 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | openssl x509 -text -noout | grep Not - # MASTER: scheduler cert - echo -n | openssl s_client -connect localhost:10259 2>&1 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | openssl x509 -text -noout | grep Not - - # WORKER: kubelet cert - echo -n | openssl s_client -connect localhost:10250 2>&1 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | openssl x509 -text -noout | grep Not - -2. Allocate a terminal session on one node and backup existing - certificates & configurations - -.. code:: bash - - cd /etc/kubernetes - - cp -r ./ssl ./ssl.bkp - - cp admin.conf admin.conf.bkp - cp controller-manager.conf controller-manager.conf.bkp - cp scheduler.conf scheduler.conf.bkp - cp kubelet.conf kubelet.conf.bkp - -3. Renew certificates on that very node - -.. code:: bash - - kubeadm alpha certs renew apiserver - kubeadm alpha certs renew apiserver-kubelet-client - kubeadm alpha certs renew front-proxy-client - -*Looking at the timestamps of the certificates, it is indicated, that apicerver, kubelet & proxy-client have been -renewed. This can be confirmed, by executing parts of (1).* - -:: - - root@kubenode01:/etc/kubernetes$ ls -al ./ssl - total 56 - drwxr-xr-x 2 kube root 4096 Mar 20 17:09 . - drwxr-xr-x 5 kube root 4096 Mar 20 17:08 .. - -rw-r--r-- 1 root root 1517 Mar 20 15:12 apiserver.crt - -rw------- 1 root root 1675 Mar 20 15:12 apiserver.key - -rw-r--r-- 1 root root 1099 Mar 20 15:13 apiserver-kubelet-client.crt - -rw------- 1 root root 1675 Mar 20 15:13 apiserver-kubelet-client.key - -rw-r--r-- 1 root root 1025 Sep 23 14:53 ca.crt - -rw------- 1 root root 1679 Sep 23 14:53 ca.key - -rw-r--r-- 1 root root 1038 Sep 23 14:53 front-proxy-ca.crt - -rw------- 1 root root 1679 Sep 23 14:53 front-proxy-ca.key - -rw-r--r-- 1 root root 1058 Mar 20 15:13 front-proxy-client.crt - -rw------- 1 root root 1675 Mar 20 15:13 front-proxy-client.key - -rw------- 1 root root 1679 Sep 23 14:53 sa.key - -rw------- 1 root root 451 Sep 23 14:53 sa.pub - -4. Based on those renewed certificates, generate new kubeconfig files - -The first command assumes it's being executed on a master node. You may need to swap ``masters`` with ``nodes`` in -case you are on a different sort of machines. - -.. code:: bash - - kubeadm alpha kubeconfig user --org system:masters --client-name kubernetes-admin > /etc/kubernetes/admin.conf - kubeadm alpha kubeconfig user --client-name system:kube-controller-manager > /etc/kubernetes/controller-manager.conf - kubeadm alpha kubeconfig user --client-name system:kube-scheduler > /etc/kubernetes/scheduler.conf - -*Again, check if ownership and permission for these files are the same -as all the others around them.* - -And, in case you are operating the cluster from the current node, you may want to replace the user's kubeconfig. -Afterwards, compare the backup version with the new one, to see if any configuration (e.g. pre-configured *namespace*) -might need to be moved over, too. - -.. code:: bash - - mv ~/.kube/config ~/.kube/config.bkp - cp /etc/kubernetes/admin.conf ~/.kube/config - chown $(id -u):$(id -g) ~/.kube/config - chmod 770 ~/.kube/config - -5. Now that certificates and configuration files are in place, the - control plane must be restarted. They typically run in containers, so - the easiest way to trigger a restart, is to kill the processes - running in there. Use (1) to verify, that the expiration dates indeed - have been changed. - -.. code:: bash - - kill -s SIGHUP $(pidof kube-apiserver) - kill -s SIGHUP $(pidof kube-controller-manager) - kill -s SIGHUP $(pidof kube-scheduler) - -6. Make *kubelet* aware of the new certificate - -a) Drain the node - -:: - - kubectl drain --delete-local-data --ignore-daemonsets $(hostname) - -b) Stop the kubelet process - -:: - - systemctl stop kubelet - -c) Remove old certificates and configuration - -:: - - mv /var/lib/kubelet/pki{,old} - mkdir /var/lib/kubelet/pki - -d) Generate new kubeconfig file for the kubelet - -:: - - kubeadm alpha kubeconfig user --org system:nodes --client-name system:node:$(hostname) > /etc/kubernetes/kubelet.conf - -e) Start kubelet again - -:: - - systemctl start kubelet - -f) [Optional] Verify kubelet has recognized certificate rotation - -:: - - sleep 5 && systemctl status kubelet - -g) Allow workload to be scheduled again on the node - -:: - - kubectl uncordon $(hostname) - -7. Copy certificates over to all the other nodes - -Option A - you can ssh from one kubernetes node to another - -.. code:: bash - - # set the ip or hostname: - export NODE2=root@ip-or-hostname - export NODE3=... - - scp ./ssl/apiserver.* "${NODE2}:/etc/kubernetes/ssl/" - scp ./ssl/apiserver.* "${NODE3}:/etc/kubernetes/ssl/" - - scp ./ssl/apiserver-kubelet-client.* "${NODE2}:/etc/kubernetes/ssl/" - scp ./ssl/apiserver-kubelet-client.* "${NODE3}:/etc/kubernetes/ssl/" - - scp ./ssl/front-proxy-client.* "${NODE2}:/etc/kubernetes/ssl/" - scp ./ssl/front-proxy-client.* "${NODE3}:/etc/kubernetes/ssl/" - -Option B - copy via local administrator's machine - -.. code:: bash - - # set the ip or hostname: - export NODE1=root@ip-or-hostname - export NODE2= - export NODE3= - - scp -3 "${NODE1}:/etc/kubernetes/ssl/apiserver.*" "${NODE2}:/etc/kubernetes/ssl/" - scp -3 "${NODE1}:/etc/kubernetes/ssl/apiserver.*" "${NODE3}:/etc/kubernetes/ssl/" - - scp -3 "${NODE1}:/etc/kubernetes/ssl/apiserver-kubelet-client.*" "${NODE2}:/etc/kubernetes/ssl/" - scp -3 "${NODE1}:/etc/kubernetes/ssl/apiserver-kubelet-client.*" "${NODE3}:/etc/kubernetes/ssl/" - - scp -3 "${NODE1}:/etc/kubernetes/ssl/front-proxy-client.*" "${NODE2}:/etc/kubernetes/ssl/" - scp -3 "${NODE1}:/etc/kubernetes/ssl/front-proxy-client.*" "${NODE3}:/etc/kubernetes/ssl/" - -8. Continue again with (4) for each node that is left diff --git a/docs/src/how-to/administrate/kubernetes/index.md b/docs/src/how-to/administrate/kubernetes/index.md new file mode 100644 index 0000000000..cc2c6a0143 --- /dev/null +++ b/docs/src/how-to/administrate/kubernetes/index.md @@ -0,0 +1,20 @@ +# Kubernetes + +```{note} +These are not the official documentations you are looking for. +[This way](https://kubernetes.io/docs/tasks/administer-cluster/) please. + +The content referred below merely contains either some deviation from upstream or +additional information enriched here and there with shortcuts to the official documentation. +``` + +```{toctree} +:glob: true +:maxdepth: 1 + +Certificate renewal +How to restart a machine that is part of a Kubernetes cluster? +How to upgrade Kubernetes? +``` + +% diff --git a/docs/src/how-to/administrate/kubernetes/index.rst b/docs/src/how-to/administrate/kubernetes/index.rst deleted file mode 100644 index 2e6fcd71da..0000000000 --- a/docs/src/how-to/administrate/kubernetes/index.rst +++ /dev/null @@ -1,21 +0,0 @@ -Kubernetes -========== - -.. note:: - - These are not the official documentations you are looking for. - `This way `__ please. - - The content referred below merely contains either some deviation from upstream or - additional information enriched here and there with shortcuts to the official documentation. - - -.. toctree:: - :maxdepth: 1 - :glob: - - Certificate renewal - How to restart a machine that is part of a Kubernetes cluster? - How to upgrade Kubernetes? - -.. diff --git a/docs/src/how-to/administrate/kubernetes/restart-machines/index.md b/docs/src/how-to/administrate/kubernetes/restart-machines/index.md new file mode 100644 index 0000000000..0323efcf2d --- /dev/null +++ b/docs/src/how-to/administrate/kubernetes/restart-machines/index.md @@ -0,0 +1,42 @@ +(restarting-a-machine-in-a-kubernetes-cluster)= + +# Restarting a machine in a Kubernetes cluster + +```{note} +1. Know which kind of machine is going to be restarted + + > 1. control plane (api-server, controllers, etc.) + > 2. node (runs actual workload, e.g. *Brig* or *Webapp*) + > 3. *a* and *b* combined + +2. The kind of machine in question must be deployed redundantly + +3. Take out machines in a rolling fashion (sequentially, one at a time) +``` + +## Control plane + +Depending on whether *etcd* is hosted on the same machine alongside the control plane (common practise), you need +to take its implications into account (see {ref}`How to rolling-restart an etcd cluster `) +when restarting a machine. + +Regardless of where *etcd* is located, before turning off any machine that is part of the control plane, one should +{ref}`back up the cluster state `. + +If a part of the control plane does not run sufficiently redundant, it is advised to prevent any mutating interaction +during the procedure, until the cluster is healthy again. + +```bash +kubectl get nodes +``` + +## Node + +```{rubric} High-level steps: +``` + +1. Drain the node so that all workload is rescheduled on other nodes +2. Restart / Update / Decommission +3. Mark the node as being schedulable again (if not decommissioned) + +*For more details please refer to the official documentation:* [Safely Drain a Node](https://kubernetes.io/docs/tasks/administer-cluster/safely-drain-node/) diff --git a/docs/src/how-to/administrate/kubernetes/restart-machines/index.rst b/docs/src/how-to/administrate/kubernetes/restart-machines/index.rst deleted file mode 100644 index 4f4a315a93..0000000000 --- a/docs/src/how-to/administrate/kubernetes/restart-machines/index.rst +++ /dev/null @@ -1,45 +0,0 @@ -.. _restarting-a-machine-in-a-kubernetes-cluster: - -Restarting a machine in a Kubernetes cluster -============================================ - -.. note:: - - 1. Know which kind of machine is going to be restarted - - a) control plane (api-server, controllers, etc.) - b) node (runs actual workload, e.g. *Brig* or *Webapp*) - c) *a* and *b* combined - - 2. The kind of machine in question must be deployed redundantly - 3. Take out machines in a rolling fashion (sequentially, one at a time) - - -Control plane -~~~~~~~~~~~~~ - -Depending on whether *etcd* is hosted on the same machine alongside the control plane (common practise), you need -to take its implications into account (see :ref:`How to rolling-restart an etcd cluster `) -when restarting a machine. - -Regardless of where *etcd* is located, before turning off any machine that is part of the control plane, one should -:ref:`back up the cluster state `. - -If a part of the control plane does not run sufficiently redundant, it is advised to prevent any mutating interaction -during the procedure, until the cluster is healthy again. - -.. code:: bash - - kubectl get nodes - - -Node -~~~~ - -.. rubric:: High-level steps: - -1. Drain the node so that all workload is rescheduled on other nodes -2. Restart / Update / Decommission -3. Mark the node as being schedulable again (if not decommissioned) - -*For more details please refer to the official documentation:* `Safely Drain a Node `__ diff --git a/docs/src/how-to/administrate/kubernetes/upgrade-cluster/index.md b/docs/src/how-to/administrate/kubernetes/upgrade-cluster/index.md new file mode 100644 index 0000000000..739ae7ee2c --- /dev/null +++ b/docs/src/how-to/administrate/kubernetes/upgrade-cluster/index.md @@ -0,0 +1,75 @@ +# Upgrading a Kubernetes cluster + +Before upgrading Kubernetes, a couple of aspects should be taken into account: + +- downtime is (not) permitted +- stateful backing services that run outside or on top of Kubernetes + +As a result the following questions arise: + +1. Is an in-place upgrade required (reuse existing machines) or is it possible to + deploy a second cluster right next to the first one and install Wire on top? +2. How was the Kubernetes cluster deployed? + +Depending on the deployment method, the upgrade procedure may vary. It may be reasonable to test +the upgrade in a non-production environment first. +Regardless of the deployment method, it is recommended to {ref}`back up the cluster state +` before starting to upgrade the cluster. Additional background knowledge +can be found in the section about {ref}`restarting a machine in an kubernetes cluster `. + +```{warning} +For an in-place upgrade, it is *NOT* recommended to go straight to the latest Kubernetes +version. Instead, one should upgrade step by step between each minor version. +``` + +## Manually + +Doing an upgrade by hand is cumbersome and error-prone, which is why there are tools and +automation for this procedure. The high-level steps would be: + +1. upgrade the control plane (also see a more detailed [list](https://kubernetes.io/docs/tasks/administer-cluster/cluster-upgrade/#manual-deployments)) + : 1. all *etcd* instances + 2. api-server on each control-plane host + 3. controllers, scheduler, +2. upgrade the nodes (order may vary, depending on whether the kube-components run in containers) + : - kubelet + - kube-proxy + - container runtime +3. then upgrade the clients (`kubectl`, e.g. on workstations or in pipelines) + +*For more details, please refer to the official documentation:* +[Upgrade A Cluster](https://kubernetes.io/docs/tasks/administer-cluster/cluster-upgrade/) + +## Kubespray (Ansible) + +Kubespray comes with a dedicated playbook that should be used to perform the upgrade: +`upgrade-cluster.yml`. Before running the playbook, make sure that the right Kubespray version +is being used. Each Kubespray version supports only a small and sliding window of Kubernetes +versions (check `kube_version` & `kube_version_min_required` in `roles/kubespray-defaults/defaults/main.yaml` +for a given [release version tag](https://github.com/kubernetes-sigs/kubespray/releases)). + +The commands may look similar to this example (assuming Kubernetes v1.18 version installed +with Kubespray 2.14): + +```bash +git clone https://github.com/kubernetes-sigs/kubespray +cd kubespray +git checkout release-2.15 +${EDITOR} roles/kubespray-defaults/defaults/main.yaml + +ansible-playbook -i ./../path/my/inventory-dir -e kube_version=v1.19.7 ./upgrade-cluster.yml +``` + +% TODO: adjust the example showing how to run this with wire-server-deploy a/o the offline toolchain container image + +% TODO: add ref to the part of this documentation that talks about the air-gapped installation + +Kubespray takes care of bringing the new binaries into position on each machine, restarting +the components, and draining/uncordon nodes. + +*For more details please refer to the official documentation:* +[Upgrading Kubernetes in Kubespray](https://kubespray.io/#/docs/upgrades) + +## Kubeadm + +Please refer to the *official documentation:* [Upgrading kubeadm clusters](https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/kubeadm-upgrade/) diff --git a/docs/src/how-to/administrate/kubernetes/upgrade-cluster/index.rst b/docs/src/how-to/administrate/kubernetes/upgrade-cluster/index.rst deleted file mode 100644 index 1c09a137f9..0000000000 --- a/docs/src/how-to/administrate/kubernetes/upgrade-cluster/index.rst +++ /dev/null @@ -1,82 +0,0 @@ -Upgrading a Kubernetes cluster -============================== - -Before upgrading Kubernetes, a couple of aspects should be taken into account: - -* downtime is (not) permitted -* stateful backing services that run outside or on top of Kubernetes - -As a result the following questions arise: - -1. Is an in-place upgrade required (reuse existing machines) or is it possible to - deploy a second cluster right next to the first one and install Wire on top? -2. How was the Kubernetes cluster deployed? - -Depending on the deployment method, the upgrade procedure may vary. It may be reasonable to test -the upgrade in a non-production environment first. -Regardless of the deployment method, it is recommended to :ref:`back up the cluster state -` before starting to upgrade the cluster. Additional background knowledge -can be found in the section about :ref:`restarting a machine in an kubernetes cluster `. - - -.. warning:: - - For an in-place upgrade, it is *NOT* recommended to go straight to the latest Kubernetes - version. Instead, one should upgrade step by step between each minor version. - - -Manually -~~~~~~~~ - -Doing an upgrade by hand is cumbersome and error-prone, which is why there are tools and -automation for this procedure. The high-level steps would be: - -1. upgrade the control plane (also see a more detailed `list `__) - a) all *etcd* instances - b) api-server on each control-plane host - c) controllers, scheduler, -2. upgrade the nodes (order may vary, depending on whether the kube-components run in containers) - * kubelet - * kube-proxy - * container runtime -3. then upgrade the clients (``kubectl``, e.g. on workstations or in pipelines) - -*For more details, please refer to the official documentation:* -`Upgrade A Cluster `__ - - -Kubespray (Ansible) -~~~~~~~~~~~~~~~~~~~ - -Kubespray comes with a dedicated playbook that should be used to perform the upgrade: -``upgrade-cluster.yml``. Before running the playbook, make sure that the right Kubespray version -is being used. Each Kubespray version supports only a small and sliding window of Kubernetes -versions (check ``kube_version`` & ``kube_version_min_required`` in ``roles/kubespray-defaults/defaults/main.yaml`` -for a given `release version tag `__). - -The commands may look similar to this example (assuming Kubernetes v1.18 version installed -with Kubespray 2.14): - -.. code:: bash - - git clone https://github.com/kubernetes-sigs/kubespray - cd kubespray - git checkout release-2.15 - ${EDITOR} roles/kubespray-defaults/defaults/main.yaml - - ansible-playbook -i ./../path/my/inventory-dir -e kube_version=v1.19.7 ./upgrade-cluster.yml - -.. TODO: adjust the example showing how to run this with wire-server-deploy a/o the offline toolchain container image -.. TODO: add ref to the part of this documentation that talks about the air-gapped installation - -Kubespray takes care of bringing the new binaries into position on each machine, restarting -the components, and draining/uncordon nodes. - -*For more details please refer to the official documentation:* -`Upgrading Kubernetes in Kubespray `__ - - -Kubeadm -~~~~~~~ - -Please refer to the *official documentation:* `Upgrading kubeadm clusters `__ diff --git a/docs/src/how-to/administrate/minio.rst b/docs/src/how-to/administrate/minio.md similarity index 58% rename from docs/src/how-to/administrate/minio.rst rename to docs/src/how-to/administrate/minio.md index 6953d1355c..1a79ba648d 100644 --- a/docs/src/how-to/administrate/minio.rst +++ b/docs/src/how-to/administrate/minio.md @@ -1,20 +1,18 @@ -Minio ------- +# Minio +```{eval-rst} .. include:: includes/intro.rst +``` -This section only covers the bare minimum, for more information, see the `minio documentation `__ +This section only covers the bare minimum, for more information, see the [minio documentation](https://docs.min.io/) - -Should you be using minio? -=========================== +## Should you be using minio? Minio can be used to emulate an S3-compatible setup. When a native S3-like storage provider is already present in your network or cloud provider, we advise using that instead. -Setting up interaction with Minio -================================= +## Setting up interaction with Minio Minio can be installed on your servers using our provided ansible playbooks. The ansible playbook will also install the minio client and configure it to @@ -25,29 +23,33 @@ minio to run behind a loadbalancer like HAProxy, and configure the Minio client to point to this loadbalancer instead. Our ansible playbooks will also configure the minio client and adds the locally -reachable API under the ``local`` alias:: +reachable API under the `local` alias: - mc config host list +``` +mc config host list +``` -If it is not there, it can be added manually as follows:: +If it is not there, it can be added manually as follows: - mc config host add local http://localhost:9000 +``` +mc config host add local http://localhost:9000 +``` The status of the cluster can be requested by contacting any of the servers. In -our case we will contact the locally running server:: +our case we will contact the locally running server: - mc admin info local +``` +mc admin info local +``` -Minio maintenance -================= +## Minio maintenance There will be times where one wants to take a minio server down for maintenance. One might want to apply security patches, or want to take out a broken disk and replace it with a fixed one. Minio will not tell you the health status of disks. You should have separate alerting and monitoring in place to keep track of hardware health. For example, one could look at -S.M.A.R.T. values that the disks produce with Prometheus `node_exporter -`_ +S.M.A.R.T. values that the disks produce with Prometheus [node_exporter](https://github.com/prometheus-community/node-exporter-textfile-collector-scripts/blob/master/smartmon.sh) Special care has to be taken when restarting Minio nodes, but it should be safe to do so. Minio can operate in read-write mode with (N/2) + 1 instances @@ -62,9 +64,11 @@ interrupted and the user must retry. When you shut down a node, one should take precautions that subsequent API calls are sent to other nodes in the cluster. -To stop a server, type:: +To stop a server, type: - systemctl stop minio-server +``` +systemctl stop minio-server +``` Writes that happen during the server being down will not be synced to the server that is offline. It is important that once you bring the server back @@ -77,32 +81,40 @@ is thus recommended to heal an instance immediately once it is back up; before a restart any other instances. Now that the server is offline, perform any maintenance that you want to do. -Afterwards, restart it with:: +Afterwards, restart it with: - systemctl start minio-server +``` +systemctl start minio-server +``` -Now check:: +Now check: - mc admin info local +``` +mc admin info local +``` to see if the cluster is healthy. Now that the server is back online, it has missed writes that have happened whilst it was offline. Because of this we must heal the cluster now -A heal of the cluster is performed as follows:: +A heal of the cluster is performed as follows: - mc admin heal -r local +``` +mc admin heal -r local +``` -Which will show a result page that looks like this:: +Which will show a result page that looks like this: - ◑ bunny - 0/0 objects; 0 B in 2s - ┌────────┬───┬─────────────────────┐ - │ Green │ 2 │ 66.7% ████████ │ - │ Yellow │ 1 │ 33.3% ████ │ - │ Red │ 0 │ 0.0% │ - │ Grey │ 0 │ 0.0% │ - └────────┴───┴─────────────────────┘ +``` +◑ bunny + 0/0 objects; 0 B in 2s + ┌────────┬───┬─────────────────────┐ + │ Green │ 2 │ 66.7% ████████ │ + │ Yellow │ 1 │ 33.3% ████ │ + │ Red │ 0 │ 0.0% │ + │ Grey │ 0 │ 0.0% │ + └────────┴───┴─────────────────────┘ +``` green - all good yellow - healed partially @@ -110,33 +122,32 @@ red - quorum missing grey - more than quorum number shards are gone, means the object for some reason is not recoverable When there are any yellow items, it usually means that not all servers have seen -the node come up properly again. Running the heal command with the ``--json`` option +the node come up properly again. Running the heal command with the `--json` option will give you more verbose and precise information why the heal only happened partially. -.. code:: json - - { - "after" : { - "online" : 5, - "offline" : 1, - "missing" : 0, - "corrupted" : 0, - "drives" : [ - { - "endpoint" : "http://10.0.0.42:9091/var/lib/minio-server1", - "state" : "offline", - "uuid" : "" - }, - { - "uuid" : "", - "endpoint" : "/var/lib/minio-server1", - "state" : "ok" - } - ], - "color" : "yellow" - } - } - +```json +{ + "after" : { + "online" : 5, + "offline" : 1, + "missing" : 0, + "corrupted" : 0, + "drives" : [ + { + "endpoint" : "http://10.0.0.42:9091/var/lib/minio-server1", + "state" : "offline", + "uuid" : "" + }, + { + "uuid" : "", + "endpoint" : "/var/lib/minio-server1", + "state" : "ok" + } + ], + "color" : "yellow" + } +} +``` In our case, we see that the reason for the partial recovery was that one the server was still considered offline. Rerunning the command yielded @@ -158,97 +169,96 @@ thus important to have good monitoring in place and respond accordingly. Minio itself will auto-heal the cluster every month if the administrator doesn't trigger a heal themselves. - -Rotate root credentials -======================= +## Rotate root credentials In order to change the root credentials, one needs to restart minio once but set with the old and the new credentials at the same time. -If you installed minio with the Ansible, the `role `__ +If you installed minio with the Ansible, the [role](https://github.com/wireapp/ansible-minio) takes care of this. Just change the inventory accordingly and re-apply the role. -For more information, please refer to the *Credentials* section in the `official documentation `__. +For more information, please refer to the *Credentials* section in the [official documentation](https://docs.min.io/docs/minio-server-configuration-guide.html). -Check the health of a MinIO node -================================ +(check-the-health-of-a-minio-node)= -This is the procedure to check a minio node's health. +## Check the health of a MinIO node -First log into the minio server +This is the procedure to check a minio node's health -.. code:: sh +First log into the minio server - ssh +```sh +ssh +``` There, run the following commands: -.. code:: sh - - env $(sudo grep KEY /etc/default/minio-server1 | xargs) bash - export MC_HOST_local="http://$MINIO_ACCESS_KEY:$MINIO_SECRET_KEY@127.0.0.1:9000" - mc admin info local +```sh +env $(sudo grep KEY /etc/default/minio-server1 | xargs) bash +export MC_HOST_local="http://$MINIO_ACCESS_KEY:$MINIO_SECRET_KEY@127.0.0.1:9000" +mc admin info local +``` You should see a result similar to this: -.. code:: sh - - * 192.168.0.12:9092 - Uptime: 2 months - Version: 2020-10-28T08:16:50Z - Network: 6/6 OK - Drives: 1/1 OK - - * 192.168.0.22:9000 - Uptime: 2 months - Version: 2020-10-28T08:16:50Z - Network: 6/6 OK - Drives: 1/1 OK - - * 192.168.0.22:9092 - Uptime: 2 months - Version: 2020-10-28T08:16:50Z - Network: 6/6 OK - Drives: 1/1 OK - - * 192.168.0.32:9000 - Uptime: 2 months - Version: 2020-10-28T08:16:50Z - Network: 6/6 OK - Drives: 1/1 OK - - * 192.168.0.32:9092 - Uptime: 2 months - Version: 2020-10-28T08:16:50Z - Network: 6/6 OK - Drives: 1/1 OK - - * 192.168.0.12:9000 - Uptime: 2 months - Version: 2020-10-28T08:16:50Z - Network: 6/6 OK - Drives: 1/1 OK - -Make sure you see ``Network: 6/6 OK``. +```sh +* 192.168.0.12:9092 +Uptime: 2 months +Version: 2020-10-28T08:16:50Z +Network: 6/6 OK +Drives: 1/1 OK + +* 192.168.0.22:9000 +Uptime: 2 months +Version: 2020-10-28T08:16:50Z +Network: 6/6 OK +Drives: 1/1 OK + +* 192.168.0.22:9092 +Uptime: 2 months +Version: 2020-10-28T08:16:50Z +Network: 6/6 OK +Drives: 1/1 OK + +* 192.168.0.32:9000 +Uptime: 2 months +Version: 2020-10-28T08:16:50Z +Network: 6/6 OK +Drives: 1/1 OK + +* 192.168.0.32:9092 +Uptime: 2 months +Version: 2020-10-28T08:16:50Z +Network: 6/6 OK +Drives: 1/1 OK + +* 192.168.0.12:9000 +Uptime: 2 months +Version: 2020-10-28T08:16:50Z +Network: 6/6 OK +Drives: 1/1 OK +``` + +Make sure you see `Network: 6/6 OK`. Reboot the machine with: -.. code:: sh - - sudo reboot +```sh +sudo reboot +``` Then wait at least a minute. If you go to ssh in, and get 'Connection refused', it just means you need to wait a bit longer. -Tip: You can automatically ask SSH to attempt to connect until it is succesful, by using the following command: - -.. code:: sh +Tip: You can automatically ask SSH to attempt to connect until it is succesful, by using the following command: - ssh -o 'ConnectionAttempts 3600' exit +```sh +ssh -o 'ConnectionAttempts 3600' exit +``` Log into minio ( repeat the steps above ), and check again. You should see a very low uptime value on two hosts now. -This is because we install minio 'twice' on each host. \ No newline at end of file +This is because we install minio 'twice' on each host. diff --git a/docs/src/how-to/administrate/operations.md b/docs/src/how-to/administrate/operations.md new file mode 100644 index 0000000000..9a8b8522a6 --- /dev/null +++ b/docs/src/how-to/administrate/operations.md @@ -0,0 +1,139 @@ +# Operational procedures + +This section describes common operations to be performed on operational clusters. + +## Reboot procedures + +The general procedure to reboot a service is as follows: + +- 1. {ref}`Check the health ` of the service. (If the health isn't good search for "troubleshooting" in the documentation. If it is good, move to the next step.) +- 2. Reboot the server the service is running on. +- 3. {ref}`Check the health ` of the service **again**. (If the health isn't good search for "troubleshooting" in the documentation. If it is good, your reboot was succesful.) + +The method for checking health is different for each service type, you can find a list of those methods {ref}`here `. + +The method to reset a service is the same for most services, except for `restund`, for which the procedure is different, and can be found {ref}`here `. + +For other (non-`restund`) services, the procedure is as follows: + +Assuming in this example you are trying to reboot a minio server, follow these steps: + +First, {ref}`check the health ` of the services. + +Second, reboot the services: + +```sh +ssh -t sudo reboot +``` + +Third, wait until the service is up again by trying to connect to it via SSH : + +```sh +ssh -o 'ConnectionAttempts 3600' exit +``` + +(`ConnectionAttempts` will make it so it attempts to connect until the host is actually Up and the connection is succesful) + +Fourth, {ref}`check the health ` of the service again. + +(operations-health-checks)= + +## Health checks + +This is a list of the health-checking procedures currently documented, for different service types: + +- {ref}`MinIO ` +- {ref}`Cassandra ` +- {ref}`Elasticsearch ` +- {ref}`Etcd ` +- {ref}`Restund ` (the health check is explained as part of the reboot procedure). + +To check the health of different services not listed here, see the documentation for that specific project, or ask your Wire contact. + +```{note} +If a service is running inside a Kubernetes pod, checking its health is easy: if the pod is running, it is healthy. A non-healthy pod will stop running, and will be shown as such. +``` + +## Draining pods from a node for maintainance + +You might want to remove («drain») all pods from a specific node/server, so you can do maintainance work on it, without disrupting the entire cluster. + +If you want to do this, you should follow the procudure found at: + +In short, the procedure is essentially: + +First, identify the name of the node you wish to drain. You can list all of the nodes in your cluster with + +```sh +kubectl get nodes +``` + +Next, tell Kubernetes to drain the node: + +```sh +kubectl drain +``` + +Once it returns (without giving an error), you can power down the node (or equivalently, if on a cloud platform, delete the virtual machine backing the node). If you leave the node in the cluster during the maintenance operation, you need to run + +```sh +kubectl uncordon +``` + +afterwards to tell Kubernetes that it can resume scheduling new pods onto the node. + +## Understand release tags + +We have two major release tags that you sometimes want to map on each other: *github*, and *helm chart*. + +Github have a tag of the form `vYYYY-MM-DD`, and the release notes and (some build artefacts) can be found on github, eg., [here](https://github.com/wireapp/wire-server/releases/v2022-01-18). Helm chart tags have the form `N.NNN.0`. The minor version `0` is for the development branch; non-zero values refer to unreleased intermediate states. + +### On the command line + +You can find the github tag for a helm chart tag like this: + +```sh +git tag --points-at v2022-01-18 | sort +``` + +... and the other way around like this: + +```sh +git tag --points-at chart=2.122.0,image=2.122.0 | sort +``` + +Note that the actual tag has the form `chart=,image=`. + +Unfortunately, older releases may have more helm chart tags; you need to find the largest number that has the form `N.NNN.0` from the list yourself. + +A list of all releases can be produced like this: + +```sh +git log --decorate --first-parent origin/master +``` + +If you want to find the + +### In the github UI + +Consult [the changelog](https://github.com/wireapp/wire-server/blob/develop/CHANGELOG.md) +to find the github tag of the release you're interested in (say, +v2022-01-18). + +Visit [the release notes of that release](https://github.com/wireapp/wire-server/releases/v2022-01-18). +Click on the commit hash: + +```{image} operations/fig1.png +``` + +Click on the 3 dots: + +```{image} operations/fig2.png +``` + +Now you can see a (possibly rather long) list of tags, some of then +have the form `chart=N.NNN.0,image=N.NNN.0`. Pick the one with the +largest number. + +```{image} operations/fig3.png +``` diff --git a/docs/src/how-to/administrate/operations.rst b/docs/src/how-to/administrate/operations.rst deleted file mode 100644 index bee240acb1..0000000000 --- a/docs/src/how-to/administrate/operations.rst +++ /dev/null @@ -1,144 +0,0 @@ - -Operational procedures -====================== - -This section describes common operations to be performed on operational clusters. - -Reboot procedures ------------------ - -The general procedure to reboot a service is as follows: - -* 1. `Check the health `__ of the service. (If the health isn't good, move to `troubleshooting `__. If it is good, move to the next step.) -* 2. Reboot the server the service is running on. -* 3. `Check the health `__ of the service **again**. (If the health isn't good, move to `troubleshooting `__. If it is good, your reboot was succesful.) - -The method for checking health is different for each service type, you can find a list of those methods `here `__. - -The method to reset a service is the same for most services, except for ``restund``, for which the procedure is different, and can be found `here `__. - -For other (non-``restund``) services, the procedure is as follows: - -Assuming in this example you are trying to reboot a minio server, follow these steps: - -First, `check the health `__ of the services. - -Second, reboot the services: - -.. code:: sh - - ssh -t sudo reboot - -Third, wait until the service is up again by trying to connect to it via SSH : - -.. code:: sh - - ssh -o 'ConnectionAttempts 3600' exit - -(``ConnectionAttempts`` will make it so it attempts to connect until the host is actually Up and the connection is succesful) - -Fourth, `check the health `__ of the service again. - -Health checks -------------- - -This is a list of the health-checking procedures currently documented, for different service types: - -* `MinIO `__. -* `Cassandra `__. -* `elasticsearch `__. -* `Etcd `__. -* `Restund `__ (the health check is explained as part of the reboot procedure). - -To check the health of different services not listed here, see the documentation for that specific project, or ask your Wire contact. - -.. note:: - - If a service is running inside a Kubernetes pod, checking its health is easy: if the pod is running, it is healthy. A non-healthy pod will stop running, and will be shown as such. - -Draining pods from a node for maintainance ------------------------------------------- - -You might want to remove («drain») all pods from a specific node/server, so you can do maintainance work on it, without disrupting the entire cluster. - -If you want to do this, you should follow the procudure found at: https://kubernetes.io/docs/tasks/administer-cluster/safely-drain-node/ - -In short, the procedure is essentially: - -First, identify the name of the node you wish to drain. You can list all of the nodes in your cluster with - -.. code:: sh - - kubectl get nodes - -Next, tell Kubernetes to drain the node: - -.. code:: sh - - kubectl drain - -Once it returns (without giving an error), you can power down the node (or equivalently, if on a cloud platform, delete the virtual machine backing the node). If you leave the node in the cluster during the maintenance operation, you need to run - -.. code:: sh - - kubectl uncordon - -afterwards to tell Kubernetes that it can resume scheduling new pods onto the node. - -Understand release tags ------------------------ - -We have two major release tags that you sometimes want to map on each other: *github*, and *helm chart*. - -Github have a tag of the form `vYYYY-MM-DD`, and the release notes and (some build artefacts) can be found on github, eg., `here `__. Helm chart tags have the form `N.NNN.0`. The minor version `0` is for the development branch; non-zero values refer to unreleased intermediate states. - -On the command line -^^^^^^^^^^^^^^^^^^^ - -You can find the github tag for a helm chart tag like this: - -.. code:: sh - - git tag --points-at v2022-01-18 | sort - -... and the other way around like this: - -.. code:: sh - - git tag --points-at chart=2.122.0,image=2.122.0 | sort - -Note that the actual tag has the form `chart=,image=`. - -Unfortunately, older releases may have more helm chart tags; you need to find the largest number that has the form `N.NNN.0` from the list yourself. - -A list of all releases can be produced like this: - -.. code:: sh - - git log --decorate --first-parent origin/master - -If you want to find the - -In the github UI -^^^^^^^^^^^^^^^^ - -Consult `the changelog -`__ -to find the github tag of the release you're interested in (say, -v2022-01-18). - -Visit `the release notes of that release -`__. -Click on the commit hash: - -.. image:: operations/fig1.png - -Click on the 3 dots: - -.. image:: operations/fig2.png - -Now you can see a (possibly rather long) list of tags, some of then -have the form `chart=N.NNN.0,image=N.NNN.0`. Pick the one with the -largest number. - -.. image:: operations/fig3.png diff --git a/docs/src/how-to/administrate/restund.md b/docs/src/how-to/administrate/restund.md new file mode 100644 index 0000000000..86bdd27e6a --- /dev/null +++ b/docs/src/how-to/administrate/restund.md @@ -0,0 +1,293 @@ +# Restund (TURN) + +```{eval-rst} +.. include:: includes/intro.rst +``` + +(allocations)= + +## Wire-Server Configuration + +The wire-server can either serve a static list of TURN servers to the clients or +it can discovery them using DNS SRV Records. + +### Static List + +To configure a static list of TURN servers to use, override +`values/wire-server/values.yaml` like this: + +```yaml +# (...) + +brig: +# (...) + turnStatic: + v1: + # v1 entries can be ignored and are not in use anymore since end of 2018. + v2: + - turn:server1.example.com:3478 # server 1 UDP + - turn:server1.example.com:3478?transport=tcp # server 1 TCP + - turns:server1.example.com:5478?transport=tcp # server 1 TLS + - turn:server2.example.com:3478 # server 2 UDP + - turn:server2.example.com:3478?transport=tcp # server 2 TCP + - turns:server2.example.com:5478?transport=tcp # server 2 TLS + turn: + serversSource: files +``` + +### DNS SRV Records + +To configure wire-server to use DNS SRV records in order to discover TURN +servers, override `values/wire-server/values.yaml` like this: + +```yaml +# (...) + +brig: +# (...) + turn: + serversSource: dns + baseDomain: prod.example.com + discoveryIntervalSeconds: 10 +``` + +When configured like this, the wire-server would look for these 3 SRV records +every 10 seconds: + +1. `_turn._udp.prod.example.com` will be used to discover UDP hostnames and port for all the + turn servers. +2. `_turn._tcp.prod.example.com` will be used to discover the TCP hostnames and port for all + the turn servers. +3. `_turns._tcp.prod.example.com` will be used to discover the TLS hostnames and port for + all the turn servers. + +Entries with weight 0 will be ignored. Example: + +``` +dig +retries=3 +short SRV _turn._udp.prod.example.com + +0 0 3478 turn36.prod.example.com +0 10 3478 turn34..prod.example.com +0 10 3478 turn35.prod.example.com +``` + +At least one of these 3 lookups must succeed for the wire-server to be able to +respond correctly when `GET /calls/config/v2` is called. All successful +responses are served in the result. + +In addition, if there are any clients using the legacy endpoint, `GET +/calls/config`, (all versions of all mobile apps since 2018 no longer use this) they will be served by the servers listed in the +`_turn._udp.prod.example.com` SRV record. This endpoint, however, will not +serve the domain names received inside the SRV record, instead it will serve the +first `A` record that is associated with each domain name in the SRV record. + +## How to see how many people are currently connected to the restund server + +You can see the count of currently ongoing calls (also called "allocations"): + +```sh +echo turnstats | nc -u 127.0.0.1 33000 -q1 | grep allocs_cur | cut -d' ' -f2 +``` + +## How to restart restund (with downtime) + +With downtime, it's very easy: + +``` +systemctl restart restund +``` + +```{warning} +Restarting `restund` means any user that is currently connected to it (i.e. having a call) will lose its audio/video connection. If you wish to have no downtime, check the next section\* +``` + +(rebooting-a-restund-node)= + +## Rebooting a Restund node + +If you want to reboot a restund node, you need to make sure the other restund nodes in the cluster are running, so that services are not interrupted by the reboot. + +```{warning} +This procedure as described here will cause downtime, even if a second restund server is up; and kill any ongoing audio/video calls. The sections further up describe a downtime and a no-downtime procedure. +``` + +Presuming your two restund nodes are called: + +- `restund-1` +- `restund-2` + +To prepare for a reboot of `restund-1`, log into the other restund server (`restund-2`, for example here), and make sure the docker service is running. + +List the running containers, to ensure restund is running, by executing: + +```sh +ssh -t sudo docker container ls +``` + +You should see the following in the results: + +```sh +CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES + quay.io/wire/restund:v0.4.16b1.0.53 22 seconds ago Up 18 seconds restund +``` + +Make sure you see this restund container, and it is running ("Up"). + +If it is not, you need to do troubleshooting work, if it is running, you can move forward and reboot restund-1. + +Now log into the restund server you wish to reboot (`restund-1` in this example), and reboot it + +```sh +ssh -t sudo reboot +``` + +Wait at least a minute for the machine to restart, you can use this command to automatically retry SSH access until it is succesful: + +```sh +ssh -o 'ConnectionAttempts 3600' exit +``` + +Then log into the restund server (`restund-1`, in this example), and make sure the docker service is running: + +```sh +ssh -t sudo docker container ls +``` + +```sh +CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES + quay.io/wire/restund:v0.4.16b1.0.53 22 seconds ago Up 18 seconds restund +``` + +Here again, make sure you see a restund container, and it is running ("Up"). + +If it is, you have succesfully reboot the restund server, and can if you need to apply the same procedure to the other restund servers in your cluster. + +## How to restart restund without having downtime + +For maintenance you may need to restart a restund server. + +1. Remove that restund server you want to restart from the list of advertised nodes, by taking it out of the turn server list that brig advertises: + +Go to the place where you store kubernetes configuration for your wire-server installation. This might be a directory on your admin laptop, or a directory on the kubernetes machine. + +If your override configuration (`values/wire-server/values.yaml`) looks like the following: + +```yaml +# (...) + +brig: +# (...) + turnStatic: + v1: + # v1 entries can be ignored and are not in use anymore since end of 2018. + v2: + - turn:server1.example.com:3478 # server 1 UDP + - turn:server1.example.com:3478?transport=tcp # server 1 TCP + - turns:server1.example.com:5478?transport=tcp # server 1 TLS + - turn:server2.example.com:3478 # server 2 UDP + - turn:server2.example.com:3478?transport=tcp # server 2 TCP + - turns:server2.example.com:5478?transport=tcp # server 2 TLS +``` + +And you want to remove server 1, then change the configuration to read + +```yaml +turnStatic: + v2: + - turn:server2.example.com:3478 # server 2 UDP + - turn:server2.example.com:3478?transport=tcp # server 2 TCP + - turns:server2.example.com:5478?transport=tcp # server 2 TLS +``` + +(or comment out lines by adding a `#` in front of the respective line) + +```yaml +turnStatic: + v2: + #- turn:server1.example.com:3478 # server 1 UDP + #- turn:server1.example.com:3478?transport=tcp # server 1 TCP + #- turns:server1.example.com:5478?transport=tcp # server 1 TLS + - turn:server2.example.com:3478 # server 2 UDP + - turn:server2.example.com:3478?transport=tcp # server 2 TCP + - turns:server2.example.com:5478?transport=tcp # server 2 TLS +``` + +Next, apply these changes to configuration with `./bin/prod-setup.sh` + +You then need to restart the `brig` pods if your code is older than September 2019 (otherwise brig will restart itself automatically): + +```bash +kubectl delete pod -l app=brig +``` + +2. Wait for traffic to drain. This can take up to 12 hours after the configuration change. Wait until current allocations (people connected to the restund server) return 0. See {ref}`allocations`. +3. It's now safe to `systemctl stop restund`, and take any necessary actions. +4. `systemctl start restund` and then add the restund server back to configuration of advertised nodes (see step 1, put the server back). + +## How to renew a certificate for restund + +1. Replace the certificate file on the server (under `/etc/restund/restund.pem` usually), either with ansible or manually. Ensure the new certificate file is a concatenation of your whole certificate chain *and* the private key: + +```text +-----BEGIN CERTIFICATE----- +... +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +... +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +... +-----END PRIVATE KEY----- +``` + +2. Restart restund (see sections above) + +## How to check which restund/TURN servers will be used by clients + +The list of turn servers contacted by clients *should* match what you added to your `turnStatic` configuration. But if you'd like to double-check, here's how: + +Terminal one: + +```sh +kubectl port-forward svc/brig 9999:8080 +``` + +Terminal two: + +```sh +UUID=$(cat /proc/sys/kernel/random/uuid) +curl -s -H "Z-User:$UUID" -H "Z-Connection:anything" "http://localhost:9999/calls/config/v2" | json_pp +``` + +May return something like: + +```json +{ + "ice_servers" : [ + { + "credential" : "ASyFLXqbmg8fuK4chJG3S1Qg4L/nnhpkN0/UctdtTFbGW1AcuuAaOqUMDhm9V2w7zKHY6PPMqjhwKZ2neSE78g==", + "urls" : [ + "turn:turn1.example.com:3478" + ], + "username" : "d=1582157904.v=1.k=0.t=s.r=mbzovplogqxbasbf" + }, + { + "credential" : "ZsxEtGWbpUZ3QWxPZtbX6g53HXu6PWfhhUfGNqRBJjrsly5w9IPAsuAWLEOP7fsoSXF13mgSPROXxMYAB/fQ6g==", + "urls" : [ + "turn:turn1.example.com:3478?transport=tcp" + ], + "username" : "d=1582157904.v=1.k=0.t=s.r=jsafnwtgqhfqjvco" + }, + { + "credential" : "ZsxEtGWbpUZ3QWxPZtbX6g53HXu6PWfhhUfGNqRBJjrsly5w9IPAsuAWLEOP7fsoSXF13mgSPROXxMYAB/fQ6g==", + "urls" : [ + "turns:turn1.example.com:5349?transport=tcp" + ], + "username" : "d=1582157904.v=1.k=0.t=s.r=jsafnwtgqhfqjvco" + } + ], + "ttl" : 3600 +} +``` + +In the above case, there is a single server configured to use UDP on port 3478, plain TCP on port 3478, and TLS over TCP on port 5349. The ordering of the list is random and will change on every request made with curl. diff --git a/docs/src/how-to/administrate/restund.rst b/docs/src/how-to/administrate/restund.rst deleted file mode 100644 index 584066ab43..0000000000 --- a/docs/src/how-to/administrate/restund.rst +++ /dev/null @@ -1,301 +0,0 @@ -Restund (TURN) --------------- - -.. include:: includes/intro.rst - -.. _allocations: - -Wire-Server Configuration -~~~~~~~~~~~~~~~~~~~~~~~~~ - -The wire-server can either serve a static list of TURN servers to the clients or -it can discovery them using DNS SRV Records. - -Static List -+++++++++++ - -To configure a static list of TURN servers to use, override -``values/wire-server/values.yaml`` like this: - -.. code:: yaml - - # (...) - - brig: - # (...) - turnStatic: - v1: - # v1 entries can be ignored and are not in use anymore since end of 2018. - v2: - - turn:server1.example.com:3478 # server 1 UDP - - turn:server1.example.com:3478?transport=tcp # server 1 TCP - - turns:server1.example.com:5478?transport=tcp # server 1 TLS - - turn:server2.example.com:3478 # server 2 UDP - - turn:server2.example.com:3478?transport=tcp # server 2 TCP - - turns:server2.example.com:5478?transport=tcp # server 2 TLS - turn: - serversSource: files - -DNS SRV Records -+++++++++++++++ - -To configure wire-server to use DNS SRV records in order to discover TURN -servers, override ``values/wire-server/values.yaml`` like this: - -.. code:: yaml - - # (...) - - brig: - # (...) - turn: - serversSource: dns - baseDomain: prod.example.com - discoveryIntervalSeconds: 10 - -When configured like this, the wire-server would look for these 3 SRV records -every 10 seconds: - -1. ``_turn._udp.prod.example.com`` will be used to discover UDP hostnames and port for all the - turn servers. -2. ``_turn._tcp.prod.example.com`` will be used to discover the TCP hostnames and port for all - the turn servers. -3. ``_turns._tcp.prod.example.com`` will be used to discover the TLS hostnames and port for - all the turn servers. - -Entries with weight 0 will be ignored. Example: - -.. code:: - - dig +retries=3 +short SRV _turn._udp.prod.example.com - - 0 0 3478 turn36.prod.example.com - 0 10 3478 turn34..prod.example.com - 0 10 3478 turn35.prod.example.com - -At least one of these 3 lookups must succeed for the wire-server to be able to -respond correctly when ``GET /calls/config/v2`` is called. All successful -responses are served in the result. - -In addition, if there are any clients using the legacy endpoint, ``GET -/calls/config``, (all versions of all mobile apps since 2018 no longer use this) they will be served by the servers listed in the -``_turn._udp.prod.example.com`` SRV record. This endpoint, however, will not -serve the domain names received inside the SRV record, instead it will serve the -first ``A`` record that is associated with each domain name in the SRV record. - -How to see how many people are currently connected to the restund server -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can see the count of currently ongoing calls (also called "allocations"): - -.. code:: sh - - echo turnstats | nc -u 127.0.0.1 33000 -q1 | grep allocs_cur | cut -d' ' -f2 - -How to restart restund (with downtime) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -With downtime, it's very easy:: - - systemctl restart restund - -.. warning:: - - Restarting ``restund`` means any user that is currently connected to it (i.e. having a call) will lose its audio/video connection. If you wish to have no downtime, check the next section* - -Rebooting a Restund node -~~~~~~~~~~~~~~~~~~~~~~~~ - -If you want to reboot a restund node, you need to make sure the other restund nodes in the cluster are running, so that services are not interrupted by the reboot. - -.. warning:: - - This procedure as described here will cause downtime, even if a second restund server is up; and kill any ongoing audio/video calls. The sections further up describe a downtime and a no-downtime procedure. - -Presuming your two restund nodes are called: - -* ``restund-1`` -* ``restund-2`` - -To prepare for a reboot of ``restund-1``, log into the other restund server (``restund-2``, for example here), and make sure the docker service is running. - -List the running containers, to ensure restund is running, by executing: - -.. code:: sh - - ssh -t sudo docker container ls - -You should see the following in the results: - -.. code:: sh - - CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES - quay.io/wire/restund:v0.4.16b1.0.53 22 seconds ago Up 18 seconds restund - -Make sure you see this restund container, and it is running ("Up"). - -If it is not, you need to do troubleshooting work, if it is running, you can move forward and reboot restund-1. - -Now log into the restund server you wish to reboot (``restund-1`` in this example), and reboot it - -.. code:: sh - - ssh -t sudo reboot - -Wait at least a minute for the machine to restart, you can use this command to automatically retry SSH access until it is succesful: - -.. code:: sh - - ssh -o 'ConnectionAttempts 3600' exit - -Then log into the restund server (``restund-1``, in this example), and make sure the docker service is running: - -.. code:: sh - - ssh -t sudo docker container ls - -.. code:: sh - - CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES - quay.io/wire/restund:v0.4.16b1.0.53 22 seconds ago Up 18 seconds restund - -Here again, make sure you see a restund container, and it is running ("Up"). - -If it is, you have succesfully reboot the restund server, and can if you need to apply the same procedure to the other restund servers in your cluster. - -How to restart restund without having downtime -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -For maintenance you may need to restart a restund server. - -1. Remove that restund server you want to restart from the list of advertised nodes, by taking it out of the turn server list that brig advertises: - -Go to the place where you store kubernetes configuration for your wire-server installation. This might be a directory on your admin laptop, or a directory on the kubernetes machine. - -If your override configuration (``values/wire-server/values.yaml``) looks like the following: - -.. code:: yaml - - # (...) - - brig: - # (...) - turnStatic: - v1: - # v1 entries can be ignored and are not in use anymore since end of 2018. - v2: - - turn:server1.example.com:3478 # server 1 UDP - - turn:server1.example.com:3478?transport=tcp # server 1 TCP - - turns:server1.example.com:5478?transport=tcp # server 1 TLS - - turn:server2.example.com:3478 # server 2 UDP - - turn:server2.example.com:3478?transport=tcp # server 2 TCP - - turns:server2.example.com:5478?transport=tcp # server 2 TLS - -And you want to remove server 1, then change the configuration to read - -.. code:: yaml - - turnStatic: - v2: - - turn:server2.example.com:3478 # server 2 UDP - - turn:server2.example.com:3478?transport=tcp # server 2 TCP - - turns:server2.example.com:5478?transport=tcp # server 2 TLS - -(or comment out lines by adding a ``#`` in front of the respective line) - -.. code:: yaml - - turnStatic: - v2: - #- turn:server1.example.com:3478 # server 1 UDP - #- turn:server1.example.com:3478?transport=tcp # server 1 TCP - #- turns:server1.example.com:5478?transport=tcp # server 1 TLS - - turn:server2.example.com:3478 # server 2 UDP - - turn:server2.example.com:3478?transport=tcp # server 2 TCP - - turns:server2.example.com:5478?transport=tcp # server 2 TLS - -Next, apply these changes to configuration with ``./bin/prod-setup.sh`` - -You then need to restart the ``brig`` pods if your code is older than September 2019 (otherwise brig will restart itself automatically): - -.. code:: bash - - kubectl delete pod -l app=brig - -2. Wait for traffic to drain. This can take up to 12 hours after the configuration change. Wait until current allocations (people connected to the restund server) return 0. See :ref:`allocations`. -3. It's now safe to ``systemctl stop restund``, and take any necessary actions. -4. ``systemctl start restund`` and then add the restund server back to configuration of advertised nodes (see step 1, put the server back). - -How to renew a certificate for restund -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -1. Replace the certificate file on the server (under ``/etc/restund/restund.pem`` usually), either with ansible or manually. Ensure the new certificate file is a concatenation of your whole certificate chain *and* the private key: - -.. code:: text - - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- - -----BEGIN CERTIFICATE----- - ... - -----END CERTIFICATE----- - -----BEGIN PRIVATE KEY----- - ... - -----END PRIVATE KEY----- - - -2. Restart restund (see sections above) - - -How to check which restund/TURN servers will be used by clients -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The list of turn servers contacted by clients *should* match what you added to your `turnStatic` configuration. But if you'd like to double-check, here's how: - -Terminal one: - -.. code:: sh - - kubectl port-forward svc/brig 9999:8080 - -Terminal two: - -.. code:: sh - - UUID=$(cat /proc/sys/kernel/random/uuid) - curl -s -H "Z-User:$UUID" -H "Z-Connection:anything" "http://localhost:9999/calls/config/v2" | json_pp - - -May return something like: - -.. code:: json - - { - "ice_servers" : [ - { - "credential" : "ASyFLXqbmg8fuK4chJG3S1Qg4L/nnhpkN0/UctdtTFbGW1AcuuAaOqUMDhm9V2w7zKHY6PPMqjhwKZ2neSE78g==", - "urls" : [ - "turn:turn1.example.com:3478" - ], - "username" : "d=1582157904.v=1.k=0.t=s.r=mbzovplogqxbasbf" - }, - { - "credential" : "ZsxEtGWbpUZ3QWxPZtbX6g53HXu6PWfhhUfGNqRBJjrsly5w9IPAsuAWLEOP7fsoSXF13mgSPROXxMYAB/fQ6g==", - "urls" : [ - "turn:turn1.example.com:3478?transport=tcp" - ], - "username" : "d=1582157904.v=1.k=0.t=s.r=jsafnwtgqhfqjvco" - }, - { - "credential" : "ZsxEtGWbpUZ3QWxPZtbX6g53HXu6PWfhhUfGNqRBJjrsly5w9IPAsuAWLEOP7fsoSXF13mgSPROXxMYAB/fQ6g==", - "urls" : [ - "turns:turn1.example.com:5349?transport=tcp" - ], - "username" : "d=1582157904.v=1.k=0.t=s.r=jsafnwtgqhfqjvco" - } - ], - "ttl" : 3600 - } - -In the above case, there is a single server configured to use UDP on port 3478, plain TCP on port 3478, and TLS over TCP on port 5349. The ordering of the list is random and will change on every request made with curl. - diff --git a/docs/src/how-to/administrate/users.md b/docs/src/how-to/administrate/users.md new file mode 100644 index 0000000000..b1ec7d1c69 --- /dev/null +++ b/docs/src/how-to/administrate/users.md @@ -0,0 +1,590 @@ +(investigative-tasks)= + +# Investigative tasks (e.g. searching for users as server admin) + +This page requires that you have root access to the machines where kubernetes runs on, or have kubernetes permissions allowing you to port-forward arbitrary pods and services. + +If you have the `backoffice` pod installed, see also the [backoffice README](https://github.com/wireapp/wire-server/tree/develop/charts/backoffice). + +If you don't have `backoffice`, see below for some options: + +## Manually searching for users in cassandra + +Terminal one: + +```sh +kubectl port-forward svc/brig 9999:8080 +``` + +Terminal two: Search for your user by email: + +```sh +EMAIL=user@example.com +curl -v -G localhost:9999/i/users --data-urlencode email=$EMAIL; echo +# or, for nicer formatting +curl -v -G localhost:9999/i/users --data-urlencode email=$EMAIL | json_pp +``` + +You can also search by `handle` (unique username) or by phone: + +```sh +HANDLE=user123 +curl -v -G localhost:9999/i/users --data-urlencode handles=$HANDLE; echo + +PHONE=+490000000000000 # phone numbers must have the +country prefix and no spaces +curl -v -G localhost:9999/i/users --data-urlencode phone=$PHONE; echo +``` + +Which should give you output like: + +```json +[ + { + "managed_by" : "wire", + "assets" : [ + { + "key" : "3-2-a749af8d-a17b-4445-b360-46c93fc41bc6", + "size" : "preview", + "type" : "image" + }, + { + "size" : "complete", + "type" : "image", + "key" : "3-2-6cac6b57-9972-4aba-acbb-f078bc538b54" + } + ], + "picture" : [], + "accent_id" : 0, + "status" : "active", + "name" : "somename", + "email" : "user@example.com", + "id" : "9122e5de-b4fb-40fa-99ad-1b5d7d07bae5", + "locale" : "en", + "handle" : "user123" + } +] +``` + +The interesting part is the `id` (in the example case `9122e5de-b4fb-40fa-99ad-1b5d7d07bae5`): + +(user-deletion)= + +## Deleting a user which is not a team user + +The following will completely delete a user, its conversations, assets, etc. The only thing remaining will be an entry in cassandra indicating that this user existed in the past (only the UUID remains, all other attributes like name etc are purged) + +You can now delete that user by double-checking that the user you wish to delete is really the correct user: + +```sh +# replace the id with the id of the user you want to delete +curl -v localhost:9999/i/users/9122e5de-b4fb-40fa-99ad-1b5d7d07bae5 -XDELETE +``` + +Afterwards, the previous command (to search for a user in cassandra) should return an empty list (`[]`). + +When done, on terminal 1, ctrl+c to cancel the port-forwarding. + +## Searching and deleting users with no team + +If you require users to be part of a team, or for some other reason you need to delete all users who are not part of a team, you need to first find all such users, and then delete them. + +To find users that are not part of a team, first you need to connect via SSH to the machine where cassandra is running, and then run the following command: + +```sh +cqlsh 9042 -e "select team, handle, id from brig.user" | grep -E "^\s+null" +``` + +This will give you a list of handles and IDs with no team associated: + +```sh +null | null | bc22119f-ce11-4402-aa70-307a58fb22ec +null | tom | 8ecee3d0-47a4-43ff-977b-40a4fc350fed +null | alice | 2a4c3468-c1e6-422f-bc4d-4aeff47941ac +null | null | 1b5ca44a-aeb4-4a68-861b-48612438c4cc +null | bob | 701b4eab-6df2-476d-a818-90dc93e8446e +``` + +You can then {ref}`delete each user with these instructions `. + +## Manual search on elasticsearch (via brig, recommended) + +This should only be necessary in the case of some (suspected) data inconsistency between cassandra and elasticsearch. + +Terminal one: + +```sh +kubectl port-forward svc/brig 9999:8080 +``` + +Terminal two: Search for your user by name or handle or a prefix of that handle or name: + +```sh +NAMEORPREFIX=test7 +UUID=$(cat /proc/sys/kernel/random/uuid) +curl -H "Z-User:$UUID" "http://localhost:9999/search/contacts?q=$NAMEORPREFIX"; echo +# or, for pretty output: +curl -H "Z-User:$UUID" "http://localhost:9999/search/contacts?q=$NAMEORPREFIX" | json_pp +``` + +If no match is found, expect a query like this: + +```json +{"took":91,"found":0,"documents":[],"returned":0} +``` + +If matches are found, the result should look like this: + +```json +{ + "found" : 2, + "documents" : [ + { + "id" : "dbdbf370-48b3-4e1e-b377-76d7d4cbb4f2", + "name" : "Test", + "handle" : "test7", + "accent_id" : 7 + }, + { + "name" : "Test", + "accent_id" : 0, + "handle" : "test7476", + "id" : "a93240b0-ba89-441e-b8ee-ff4403808f93" + } + ], + "returned" : 2, + "took" : 4 +} +``` + +## How to manually search for a user on elasticsearh directly (not recommended) + +First, ssh to an elasticsearch instance. + +```sh +ssh +``` + +Then run the following: + +```sh +PREFIX=... +curl -s "http://localhost:9200/directory/_search?q=$PREFIX" | json_pp +``` + +The `id` (UUID) returned can be used when deleting (see below). + +## How to manually delete a user from elasticsearch only + +```{warning} +This is NOT RECOMMENDED. Be sure you know what you're doing. This only deletes the user from elasticsearch, but not from cassandra. Any change of e.g. the username or displayname of that user means this user will re-appear in the elasticsearch database. Instead, either fully delete a user: {ref}`user-deletion` or make use of the internal GET/PUT `/i/searchable` endpoint on brig to make this user prefix-unsearchable. +``` + +If, despite the warning, you wish to continue? + +First, ssh to an elasticsearch instance: + +```sh +ssh +``` + +Next, check that the user exists: + +```sh +UUID=... +curl -s "http://localhost:9200/directory/user/$UUID" | json_pp +``` + +That should return a `"found": true`, like this: + +```json +{ + "_type" : "user", + "_version" : 1575998428262000, + "_id" : "b3e9e445-fb02-47f3-bac0-63f5f680d258", + "found" : true, + "_index" : "directory", + "_source" : { + "normalized" : "Mr Test", + "handle" : "test12345", + "id" : "b3e9e445-fb02-47f3-bac0-63f5f680d258", + "name" : "Mr Test", + "accent_id" : 1 + } +} +``` + +Then delete it: + +```sh +UUID=... +curl -s -XDELETE "http://localhost:9200/directory/user/$UUID" | json_pp +``` + +## Mass-invite users to a team + +If you need to invite members to a specific given team, you can use the `create_team_members.sh` Bash script, located [here](https://github.com/wireapp/wire-server/blob/develop/hack/bin/create_team_members.sh). + +This script does not create users or causes them to join a team by itself, instead, it sends invites to potential users via email, and when users accept the invitation, they create their account, set their password, and are added to the team as team members. + +Input is a [CSV file](https://en.wikipedia.org/wiki/Comma-separated_values), in comma-separated format, in the form `'Email,Suggested User Name'`. + +You also need to specify the inviting admin user, the team, the URI for the Brig ([API](https://docs.wire.com/understand/federation/api.html?highlight=brig)) service (Host), and finally the input (CSV) file containing the users to invite. + +The exact format for the parameters passed to the script is [as follows](https://github.com/wireapp/wire-server/blob/develop/hack/bin/create_team_members.sh#L17): + +- `-a `: [User ID](https://docs.wire.com/understand/federation/api.html?highlight=user%20id#qualified-identifiers-and-names) in [UUID format](https://en.wikipedia.org/wiki/Universally_unique_identifier) of the inviting admin. For example `9122e5de-b4fb-40fa-99ad-1b5d7d07bae5`. +- `-t `: ID of the inviting team, same format. +- `-h `: Base URI of brig's internal endpoint. +- `-c `: file containing info on the invitees in format 'Email,UserName'. + +For example, one such execution of the script could look like: + +```sh +sh create_team_members.sh -a 9122e5de-b4fb-40fa-99ad-1b5d7d07bae5 -t 123e4567-e89b-12d3-a456-426614174000 -h http://localhost:9999 -c users_to_invite.csv +``` + +Note: the '' implies you are running the 'kubectl port-forward' given at the top of this document +. +Once the script is run, invitations will be sent to each user in the file every second until all invitations have been sent. + +If you have a lot of invitations to send and this is too slow, you can speed things up by commenting [this line](https://github.com/wireapp/wire-server/blob/develop/hack/bin/create_team_members.sh#L91). + +## How to obtain logs from an Android client to investigate issues + +Wire clients communicate with Wire servers (backend). + +Sometimes to investigate server issues, you (or the Wire team) will need client information, in the form of client logs. + +In order to obtain client logs on the Android Wire client, follow this procedure: + +- Open the Wire app (client) on your Android device +- Click on the round user icon in the top left of the screen, leading to your user Profile. +- Click on "Settings" at the bottom of the screen +- Click on "Advanced" in the menu +- Check/activate "Collect usage data" +- Now go back to using your client normally, so usage data is generated. If you have been asked to follow a specific testing regime, or log a specific problem, this is the time to do so. +- Once enough usage data is generated, go back to the "Advanced" screen (User profile > Settings > Advanced) +- Click on "Create debug report" +- A menu will open allowing you to share the debug report, you can now save it or send it via email/any other means to the Wire team. + +## How to obtain logs from an iOS client to investigate issues + +Wire clients communicate with Wire servers (backend). + +Sometimes to investigate server issues, you (or the Wire team) will need client information, in the form of client logs. + +In order to obtain client logs on the iOS Wire client, follow this procedure: + +- Open the Wire app (client) on your iOS device +- Click on the round user icon in the top left of the screen, leading to your user Profile. +- Click on "Settings" at the bottom of the screen +- Click on "Advanced" in the menu +- Check/activate "Collect usage data" +- Now go back to using your client normally, so usage data is generated. If you have been asked to follow a specific testing regime, or log a specific problem, this is the time to do so. +- Once enough usage data is generated, go back to the "Advanced" screen (User profile > Settings > Advanced) +- Click on "Send report to wire" +- A menu will open to share the debug report via email, allowing you to send it to the Wire team. + +## How to retrieve metric values manually + +Metric values are sets of data points about services, such as status and other measures, that can be retrieved at specific endpoints, typically by a monitoring system (such as Prometheus) for monitoring, diagnosis and graphing. + +Sometimes, you will want to manually obtain this data that is normally automatically grabbed by Prometheus. + +Some of the pods allow you to grab metrics by accessing their `/i/metrics` endpoint, in particular: + +- `brig`: User management API +- `cannon`: WebSockets API +- `cargohold`: Assets storage API +- `galley`: Conversations and Teams API +- `gundeck`: Push Notifications API +- `spar`: Single-Sign-ON and SCIM + +For more details on the various services/pods, you can check out {ref}`this link `. + +Before you can grab metrics from a pod, you need to find its IP address. You do this by running the following command: + +```sh +d kubectl get pods -owide +``` + +(this presumes you are already in your normal Wire environment, which you obtain by running `source ./bin/offline-env.sh`) + +Which will give you an output that looks something like this: + +``` +demo@Ubuntu-1804-bionic-64-minimal:~/Wire-Server$ d kubectl get pods -owide +NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES +account-pages-784f9b547c-cp444 1/1 Running 0 6d23h 10.233.113.5 kubenode3 +brig-746ddc55fd-6pltz 1/1 Running 0 6d23h 10.233.110.11 kubenode2 +brig-746ddc55fd-d59dw 1/1 Running 0 6d4h 10.233.110.23 kubenode2 +brig-746ddc55fd-zp7jl 1/1 Running 0 6d23h 10.233.113.10 kubenode3 +brig-index-migrate-data-45rm7 0/1 Completed 0 6d23h 10.233.110.9 kubenode2 +cannon-0 1/1 Running 0 3h1m 10.233.119.41 kubenode1 +cannon-1 1/1 Running 0 3h1m 10.233.113.47 kubenode3 +cannon-2 1/1 Running 0 3h1m 10.233.110.51 kubenode2 +cargohold-65bff97fc6-8b9ls 1/1 Running 0 6d4h 10.233.113.20 kubenode3 +cargohold-65bff97fc6-bkx6x 1/1 Running 0 6d23h 10.233.113.4 kubenode3 +cargohold-65bff97fc6-tz8fh 1/1 Running 0 6d23h 10.233.110.5 kubenode2 +cassandra-migrations-bjsdz 0/1 Completed 0 6d23h 10.233.110.3 kubenode2 +demo-smtp-784ddf6989-vmj7t 1/1 Running 0 6d23h 10.233.113.2 kubenode3 +elasticsearch-index-create-7r8g4 0/1 Completed 0 6d23h 10.233.110.4 kubenode2 +fake-aws-sns-6c7c4b7479-wfp82 2/2 Running 0 6d4h 10.233.110.27 kubenode2 +fake-aws-sqs-59fbfbcbd4-n4c5z 2/2 Running 0 6d23h 10.233.110.2 kubenode2 +galley-7c89c44f7b-nm2rr 1/1 Running 0 6d23h 10.233.110.8 kubenode2 +galley-7c89c44f7b-tdxz4 1/1 Running 0 6d23h 10.233.113.6 kubenode3 +galley-7c89c44f7b-tr8pm 1/1 Running 0 6d4h 10.233.110.29 kubenode2 +galley-migrate-data-g66rz 0/1 Completed 0 6d23h 10.233.110.13 kubenode2 +gundeck-7fd75c7c5f-jb8xq 1/1 Running 0 6d23h 10.233.110.6 kubenode2 +gundeck-7fd75c7c5f-lbth9 1/1 Running 0 6d23h 10.233.113.8 kubenode3 +gundeck-7fd75c7c5f-wvcw6 1/1 Running 0 6d4h 10.233.113.23 kubenode3 +nginz-5cdd8b588b-dbn86 2/2 Running 16 6d23h 10.233.113.11 kubenode3 +nginz-5cdd8b588b-gk6rw 2/2 Running 14 6d23h 10.233.110.12 kubenode2 +nginz-5cdd8b588b-jvznt 2/2 Running 11 6d4h 10.233.113.21 kubenode3 +reaper-6957694667-s5vz5 1/1 Running 0 6d4h 10.233.110.26 kubenode2 +redis-ephemeral-master-0 1/1 Running 0 6d23h 10.233.113.3 kubenode3 +spar-56d77f85f6-bw55q 1/1 Running 0 6d23h 10.233.113.9 kubenode3 +spar-56d77f85f6-mczzd 1/1 Running 0 6d4h 10.233.110.28 kubenode2 +spar-56d77f85f6-vvvfq 1/1 Running 0 6d23h 10.233.110.7 kubenode2 +spar-migrate-data-ts4sx 0/1 Completed 0 6d23h 10.233.110.14 kubenode2 +team-settings-fbbb899c-qxx7m 1/1 Running 0 6d4h 10.233.110.24 kubenode2 +webapp-d97869795-grnft 1/1 Running 0 6d4h 10.233.110.25 kubenode2 +``` + +Here presuming we need to get metrics from `gundeck`, we can see the IP of one of the gundeck pods is `10.233.110.6`. + +We can therefore connect to node `kubenode2` on which this pod runs with `ssh kubenode2.your-domain.com`, and run the following: + +```sh +curl 10.233.110.6:8080/i/metrics +``` + +Alternatively, if you don't want to, or can't for some reason, connect to kubenode2, you can use port redirect instead: + +```sh +# Allow Gundeck to be reached via the port 7777 +kubectl --kubeconfig kubeconfig.dec -n wire port-forward service/gundeck 7777:8080 +# Reach Gundeck directly at port 7777 using curl, output resulting data to stdout/terminal +curl -v http://127.0.0.1:7777/i/metrics +``` + +Output will look something like this (truncated): + +```sh +# HELP gc_seconds_wall Wall clock time spent on last GC +# TYPE gc_seconds_wall gauge +gc_seconds_wall 5481304.0 +# HELP gc_seconds_cpu CPU time spent on last GC +# TYPE gc_seconds_cpu gauge +gc_seconds_cpu 5479828.0 +# HELP gc_bytes_used_current Number of bytes in active use as of the last GC +# TYPE gc_bytes_used_current gauge +gc_bytes_used_current 1535232.0 +# HELP gc_bytes_used_max Maximum amount of memory living on the heap after the last major GC +# TYPE gc_bytes_used_max gauge +gc_bytes_used_max 2685312.0 +# HELP gc_bytes_allocated_total Bytes allocated since the start of the server +# TYPE gc_bytes_allocated_total gauge +gc_bytes_allocated_total 4.949156056e9 +``` + +This example is for Gundeck, but you can also get metrics for other services. All k8s services are listed at {ref}`this link `. + +This is an example adapted for Cannon: + +```sh +kubectl --kubeconfig kubeconfig.dec -n wire port-forward service/cannon 7777:8080 +curl -v http://127.0.0.1:7777/i/metrics +``` + +In the output of this command, `net_websocket_clients` is roughly the number of connected clients. + +(reset-session-cookies)= + +## Reset session cookies + +Remove session cookies on your system to force users to login again within the next 15 minutes (or whenever they come back online): + +```{warning} +This will cause interruptions to ongoing calls and should be timed properly. +``` + +### Reset cookies of all users + +```sh +ssh +# from the ssh session +cqlsh +# from the cqlsh shell +truncate brig.user_cookies; +``` + +### Reset cookies for a defined list of users + +```sh +ssh +# within the ssh session +cqlsh +# within the cqlsh shell: delete all users by userId +delete from brig.user_cookies where user in (c0d64244-8ab4-11ec-8fda-37788be3a4e2, ...); +``` + +(Keep reading if you want to find out which users on your system are using SSO.) + +(identify-sso-users)= + +## Identify all users using SSO + +Collect all teams configured with an IdP: + +```sh +ssh +# within the ssh session start cqlsh +cqlsh +# within the cqlsh shell export all teams with idp +copy spar.idp (team) TO 'teams_with_idp.csv' with header=false; +``` + +Close the session and proceed locally: + +```sh +# download csv file +scp :teams_with_idp.csv . +# convert to a single line, comma separated list +tr '\n' ',' < teams_with_idp.csv; echo +``` + +And use this list to get all team members in these teams: + +```sh +ssh +# within the ssh session start cqlsh +cqlsh +# within the cqlsh shell select all members of previous identified teams +# should look like this: f2207d98-8ab3-11ec-b689-07fc1fd409c9, ... +select user from galley.team_member where team in (); +# alternatively, export the list of all users (for filterling locally in eg. excel) +copy galley.team_member (user, team, sso_id) TO 'users_with_idp.csv' with header=true; +``` + +Close the session and proceed locally to generate the list of all users from teams with IdP: + +```sh +# download csv file +scp :users_with_idp.csv . +# convert to a single line, comma separated list +tr '\n' ',' < users_with_idp.csv; echo +``` + +```{note} +Don't forget to dellete the created csv files after you have downloaded/processed them. +``` + +## Create a team using the SCIM API + +If you need to create a team manually, maybe because team creation was blocked in the "teams" interface, follow this procedure: + +First download or locate this bash script: `wire-server/hack/bin/create_test_team_scim.sh ` + +Then, run it the following way: + +```sh +./create_test_team_scim.sh -h -s +``` + +Where: + +- In `-h `, replace `` with the base URL for your brig host (for example: `https://brig-host.your-domain.com`, defaults to `http://localhost:8082`) +- In `-s `, replace `` with the base URL for your spar host (for example: `https://spar-host.your-domain.com`, defaults to `http://localhost:8088`) + +You might also need to edit the admin email and admin passwords at lines `48` and `49` of the script. + +To learn more about the different pods and how to identify them, see `this page`. + +You can list your pods with `kubectl get pods --namespace wire`. + +Alternatively, you can run the series of commands manually with `curl`, like this: + +```sh +curl -i -s --show-error \ + -XPOST "$BRIG_HOST/i/users" \ + -H'Content-type: application/json' \ + -d'{"email":"$ADMIN_EMAIL","password":"$ADMIN_PASSWORD","name":"$NAME_OF_TEAM","team":{"name":"$NAME_OF_TEAM","icon":"default"}}' +``` + +Where: + +- `$BRIG_HOST` is the base URL for your brig host +- `$ADMIN_EMAIL` is the email for the admin account for the new team +- `$ADMIN_PASSWORD` is the password for the admin account for the new team +- `$NAME_OF_TEAM` is the name of the team newly created + +Out of the result of this command, you will be able to extract an `Admin UUID`, and a `Team UUID`, which you will need later. + +Then run: + +```sh +curl -X POST \ + --header 'Content-Type: application/json' \ + --header 'Accept: application/json' \ + -d '{"email":"$ADMIN_EMAIL","password":"$ADMIN_PASSWORD"}' \ + $BRIG_HOST/login'?persist=false' | jq -r .access_token +``` + +Where the values to replace are the same as the command above. + +This command should output an access token, take note of it. + +Then run: + +```sh +curl -X POST \ + --header "Authorization: Bearer $ACCESS_TOKEN" \ + --header 'Content-Type: application/json;charset=utf-8' \ + --header 'Z-User: '"$ADMIN_UUID" \ + -d '{ "description": "test '"`date`"'", "password": "'"$ADMIN_PASSWORD"'" }' \ + $SPAR_HOST/scim/auth-tokens +``` + +Where the values to replace are the same as the first command, plus `$ACCESS_TOKEN` is access token you just took note of in the previous command. + +Out of the JSON output of this command, you should be able to extract: + +- A SCIM token (`token` value in the JSON). +- A SCIM token ID (`id` value in the `info` value in the JSON) + +Equiped with those tokens, we move on to the next script, `wire-server/hack/bin/create_team.sh ` + +This script can be run the following way: + +```sh +./create_team.sh -h -o -e -p -v -t -c +``` + +Where: + +- -h \: Base URI of brig. default: `http://localhost:8080` +- -o \: user display name of the owner of the team to be created. default: "owner name n/a" +- -e \: email address of the owner of the team to be created. default: "owner email n/a" +- -p \: owner password. default: "owner pass n/a" +- -v \: validation code received by email after running the previous script/commands. default: "email code n/a" +- -t \: default: "team name n/a" +- -c \: default: "USD" + +Alternatively, you can manually run the command: + +```sh +curl -i -s --show-error \ + -XPOST "$BRIG_HOST/register" \ + -H'Content-type: application/json' \ + -d'{"name":"$OWNER_NAME","email":"$OWNER_EMAIL","password":"$OWNER_PASSWORD","email_code":"$EMAIL_CODE","team":{"currency":"$TEAM_CURRENCY","icon":"default","name":"$TEAM_NAME"}}' +``` + +Where: + +- `$BRIG_HOST` is the base URL for your brig service +- `$OWNER_NAME` is the name of the of the team to be created +- `$OWNER_PASSWORD` is the password of the owner of the team to be created +- `$EMAIL_CODE` is the validation code received by email after running the previous script/command +- `$TEAM_CURRENCY` is the currency of the team +- `$TEAM_NAME` is the name of the team diff --git a/docs/src/how-to/administrate/users.rst b/docs/src/how-to/administrate/users.rst deleted file mode 100644 index e7d1e856dc..0000000000 --- a/docs/src/how-to/administrate/users.rst +++ /dev/null @@ -1,609 +0,0 @@ -.. _investigative_tasks: - -Investigative tasks (e.g. searching for users as server admin) ---------------------------------------------------------------- - -This page requires that you have root access to the machines where kubernetes runs on, or have kubernetes permissions allowing you to port-forward arbitrary pods and services. - -If you have the `backoffice` pod installed, see also the `backoffice README `__. - -If you don't have `backoffice`, see below for some options: - -Manually searching for users in cassandra -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Terminal one: - -.. code:: sh - - kubectl port-forward svc/brig 9999:8080 - -Terminal two: Search for your user by email: - -.. code:: sh - - EMAIL=user@example.com - curl -v -G localhost:9999/i/users --data-urlencode email=$EMAIL; echo - # or, for nicer formatting - curl -v -G localhost:9999/i/users --data-urlencode email=$EMAIL | json_pp - -You can also search by ``handle`` (unique username) or by phone: - -.. code:: sh - - HANDLE=user123 - curl -v -G localhost:9999/i/users --data-urlencode handles=$HANDLE; echo - - PHONE=+490000000000000 # phone numbers must have the +country prefix and no spaces - curl -v -G localhost:9999/i/users --data-urlencode phone=$PHONE; echo - - -Which should give you output like: - -.. code:: json - - [ - { - "managed_by" : "wire", - "assets" : [ - { - "key" : "3-2-a749af8d-a17b-4445-b360-46c93fc41bc6", - "size" : "preview", - "type" : "image" - }, - { - "size" : "complete", - "type" : "image", - "key" : "3-2-6cac6b57-9972-4aba-acbb-f078bc538b54" - } - ], - "picture" : [], - "accent_id" : 0, - "status" : "active", - "name" : "somename", - "email" : "user@example.com", - "id" : "9122e5de-b4fb-40fa-99ad-1b5d7d07bae5", - "locale" : "en", - "handle" : "user123" - } - ] - -The interesting part is the ``id`` (in the example case ``9122e5de-b4fb-40fa-99ad-1b5d7d07bae5``): - -.. _user-deletion: - -Deleting a user which is not a team user -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The following will completely delete a user, its conversations, assets, etc. The only thing remaining will be an entry in cassandra indicating that this user existed in the past (only the UUID remains, all other attributes like name etc are purged) - -You can now delete that user by double-checking that the user you wish to delete is really the correct user: - -.. code:: sh - - # replace the id with the id of the user you want to delete - curl -v localhost:9999/i/users/9122e5de-b4fb-40fa-99ad-1b5d7d07bae5 -XDELETE - -Afterwards, the previous command (to search for a user in cassandra) should return an empty list (``[]``). - -When done, on terminal 1, ctrl+c to cancel the port-forwarding. - -Searching and deleting users with no team -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you require users to be part of a team, or for some other reason you need to delete all users who are not part of a team, you need to first find all such users, and then delete them. - -To find users that are not part of a team, first you need to connect via SSH to the machine where cassandra is running, and then run the following command: - -.. code:: sh - - cqlsh 9042 -e "select team, handle, id from brig.user" | grep -E "^\s+null" - -This will give you a list of handles and IDs with no team associated: - -.. code:: sh - - null | null | bc22119f-ce11-4402-aa70-307a58fb22ec - null | tom | 8ecee3d0-47a4-43ff-977b-40a4fc350fed - null | alice | 2a4c3468-c1e6-422f-bc4d-4aeff47941ac - null | null | 1b5ca44a-aeb4-4a68-861b-48612438c4cc - null | bob | 701b4eab-6df2-476d-a818-90dc93e8446e - -You can then `delete each user with these instructions <./users.html#deleting-a-user-which-is-not-a-team-user>`__. - -Manual search on elasticsearch (via brig, recommended) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This should only be necessary in the case of some (suspected) data inconsistency between cassandra and elasticsearch. - -Terminal one: - -.. code:: sh - - kubectl port-forward svc/brig 9999:8080 - -Terminal two: Search for your user by name or handle or a prefix of that handle or name: - -.. code:: sh - - NAMEORPREFIX=test7 - UUID=$(cat /proc/sys/kernel/random/uuid) - curl -H "Z-User:$UUID" "http://localhost:9999/search/contacts?q=$NAMEORPREFIX"; echo - # or, for pretty output: - curl -H "Z-User:$UUID" "http://localhost:9999/search/contacts?q=$NAMEORPREFIX" | json_pp - -If no match is found, expect a query like this: - -.. code:: json - - {"took":91,"found":0,"documents":[],"returned":0} - -If matches are found, the result should look like this: - -.. code:: json - - { - "found" : 2, - "documents" : [ - { - "id" : "dbdbf370-48b3-4e1e-b377-76d7d4cbb4f2", - "name" : "Test", - "handle" : "test7", - "accent_id" : 7 - }, - { - "name" : "Test", - "accent_id" : 0, - "handle" : "test7476", - "id" : "a93240b0-ba89-441e-b8ee-ff4403808f93" - } - ], - "returned" : 2, - "took" : 4 - } - -How to manually search for a user on elasticsearh directly (not recommended) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -First, ssh to an elasticsearch instance. - -.. code:: sh - - ssh - -Then run the following: - -.. code:: sh - - PREFIX=... - curl -s "http://localhost:9200/directory/_search?q=$PREFIX" | json_pp - -The `id` (UUID) returned can be used when deleting (see below). - -How to manually delete a user from elasticsearch only -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. warning:: - - This is NOT RECOMMENDED. Be sure you know what you're doing. This only deletes the user from elasticsearch, but not from cassandra. Any change of e.g. the username or displayname of that user means this user will re-appear in the elasticsearch database. Instead, either fully delete a user: :ref:`user-deletion` or make use of the internal GET/PUT ``/i/searchable`` endpoint on brig to make this user prefix-unsearchable. - -If, despite the warning, you wish to continue? - -First, ssh to an elasticsearch instance: - -.. code:: sh - - ssh - -Next, check that the user exists: - -.. code:: sh - - UUID=... - curl -s "http://localhost:9200/directory/user/$UUID" | json_pp - -That should return a ``"found": true``, like this: - -.. code:: json - - { - "_type" : "user", - "_version" : 1575998428262000, - "_id" : "b3e9e445-fb02-47f3-bac0-63f5f680d258", - "found" : true, - "_index" : "directory", - "_source" : { - "normalized" : "Mr Test", - "handle" : "test12345", - "id" : "b3e9e445-fb02-47f3-bac0-63f5f680d258", - "name" : "Mr Test", - "accent_id" : 1 - } - } - - -Then delete it: - -.. code:: sh - - UUID=... - curl -s -XDELETE "http://localhost:9200/directory/user/$UUID" | json_pp - -Mass-invite users to a team -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you need to invite members to a specific given team, you can use the ``create_team_members.sh`` Bash script, located `here `__. - -This script does not create users or causes them to join a team by itself, instead, it sends invites to potential users via email, and when users accept the invitation, they create their account, set their password, and are added to the team as team members. - -Input is a `CSV file `__, in comma-separated format, in the form ``'Email,Suggested User Name'``. - -You also need to specify the inviting admin user, the team, the URI for the Brig (`API `__) service (Host), and finally the input (CSV) file containing the users to invite. - -The exact format for the parameters passed to the script is `as follows `__: - -* ``-a ``: `User ID `__ in `UUID format `__ of the inviting admin. For example ``9122e5de-b4fb-40fa-99ad-1b5d7d07bae5``. -* ``-t ``: ID of the inviting team, same format. -* ``-h ``: Base URI of brig's internal endpoint. -* ``-c ``: file containing info on the invitees in format 'Email,UserName'. - -For example, one such execution of the script could look like: - -.. code:: sh - - sh create_team_members.sh -a 9122e5de-b4fb-40fa-99ad-1b5d7d07bae5 -t 123e4567-e89b-12d3-a456-426614174000 -h http://localhost:9999 -c users_to_invite.csv - -Note: the 'http://localhost:9999' implies you are running the 'kubectl port-forward' given at the top of this document -. -Once the script is run, invitations will be sent to each user in the file every second until all invitations have been sent. - -If you have a lot of invitations to send and this is too slow, you can speed things up by commenting `this line `__. - - -How to obtain logs from an Android client to investigate issues -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Wire clients communicate with Wire servers (backend). - -Sometimes to investigate server issues, you (or the Wire team) will need client information, in the form of client logs. - -In order to obtain client logs on the Android Wire client, follow this procedure: - -* Open the Wire app (client) on your Android device -* Click on the round user icon in the top left of the screen, leading to your user Profile. -* Click on "Settings" at the bottom of the screen -* Click on "Advanced" in the menu -* Check/activate "Collect usage data" -* Now go back to using your client normally, so usage data is generated. If you have been asked to follow a specific testing regime, or log a specific problem, this is the time to do so. -* Once enough usage data is generated, go back to the "Advanced" screen (User profile > Settings > Advanced) -* Click on "Create debug report" -* A menu will open allowing you to share the debug report, you can now save it or send it via email/any other means to the Wire team. - - -How to obtain logs from an iOS client to investigate issues -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Wire clients communicate with Wire servers (backend). - -Sometimes to investigate server issues, you (or the Wire team) will need client information, in the form of client logs. - -In order to obtain client logs on the iOS Wire client, follow this procedure: - -* Open the Wire app (client) on your iOS device -* Click on the round user icon in the top left of the screen, leading to your user Profile. -* Click on "Settings" at the bottom of the screen -* Click on "Advanced" in the menu -* Check/activate "Collect usage data" -* Now go back to using your client normally, so usage data is generated. If you have been asked to follow a specific testing regime, or log a specific problem, this is the time to do so. -* Once enough usage data is generated, go back to the "Advanced" screen (User profile > Settings > Advanced) -* Click on "Send report to wire" -* A menu will open to share the debug report via email, allowing you to send it to the Wire team. - -How to retrieve metric values manually -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Metric values are sets of data points about services, such as status and other measures, that can be retrieved at specific endpoints, typically by a monitoring system (such as Prometheus) for monitoring, diagnosis and graphing. - -Sometimes, you will want to manually obtain this data that is normally automatically grabbed by Prometheus. - -Some of the pods allow you to grab metrics by accessing their ``/i/metrics`` endpoint, in particular: - -* ``brig``: User management API -* ``cannon``: WebSockets API -* ``cargohold``: Assets storage API -* ``galley``: Conversations and Teams API -* ``gundeck``: Push Notifications API -* ``spar``: Single-Sign-ON and SCIM - -For more details on the various services/pods, you can check out `this link <../../understand/overview.html?highlight=gundeck#focus-on-pods>`. - -Before you can grab metrics from a pod, you need to find its IP address. You do this by running the following command: - -.. code:: sh - - d kubectl get pods -owide - -(this presumes you are already in your normal Wire environment, which you obtain by running ``source ./bin/offline-env.sh``) - -Which will give you an output that looks something like this: - -.. code:: - - demo@Ubuntu-1804-bionic-64-minimal:~/Wire-Server$ d kubectl get pods -owide - NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES - account-pages-784f9b547c-cp444 1/1 Running 0 6d23h 10.233.113.5 kubenode3 - brig-746ddc55fd-6pltz 1/1 Running 0 6d23h 10.233.110.11 kubenode2 - brig-746ddc55fd-d59dw 1/1 Running 0 6d4h 10.233.110.23 kubenode2 - brig-746ddc55fd-zp7jl 1/1 Running 0 6d23h 10.233.113.10 kubenode3 - brig-index-migrate-data-45rm7 0/1 Completed 0 6d23h 10.233.110.9 kubenode2 - cannon-0 1/1 Running 0 3h1m 10.233.119.41 kubenode1 - cannon-1 1/1 Running 0 3h1m 10.233.113.47 kubenode3 - cannon-2 1/1 Running 0 3h1m 10.233.110.51 kubenode2 - cargohold-65bff97fc6-8b9ls 1/1 Running 0 6d4h 10.233.113.20 kubenode3 - cargohold-65bff97fc6-bkx6x 1/1 Running 0 6d23h 10.233.113.4 kubenode3 - cargohold-65bff97fc6-tz8fh 1/1 Running 0 6d23h 10.233.110.5 kubenode2 - cassandra-migrations-bjsdz 0/1 Completed 0 6d23h 10.233.110.3 kubenode2 - demo-smtp-784ddf6989-vmj7t 1/1 Running 0 6d23h 10.233.113.2 kubenode3 - elasticsearch-index-create-7r8g4 0/1 Completed 0 6d23h 10.233.110.4 kubenode2 - fake-aws-sns-6c7c4b7479-wfp82 2/2 Running 0 6d4h 10.233.110.27 kubenode2 - fake-aws-sqs-59fbfbcbd4-n4c5z 2/2 Running 0 6d23h 10.233.110.2 kubenode2 - galley-7c89c44f7b-nm2rr 1/1 Running 0 6d23h 10.233.110.8 kubenode2 - galley-7c89c44f7b-tdxz4 1/1 Running 0 6d23h 10.233.113.6 kubenode3 - galley-7c89c44f7b-tr8pm 1/1 Running 0 6d4h 10.233.110.29 kubenode2 - galley-migrate-data-g66rz 0/1 Completed 0 6d23h 10.233.110.13 kubenode2 - gundeck-7fd75c7c5f-jb8xq 1/1 Running 0 6d23h 10.233.110.6 kubenode2 - gundeck-7fd75c7c5f-lbth9 1/1 Running 0 6d23h 10.233.113.8 kubenode3 - gundeck-7fd75c7c5f-wvcw6 1/1 Running 0 6d4h 10.233.113.23 kubenode3 - nginz-5cdd8b588b-dbn86 2/2 Running 16 6d23h 10.233.113.11 kubenode3 - nginz-5cdd8b588b-gk6rw 2/2 Running 14 6d23h 10.233.110.12 kubenode2 - nginz-5cdd8b588b-jvznt 2/2 Running 11 6d4h 10.233.113.21 kubenode3 - reaper-6957694667-s5vz5 1/1 Running 0 6d4h 10.233.110.26 kubenode2 - redis-ephemeral-master-0 1/1 Running 0 6d23h 10.233.113.3 kubenode3 - spar-56d77f85f6-bw55q 1/1 Running 0 6d23h 10.233.113.9 kubenode3 - spar-56d77f85f6-mczzd 1/1 Running 0 6d4h 10.233.110.28 kubenode2 - spar-56d77f85f6-vvvfq 1/1 Running 0 6d23h 10.233.110.7 kubenode2 - spar-migrate-data-ts4sx 0/1 Completed 0 6d23h 10.233.110.14 kubenode2 - team-settings-fbbb899c-qxx7m 1/1 Running 0 6d4h 10.233.110.24 kubenode2 - webapp-d97869795-grnft 1/1 Running 0 6d4h 10.233.110.25 kubenode2 - -Here presuming we need to get metrics from ``gundeck``, we can see the IP of one of the gundeck pods is ``10.233.110.6``. - -We can therefore connect to node ``kubenode2`` on which this pod runs with ``ssh kubenode2.your-domain.com``, and run the following: - -.. code:: sh - - curl 10.233.110.6:8080/i/metrics - -Alternatively, if you don't want to, or can't for some reason, connect to kubenode2, you can use port redirect instead: - -.. code:: sh - - # Allow Gundeck to be reached via the port 7777 - kubectl --kubeconfig kubeconfig.dec -n wire port-forward service/gundeck 7777:8080 - # Reach Gundeck directly at port 7777 using curl, output resulting data to stdout/terminal - curl -v http://127.0.0.1:7777/i/metrics - -Output will look something like this (truncated): - -.. code:: sh - - # HELP gc_seconds_wall Wall clock time spent on last GC - # TYPE gc_seconds_wall gauge - gc_seconds_wall 5481304.0 - # HELP gc_seconds_cpu CPU time spent on last GC - # TYPE gc_seconds_cpu gauge - gc_seconds_cpu 5479828.0 - # HELP gc_bytes_used_current Number of bytes in active use as of the last GC - # TYPE gc_bytes_used_current gauge - gc_bytes_used_current 1535232.0 - # HELP gc_bytes_used_max Maximum amount of memory living on the heap after the last major GC - # TYPE gc_bytes_used_max gauge - gc_bytes_used_max 2685312.0 - # HELP gc_bytes_allocated_total Bytes allocated since the start of the server - # TYPE gc_bytes_allocated_total gauge - gc_bytes_allocated_total 4.949156056e9 - -This example is for Gundeck, but you can also get metrics for other services. All k8s services are listed at `this link <../../understand/overview.html?highlight=gundeck#focus-on-pods>`__. - -This is an example adapted for Cannon: - -.. code:: sh - - kubectl --kubeconfig kubeconfig.dec -n wire port-forward service/cannon 7777:8080 - curl -v http://127.0.0.1:7777/i/metrics - -In the output of this command, ``net_websocket_clients`` is roughly the number of connected clients. - -.. _reset session cookies: - -Reset session cookies -~~~~~~~~~~~~~~~~~~~~~ - -Remove session cookies on your system to force users to login again within the next 15 minutes (or whenever they come back online): - -.. warning:: - This will cause interruptions to ongoing calls and should be timed properly. - -Reset cookies of all users -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. code:: sh - - ssh - # from the ssh session - cqlsh - # from the cqlsh shell - truncate brig.user_cookies; - -Reset cookies for a defined list of users -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. code:: sh - - ssh - # within the ssh session - cqlsh - # within the cqlsh shell: delete all users by userId - delete from brig.user_cookies where user in (c0d64244-8ab4-11ec-8fda-37788be3a4e2, ...); - -(Keep reading if you want to find out which users on your system are using SSO.) - -.. _identify sso users: - -Identify all users using SSO -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Collect all teams configured with an IdP: - -.. code:: sh - - ssh - # within the ssh session start cqlsh - cqlsh - # within the cqlsh shell export all teams with idp - copy spar.idp (team) TO 'teams_with_idp.csv' with header=false; - -Close the session and proceed locally: - -.. code:: sh - - # download csv file - scp :teams_with_idp.csv . - # convert to a single line, comma separated list - tr '\n' ',' < teams_with_idp.csv; echo - -And use this list to get all team members in these teams: - -.. code:: sh - - ssh - # within the ssh session start cqlsh - cqlsh - # within the cqlsh shell select all members of previous identified teams - # should look like this: f2207d98-8ab3-11ec-b689-07fc1fd409c9, ... - select user from galley.team_member where team in (); - # alternatively, export the list of all users (for filterling locally in eg. excel) - copy galley.team_member (user, team, sso_id) TO 'users_with_idp.csv' with header=true; - -Close the session and proceed locally to generate the list of all users from teams with IdP: - -.. code:: sh - - # download csv file - scp :users_with_idp.csv . - # convert to a single line, comma separated list - tr '\n' ',' < users_with_idp.csv; echo - - -.. note:: - Don't forget to dellete the created csv files after you have downloaded/processed them. - -Create a team using the SCIM API -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you need to create a team manually, maybe because team creation was blocked in the "teams" interface, follow this procedure: - -First download or locate this bash script: `wire-server/hack/bin/create_test_team_scim.sh ` - -Then, run it the following way: - -.. code:: sh - - ./create_test_team_scim.sh -h -s - -Where: - -* In `-h `, replace `` with the base URL for your brig host (for example: `https://brig-host.your-domain.com`, defaults to `http://localhost:8082`) -* In `-s `, replace `` with the base URL for your spar host (for example: `https://spar-host.your-domain.com`, defaults to `http://localhost:8088`) - -You might also need to edit the admin email and admin passwords at lines `48` and `49` of the script. - -To learn more about the different pods and how to identify them, see `this page`. - -You can list your pods with `kubectl get pods --namespace wire`. - -Alternatively, you can run the series of commands manually with `curl`, like this: - -.. code:: sh - - curl -i -s --show-error \ - -XPOST "$BRIG_HOST/i/users" \ - -H'Content-type: application/json' \ - -d'{"email":"$ADMIN_EMAIL","password":"$ADMIN_PASSWORD","name":"$NAME_OF_TEAM","team":{"name":"$NAME_OF_TEAM","icon":"default"}}' - -Where: - -* `$BRIG_HOST` is the base URL for your brig host -* `$ADMIN_EMAIL` is the email for the admin account for the new team -* `$ADMIN_PASSWORD` is the password for the admin account for the new team -* `$NAME_OF_TEAM` is the name of the team newly created - -Out of the result of this command, you will be able to extract an `Admin UUID`, and a `Team UUID`, which you will need later. - -Then run: - -.. code:: sh - - curl -X POST \ - --header 'Content-Type: application/json' \ - --header 'Accept: application/json' \ - -d '{"email":"$ADMIN_EMAIL","password":"$ADMIN_PASSWORD"}' \ - $BRIG_HOST/login'?persist=false' | jq -r .access_token - -Where the values to replace are the same as the command above. - -This command should output an access token, take note of it. - -Then run: - -.. code:: sh - - curl -X POST \ - --header "Authorization: Bearer $ACCESS_TOKEN" \ - --header 'Content-Type: application/json;charset=utf-8' \ - --header 'Z-User: '"$ADMIN_UUID" \ - -d '{ "description": "test '"`date`"'", "password": "'"$ADMIN_PASSWORD"'" }' \ - $SPAR_HOST/scim/auth-tokens - -Where the values to replace are the same as the first command, plus `$ACCESS_TOKEN` is access token you just took note of in the previous command. - -Out of the JSON output of this command, you should be able to extract: - -* A SCIM token (`token` value in the JSON). -* A SCIM token ID (`id` value in the `info` value in the JSON) - -Equiped with those tokens, we move on to the next script, `wire-server/hack/bin/create_team.sh ` - -This script can be run the following way: - -.. code:: sh - - ./create_team.sh -h -o -e -p -v -t -c - -Where: - -* -h : Base URI of brig. default: `http://localhost:8080` -* -o : user display name of the owner of the team to be created. default: "owner name n/a" -* -e : email address of the owner of the team to be created. default: "owner email n/a" -* -p : owner password. default: "owner pass n/a" -* -v : validation code received by email after running the previous script/commands. default: "email code n/a" -* -t : default: "team name n/a" -* -c : default: "USD" - -Alternatively, you can manually run the command: - -.. code:: sh - - curl -i -s --show-error \ - -XPOST "$BRIG_HOST/register" \ - -H'Content-type: application/json' \ - -d'{"name":"$OWNER_NAME","email":"$OWNER_EMAIL","password":"$OWNER_PASSWORD","email_code":"$EMAIL_CODE","team":{"currency":"$TEAM_CURRENCY","icon":"default","name":"$TEAM_NAME"}}' - -Where: - -* `$BRIG_HOST` is the base URL for your brig service -* `$OWNER_NAME` is the name of the of the team to be created -* `$OWNER_PASSWORD` is the password of the owner of the team to be created -* `$EMAIL_CODE` is the validation code received by email after running the previous script/command -* `$TEAM_CURRENCY` is the currency of the team -* `$TEAM_NAME` is the name of the team diff --git a/docs/src/how-to/associate/custom-backend-for-desktop-client.md b/docs/src/how-to/associate/custom-backend-for-desktop-client.md new file mode 100644 index 0000000000..ad66ca975e --- /dev/null +++ b/docs/src/how-to/associate/custom-backend-for-desktop-client.md @@ -0,0 +1,79 @@ +# How to connect the desktop application to a custom backend + +## Introduction + +This page explains how to connect the Wire desktop client to a custom Backend, which can be done either via a start-up parameter or via an initialization file. + +## Prerequisites + +Install Wire either from the App Store, or download it from our website at () + +Have a running Wire backend in your infrastructure/cloud. + +Note down the full URL of the webapp served by that backend (e.g. ) + +## Using start-up parameters + +### Windows + +- Create a shortcut to the Wire application +- Edit the shortcut ( Right click > Properties ) +- Add the following command line parameters to the shortcut: `--env {URL}`, where `{URL}` is the URL of your webapp as noted down above + +### MacOS + +To create the application + +- Open Automator +- Click New application +- Add the "Run shell script" phase +- Type in the script panel the following command: `open -b com.wearezeta.zclient.mac --args --env {URL}`, where `{URL}` is the URL of your webapp as noted down above +- Save the application from Automator (e.g. on your desktop or in Application) +- To run the application: Just open the application you created in the first step + +### Linux + +- Open a Terminal +- Start the application with the command line arguments: `--env {URL}`, where `{URL}` is the URL of your webapp as noted down above + +## Using an initialization file + +By providing an initialization file the instance connection parameters and/or proxy settings for the Wire desktop application can be pre-configured. This requires Wire version >= 3.27. + +Create a file named `init.json` and set `customWebAppURL` and optionally `proxyServerURL` e.g. as follows: + +```json +{ + "customWebAppURL": "https://app.custom-wire.com", + "env": "CUSTOM", + "proxyServerURL": "http://127.0.0.1:3128", +} +``` + +The `env` setting must be set to `CUSTOM` for this to work. + +```{note} +Consult your site admin to learn what goes into these settings. The value of `customWebAppURL` can be found [here](https://github.com/wireapp/wire-server/blob/e6aa50913cdcfde1200114787baabf7896394a2f/charts/webapp/templates/deployment.yaml#L40-L41) or [resp. here](https://github.com/wireapp/wire-server/blob/e6aa50913cdcfde1200114787baabf7896394a2f/charts/webapp/values.yaml#L26). The value of `proxyServerURL` is your browser proxy. It depends on the configuration of the network your client is running in. +``` + +### Windows + +Move the `init.json` file to `%APPDATA%\Wire\config\init.json` if it does not already exist. Otherwise update it accordingly. + +### MacOS + +Move the `init.json` file to + +``` +~/Library/Containers/com.wearezeta.zclient.mac/Data/Library/Application\ Support/Wire/config/init.json +``` + +if it does not already exist. Otherwise, update it accordingly. + +### Linux + +On Linux the `init.json` file should be located in the following directory: + +``` +$HOME/.config/Wire/config/init.json +``` diff --git a/docs/src/how-to/associate/custom-backend-for-desktop-client.rst b/docs/src/how-to/associate/custom-backend-for-desktop-client.rst deleted file mode 100644 index 6f4f768345..0000000000 --- a/docs/src/how-to/associate/custom-backend-for-desktop-client.rst +++ /dev/null @@ -1,90 +0,0 @@ -How to connect the desktop application to a custom backend -========================================================== - -Introduction ------------- - -This page explains how to connect the Wire desktop client to a custom Backend, which can be done either via a start-up parameter or via an initialization file. - -Prerequisites --------------- - -Install Wire either from the App Store, or download it from our website at (https://wire.com/en/download/) - -Have a running Wire backend in your infrastructure/cloud. - -Note down the full URL of the webapp served by that backend (e.g. https://app.custom-wire.com ) - -Using start-up parameters -------------------------- - -Windows -~~~~~~~ - -- Create a shortcut to the Wire application -- Edit the shortcut ( Right click > Properties ) -- Add the following command line parameters to the shortcut: `--env {URL}`, where `{URL}` is the URL of your webapp as noted down above - -MacOS -~~~~~ - -To create the application - -- Open Automator -- Click New application -- Add the "Run shell script" phase -- Type in the script panel the following command: `open -b com.wearezeta.zclient.mac --args --env {URL}`, where `{URL}` is the URL of your webapp as noted down above -- Save the application from Automator (e.g. on your desktop or in Application) -- To run the application: Just open the application you created in the first step - -Linux -~~~~~ - -- Open a Terminal -- Start the application with the command line arguments: `--env {URL}`, where `{URL}` is the URL of your webapp as noted down above - -Using an initialization file ----------------------------- - -By providing an initialization file the instance connection parameters and/or proxy settings for the Wire desktop application can be pre-configured. This requires Wire version >= 3.27. - -Create a file named ``init.json`` and set ``customWebAppURL`` and optionally ``proxyServerURL`` e.g. as follows: - -.. code-block:: json - - { - "customWebAppURL": "https://app.custom-wire.com", - "env": "CUSTOM", - "proxyServerURL": "http://127.0.0.1:3128", - } - -The ``env`` setting must be set to ``CUSTOM`` for this to work. - -.. note:: - - Consult your site admin to learn what goes into these settings. The value of ``customWebAppURL`` can be found `here `_ or `resp. here `_. The value of ``proxyServerURL`` is your browser proxy. It depends on the configuration of the network your client is running in. - -Windows -~~~~~~~ - -Move the ``init.json`` file to ``%APPDATA%\Wire\config\init.json`` if it does not already exist. Otherwise update it accordingly. - -MacOS -~~~~~ - -Move the ``init.json`` file to - -:: - - ~/Library/Containers/com.wearezeta.zclient.mac/Data/Library/Application\ Support/Wire/config/init.json - -if it does not already exist. Otherwise, update it accordingly. - -Linux -~~~~~ - -On Linux the ``init.json`` file should be located in the following directory: - -:: - - $HOME/.config/Wire/config/init.json diff --git a/docs/src/how-to/associate/custom-certificates.rst b/docs/src/how-to/associate/custom-certificates.md similarity index 80% rename from docs/src/how-to/associate/custom-certificates.rst rename to docs/src/how-to/associate/custom-certificates.md index 3a5c15b852..2c52391570 100644 --- a/docs/src/how-to/associate/custom-certificates.rst +++ b/docs/src/how-to/associate/custom-certificates.md @@ -1,10 +1,9 @@ -Custom root certificates -------------------------- +# Custom root certificates In case you have installed wire-server using certificates signed using a custom root CA (certificate authority) which is not trusted by default by browsers and systems, then you need to ensure Wire-clients (on Android, Desktop, iOS, and the Web) trust this root certificate. The following details the procedure for Desktop and Web on Linux/Windows: -https://thomas-leister.de/en/how-to-import-ca-root-certificate/ + For Android and iOS, if you know how to trust custom certificates, please let use know so we can update this documentation. diff --git a/docs/src/how-to/associate/deeplink.md b/docs/src/how-to/associate/deeplink.md new file mode 100644 index 0000000000..d3bfc77e27 --- /dev/null +++ b/docs/src/how-to/associate/deeplink.md @@ -0,0 +1,174 @@ +# Using a Deep Link to connect an App to a Custom Backend + +## Introduction + +Once you have your own wire-server set up and configured, you may want to use a client other than the web interface (webapp). There are a few ways to accomplish this: + +- **Using a Deep Link** (which this page is all about) +- Registering your backend instance with the hosted SaaS backend for re-direction. For which you might need to talk to the folks @ Wire (the company). + +Assumptions: + +- You have wire-server installed and working +- You have a familiarity with JSON files +- You can place a JSON file on an HTTPS supporting web server somewhere your users can reach. + +Supported client apps: + +- iOS +- Android + +```{note} +Wire deeplinks can be used to redirect a mobile (Android, iOS) Wire app to a specific backend URL. Deeplinks have no further ability implemented at this stage. +``` + +## Connecting to a custom backend utilizing a Deep Link + +A deep link is a special link a user can click on after installing wire, but before setting it up. This link instructs their wire client to connect to your wire-server, rather than wire.com. + +### With Added Proxy + +In addition to connect to a custom backend a user can specify a socks proxy to add another layer to the network and make the api calls go through the proxy. + +## From a user's perspective: + +1. First, a user installs the app from the store +2. The user clicks on a deep link, which is formatted similar to: `wire://access/?config=https://eu-north2.mycustomdomain.de/configs/backend1.json` (notice the protocol prefix: `wire://`) +3. The app will ask the user to confirm that they want to connect to a custom backend. If the user cancels, the app exits. +4. Assuming the user did not cancel, the app will download the file `eu-north2.mycustomdomain.de/configs/backend1.json` via HTTPS. If it can't download the file or the file doesn't match the expected structure, the wire client will display an error message (*'sInvalid link'*). +5. The app will memorize the various hosts (REST, websocket, team settings, website, support) specified in the JSON and use those when talking to your backend. +6. In the welcome page of the app, a "pill" (header) is shown at the top, to remind the user that they are now on a custom backend. A button "Show more" shows the URL of where the configuration was fetched from. + +### With Added Proxy + +In addition to the previous points + +7. The app will remember the (proxy host, proxy port, if the proxy need authentication) +8. In the login page the user will see new section to add the proxy credentials if the proxy need authentication + +## From the administrator's (your) perspective: + +You need to host two static files, then let your users know how to connect. There are three options listed (in order of recommendation) for hosting the static files. + +Note on the meaning of the URLs used below: + +`backendURL` + +: Use the backend API entrypoint URL, by convention `https://nginz-https.` + +`backendWSURL` + +: Use the backend Websocket API entrypoint URL, by convention `https://nginz-ssl.` + +`teamsURL` + +: Use the URL to the team settings part of the webapp, by convention `https://teams.` + +`accountsURL` + +: Use the URL to the account pages part of the webapp, by convention `https://account.` + +`blackListURL` + +: is used to disable old versions of Wire clients (mobile apps). It's a prefix URL to which e.g. `/ios` or `/android` is appended. Example URL for the wire.com production servers: `https://clientblacklist.wire.com/prod` and example json files: [android](https://clientblacklist.wire.com/prod/android) and [iPhone](https://clientblacklist.wire.com/prod/ios) . + +`websiteURL` + +: Is used as a basis for a few links within the app pointing to FAQs and troubleshooting pages for end users. You can leave this as `https://wire.com` or host your own alternative pages and point this to your own website with the equivalent pages references from within the app. + +`title` + +: Arbitrary string that may show up in a few places in the app. Should be used as an identifier of the backend servers in question. + +### With Added Proxy + +`apiProxy:host (optional)` + +: Is used to specify a proxy to be added to the network engine, so the API calls will go through it to add more security layer. + +`apiProxy:port (optional)` + +: Is used to specify the port number for the proxy when we create the proxy object in the network layer. + +`apiProxy:needsAuthentication (optional)` + +: Is used to specify if the proxy need an authentication, so we can show the section during the login to enter the proxy credentials. + +#### Host a deeplink together with your Wire installation + +As of release `2.117.0` from `2021-10-29` (see `release notes`), you can configure your deeplink endpoints to match your installation and DNS records (see explanations above) + +```yaml +# override values for wire-server +# (e.g. under ./helm_vars/wire-server/values.yaml) +nginz: + nginx_conf: + deeplink: + endpoints: + backendURL: "https://nginz-https.example.com" + backendWSURL: "https://nginz-ssl.example.com" + teamsURL: "https://teams.example.com" + accountsURL: "https://account.example.com" + blackListURL: "https://clientblacklist.wire.com/prod" + websiteURL: "https://wire.com" + apiProxy: # (optional) + host: "socks5.proxy.com" + port: 1080 + needsAuthentication: true + title: "My Custom Wire Backend" +``` + +(As with any configuration changes, you need to apply them following your usual way of updating configuration (e.g. 'helm upgrade...')) + +Now both static files should become accessible at the backend domain under `/deeplink.json` and `deeplink.html`: + +- `https://nginz-https./deeplink.json` +- `https://nginz-https./deeplink.html` + +#### Host a deeplink using minio (deprecated) + +*If possible, prefer the option in the subsection above or below. This subsection is kept for backwards compatibility.* + +**If you're using minio** installed using the ansible code from [wire-server-deploy](https://github.com/wireapp/wire-server-deploy/blob/master/ansible/), then the [minio ansible playbook](https://github.com/wireapp/wire-server-deploy/blob/master/ansible/minio.yml#L75-L88) (make sure to override these variables) creates a json and a html file in the right format, and makes it accessible at `https://assets./public/deeplink.json` and at `https://assets./public/deeplink.html` + +#### Host a deeplink file using your own web server + +Otherwise you need to create a `.json` file, and host it somewhere users can get to. This `.json` file needs to specify the URLs of your backend. For the production wire server that we host, the JSON would look like: + +```json +{ + "endpoints" : { + "backendURL" : "https://prod-nginz-https.wire.com", + "backendWSURL" : "https://prod-nginz-ssl.wire.com", + "blackListURL" : "https://clientblacklist.wire.com/prod", + "teamsURL" : "https://teams.wire.com", + "accountsURL" : "https://accounts.wire.com", + "websiteURL" : "https://wire.com" + }, + "apiProxy" : { + "host" : "socks5.proxy.com", + "port" : 1080, + "needsAuthentication" : true + }, + "title" : "Production" +} +``` + +**IMPORTANT NOTE:** Clients require **ALL** keys to be present in the JSON file; if some of these keys are irrelevant to your installation (e.g., you don't have a websiteURL) you can leave these values as indicated in the above example. + +There is no requirement for these hosts to be consistent, e.g. the REST endpoint could be `wireapp.pineapple.com` and the team setting `teams.banana.com`. If you have been following this documentation closely, these hosts will likely be consistent in naming, regardless. + +You now need to get a link referring to that `.json` file to your users, prepended with `wire://access/?config=`. For example, you can save the above `.json` file as `https://example.com/wire.json`, and save the following HTML content as `https://example.com/wire.html`: + +```html + + + + link + + +``` + +## Next steps + +Now, you can e.g. email or otherwise provide a link to the deeplink HTML page to your users on their mobile devices, and they can follow the above procedure, by clicking on `link`. diff --git a/docs/src/how-to/associate/deeplink.rst b/docs/src/how-to/associate/deeplink.rst deleted file mode 100644 index 1ef53550b1..0000000000 --- a/docs/src/how-to/associate/deeplink.rst +++ /dev/null @@ -1,174 +0,0 @@ -Using a Deep Link to connect an App to a Custom Backend -======================================================= - -Introduction ------------- - -Once you have your own wire-server set up and configured, you may want to use a client other than the web interface (webapp). There are a few ways to accomplish this: - -- **Using a Deep Link** (which this page is all about) -- Registering your backend instance with the hosted SaaS backend for re-direction. For which you might need to talk to the folks @ Wire (the company). - -Assumptions: - -- You have wire-server installed and working -- You have a familiarity with JSON files -- You can place a JSON file on an HTTPS supporting web server somewhere your users can reach. - -Supported client apps: - -- iOS -- Android - -.. note:: - Wire deeplinks can be used to redirect a mobile (Android, iOS) Wire app to a specific backend URL. Deeplinks have no further ability implemented at this stage. - -Connecting to a custom backend utilizing a Deep Link ----------------------------------------------------- - -A deep link is a special link a user can click on after installing wire, but before setting it up. This link instructs their wire client to connect to your wire-server, rather than wire.com. - -With Added Proxy -~~~~~~~~~~~~~~~~ -In addition to connect to a custom backend a user can specify a socks proxy to add another layer to the network and make the api calls go through the proxy. - -From a user's perspective: --------------------------- - -1. First, a user installs the app from the store -2. The user clicks on a deep link, which is formatted similar to: ``wire://access/?config=https://eu-north2.mycustomdomain.de/configs/backend1.json`` (notice the protocol prefix: ``wire://``) -3. The app will ask the user to confirm that they want to connect to a custom backend. If the user cancels, the app exits. -4. Assuming the user did not cancel, the app will download the file ``eu-north2.mycustomdomain.de/configs/backend1.json`` via HTTPS. If it can't download the file or the file doesn't match the expected structure, the wire client will display an error message (*'sInvalid link'*). -5. The app will memorize the various hosts (REST, websocket, team settings, website, support) specified in the JSON and use those when talking to your backend. -6. In the welcome page of the app, a "pill" (header) is shown at the top, to remind the user that they are now on a custom backend. A button "Show more" shows the URL of where the configuration was fetched from. - -With Added Proxy -~~~~~~~~~~~~~~~~ -In addition to the previous points - -7. The app will remember the (proxy host, proxy port, if the proxy need authentication) -8. In the login page the user will see new section to add the proxy credentials if the proxy need authentication - - -From the administrator's (your) perspective: --------------------------------------------- - -You need to host two static files, then let your users know how to connect. There are three options listed (in order of recommendation) for hosting the static files. - -Note on the meaning of the URLs used below: - -``backendURL`` - Use the backend API entrypoint URL, by convention ``https://nginz-https.`` - -``backendWSURL`` - Use the backend Websocket API entrypoint URL, by convention ``https://nginz-ssl.`` - -``teamsURL`` - Use the URL to the team settings part of the webapp, by convention ``https://teams.`` - -``accountsURL`` - Use the URL to the account pages part of the webapp, by convention ``https://account.`` - -``blackListURL`` - is used to disable old versions of Wire clients (mobile apps). It's a prefix URL to which e.g. `/ios` or `/android` is appended. Example URL for the wire.com production servers: ``https://clientblacklist.wire.com/prod`` and example json files: `android `_ and `iPhone `_ . - -``websiteURL`` - Is used as a basis for a few links within the app pointing to FAQs and troubleshooting pages for end users. You can leave this as ``https://wire.com`` or host your own alternative pages and point this to your own website with the equivalent pages references from within the app. - -``title`` - Arbitrary string that may show up in a few places in the app. Should be used as an identifier of the backend servers in question. - -With Added Proxy -~~~~~~~~~~~~~~~~ - -``apiProxy:host (optional)`` - Is used to specify a proxy to be added to the network engine, so the API calls will go through it to add more security layer. - -``apiProxy:port (optional)`` - Is used to specify the port number for the proxy when we create the proxy object in the network layer. - -``apiProxy:needsAuthentication (optional)`` - Is used to specify if the proxy need an authentication, so we can show the section during the login to enter the proxy credentials. - -Host a deeplink together with your Wire installation -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -As of release ``2.117.0`` from ``2021-10-29`` (see `release notes`), you can configure your deeplink endpoints to match your installation and DNS records (see explanations above) - -.. code:: yaml - - # override values for wire-server - # (e.g. under ./helm_vars/wire-server/values.yaml) - nginz: - nginx_conf: - deeplink: - endpoints: - backendURL: "https://nginz-https.example.com" - backendWSURL: "https://nginz-ssl.example.com" - teamsURL: "https://teams.example.com" - accountsURL: "https://account.example.com" - blackListURL: "https://clientblacklist.wire.com/prod" - websiteURL: "https://wire.com" - apiProxy: # (optional) - host: "socks5.proxy.com" - port: 1080 - needsAuthentication: true - title: "My Custom Wire Backend" - -(As with any configuration changes, you need to apply them following your usual way of updating configuration (e.g. 'helm upgrade...')) - -Now both static files should become accessible at the backend domain under ``/deeplink.json`` and ``deeplink.html``: - -* ``https://nginz-https./deeplink.json`` -* ``https://nginz-https./deeplink.html`` - -Host a deeplink using minio (deprecated) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -*If possible, prefer the option in the subsection above or below. This subsection is kept for backwards compatibility.* - -**If you're using minio** installed using the ansible code from `wire-server-deploy `__, then the `minio ansible playbook `__ (make sure to override these variables) creates a json and a html file in the right format, and makes it accessible at ``https://assets./public/deeplink.json`` and at ``https://assets./public/deeplink.html`` - -Host a deeplink file using your own web server -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Otherwise you need to create a ``.json`` file, and host it somewhere users can get to. This ``.json`` file needs to specify the URLs of your backend. For the production wire server that we host, the JSON would look like: - -.. code:: json - - { - "endpoints" : { - "backendURL" : "https://prod-nginz-https.wire.com", - "backendWSURL" : "https://prod-nginz-ssl.wire.com", - "blackListURL" : "https://clientblacklist.wire.com/prod", - "teamsURL" : "https://teams.wire.com", - "accountsURL" : "https://accounts.wire.com", - "websiteURL" : "https://wire.com" - }, - "apiProxy" : { - "host" : "socks5.proxy.com", - "port" : 1080, - "needsAuthentication" : true - }, - "title" : "Production" - } - -**IMPORTANT NOTE:** Clients require **ALL** keys to be present in the JSON file; if some of these keys are irrelevant to your installation (e.g., you don't have a websiteURL) you can leave these values as indicated in the above example. - -There is no requirement for these hosts to be consistent, e.g. the REST endpoint could be `wireapp.pineapple.com` and the team setting `teams.banana.com`. If you have been following this documentation closely, these hosts will likely be consistent in naming, regardless. - -You now need to get a link referring to that ``.json`` file to your users, prepended with ``wire://access/?config=``. For example, you can save the above ``.json`` file as ``https://example.com/wire.json``, and save the following HTML content as ``https://example.com/wire.html``: - -.. code:: html - - - - - link - - - -Next steps ----------- - -Now, you can e.g. email or otherwise provide a link to the deeplink HTML page to your users on their mobile devices, and they can follow the above procedure, by clicking on ``link``. diff --git a/docs/src/how-to/associate/index.md b/docs/src/how-to/associate/index.md new file mode 100644 index 0000000000..3dba99c8f2 --- /dev/null +++ b/docs/src/how-to/associate/index.md @@ -0,0 +1,10 @@ +# Connecting Wire Clients + +```{toctree} +:glob: true +:maxdepth: 2 + + How to associate a wire client to a custom backend using a deep link + How to use custom root certificates with wire clients + How to use a custom backend with the desktop client +``` diff --git a/docs/src/how-to/associate/index.rst b/docs/src/how-to/associate/index.rst deleted file mode 100644 index 95c7d790f5..0000000000 --- a/docs/src/how-to/associate/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -Connecting Wire Clients -======================= - -.. toctree:: - :maxdepth: 2 - :glob: - - How to associate a wire client to a custom backend using a deep link - How to use custom root certificates with wire clients - How to use a custom backend with the desktop client diff --git a/docs/src/how-to/index.md b/docs/src/how-to/index.md new file mode 100644 index 0000000000..46bf098a9a --- /dev/null +++ b/docs/src/how-to/index.md @@ -0,0 +1,20 @@ +# Administrator's Guide + +Documentation on the installation, deployment and administration of Wire +server components. + +```{warning} +If you already installed Wire by using `poetry`, please refer to the +[old version](https://docs.wire.com/versions/install-with-poetry/how-to/index.html) of +the installation guide. +``` + +```{toctree} +:glob: true +:maxdepth: 2 + + How to install wire-server + How to verify your wire-server installation + How to administrate servers after successful installation + How to connect the public wire clients to your wire-server installation +``` diff --git a/docs/src/how-to/index.rst b/docs/src/how-to/index.rst deleted file mode 100644 index 1ba77e0302..0000000000 --- a/docs/src/how-to/index.rst +++ /dev/null @@ -1,21 +0,0 @@ -Administrator's Guide -===================== - -Documentation on the installation, deployment and administration of Wire -server components. - -.. warning:: - - If you already installed Wire by using ``poetry``, please refer to the - `old version `__ of - the installation guide. - - -.. toctree:: - :maxdepth: 2 - :glob: - - How to install wire-server - How to verify your wire-server installation - How to administrate servers after successful installation - How to connect the public wire clients to your wire-server installation diff --git a/docs/src/how-to/install/ansible-VMs.md b/docs/src/how-to/install/ansible-VMs.md new file mode 100644 index 0000000000..2627eea5d9 --- /dev/null +++ b/docs/src/how-to/install/ansible-VMs.md @@ -0,0 +1,275 @@ +(ansible-vms)= + +# Installing kubernetes and databases on VMs with ansible + +## Introduction + +In a production environment, some parts of the wire-server +infrastructure (such as e.g. cassandra databases) are best configured +outside kubernetes. Additionally, kubernetes can be rapidly set up with +kubespray, via ansible. This section covers installing VMs with ansible. + +## Assumptions + +- A bare-metal setup (no cloud provider) +- All machines run ubuntu 18.04 +- All machines have static IP addresses +- Time on all machines is being kept in sync +- You have the following virtual machines: + +```{eval-rst} +.. include:: includes/vm-table.rst +``` + +(It's up to you how you create these machines - kvm on a bare metal +machine, VM on a cloud provider, real physical machines, etc.) + +## Preparing to run ansible + +(adding-ips-to-hostsini)= + +% TODO: section header unifications/change + +### Adding IPs to hosts.ini + +Go to your checked-out wire-server-deploy/ansible folder: + +``` +cd wire-server-deploy/ansible +``` + +Copy the example hosts file: + +``` +cp hosts.example.ini hosts.ini +``` + +- Edit the hosts.ini, setting the permanent IPs of the hosts you are + setting up wire on. +- On each of the lines declaring a database service node ( + lines in the `[all]` section beginning with cassandra, elasticsearch, + or minio) replace the `ansible_host` values (`X.X.X.X`) with the + IPs of the nodes that you can connect to via SSH. these are the + 'internal' addresses of the machines, not what a client will be + connecting to. +- On each of the lines declaring a kubernetes node (lines in the `[all]` + section starting with 'kubenode') replace the `ip` values + (`Y.Y.Y.Y`) with the IPs which you wish kubernetes to provide + services to clients on, and replace the `ansible_host` values + (`X.X.X.X`) with the IPs of the nodes that you can connect to via + SSH. If the IP you want to provide services on is the same IP that + you use to connect, remove the `ip=Y.Y.Y.Y` completely. +- On each of the lines declaring an `etcd` node (lines in the `[all]` + section starting with etcd), use the same values as you used on the + coresponding kubenode lines in the prior step. +- If you are deploying Restund for voice/video services then on each of the + lines declaring a `restund` node (lines in the `[all]` section + beginning with restund), replace the `ansible_host` values (`X.X.X.X`) + with the IPs of the nodes that you can connect to via SSH. +- Edit the minio variables in `[minio:vars]` (`prefix`, `domain` and `deeplink_title`) + by replacing `example.com` with your own domain. + +There are more settings in this file that we will set in later steps. + +% TODO: remove this warning, and remove the hostname run from the cassandra playbook, or find another way to deal with it. + +```{warning} +Some of these playbooks mess with the hostnames of their targets. You +MUST pick different hosts for playbooks that rename the host. If you +e.g. attempt to run Cassandra and k8s on the same 3 machines, the +hostnames will be overwritten by the second installation playbook, +breaking the first. + +At the least, we know that the cassandra, kubernetes and restund playbooks are +guilty of hostname manipulation. +``` + +### Authentication + +```{eval-rst} +.. include:: includes/ansible-authentication-blob.rst +``` + +## Running ansible to install software on your machines + +You can install kubernetes, cassandra, restund, etc in any order. + +```{note} +In case you only have a single network interface with public IPs but wish to protect inter-database communication, you may use the `tinc.yml` playbook to create a private network interface. In this case, ensure tinc is setup BEFORE running any other playbook. See {ref}`tinc` +``` + +### Installing kubernetes + +Kubernetes is installed via ansible. + +To install kubernetes: + +From `wire-server-deploy/ansible`: + +``` +ansible-playbook -i hosts.ini kubernetes.yml -vv +``` + +When the playbook finishes correctly (which can take up to 20 minutes), you should have a folder `artifacts` containing a file `admin.conf`. Copy this file: + +``` +mkdir -p ~/.kube +cp artifacts/admin.conf ~/.kube/config +``` + +Make sure you can reach the server: + +``` +kubectl version +``` + +should give output similar to this: + +``` +Client Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.7", GitCommit:"1dd5338295409edcfff11505e7bb246f0d325d15", GitTreeState:"clean", BuildDate:"2021-01-13T13:23:52Z", GoVersion:"go1.15.5", Compiler:"gc", Platform:"linux/amd64"} +Server Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.7", GitCommit:"1dd5338295409edcfff11505e7bb246f0d325d15", GitTreeState:"clean", BuildDate:"2021-01-13T13:15:20Z", GoVersion:"go1.15.5", Compiler:"gc", Platform:"linux/amd64"} +``` + +### Cassandra + +- If you would like to change the name of the cluster, in your + 'hosts.ini' file, in the `[cassandra:vars]` section, uncomment + the line that changes 'cassandra_clustername', and change default + to be the name you want the cluster to have. +- If you want cassandra nodes to talk to each other on a specific + network interface, rather than the one you use to connect via SSH, + In your 'hosts.ini' file, in the `[all:vars]` section, + uncomment, and set 'cassandra_network_interface' to the name of + the ethernet interface you want cassandra nodes to talk to each + other on. For example: + +```ini +[cassandra:vars] +# cassandra_clustername: default + +[all:vars] +## set to True if using AWS +is_aws_environment = False +## Set the network interface name for cassandra to bind to if you have more than one network interface +cassandra_network_interface = eth0 +``` + +(see +[defaults/main.yml](https://github.com/wireapp/ansible-cassandra/blob/master/defaults/main.yml) +for a full list of variables to change if necessary) + +- Use ansible to deploy Cassandra: + +``` +ansible-playbook -i hosts.ini cassandra.yml -vv +``` + +### ElasticSearch + +- In your 'hosts.ini' file, in the `[all:vars]` section, uncomment + and set 'elasticsearch_network_interface' to the name of the + interface you want elasticsearch nodes to talk to each other on. +- If you are performing an offline install, or for some other reason + are using an APT mirror other than the default to retrieve + elasticsearch-oss packages from, you need to specify that mirror + by setting 'es_apt_key' and 'es_apt_url' in the `[all:vars]` + section of your hosts.ini file. + +```ini +[all:vars] +# default first interface on ubuntu on kvm: +elasticsearch_network_interface=ens3 + +## Set these in order to use an APT mirror other than the default. +# es_apt_key = "https:///linux/ubuntu/gpg" +# es_apt_url = "deb [trusted=yes] https:///apt bionic stable" +``` + +- Use ansible and deploy ElasticSearch: + +``` +ansible-playbook -i hosts.ini elasticsearch.yml -vv +``` + +### Minio + +Minio is used for asset storage, in the case that you are not +running on AWS infrastructure, or feel uncomfortable storing assets +in S3 in encrypted form. If you are using S3 instead of Minio, skip +this step. + +- In your 'hosts.ini' file, in the `[all:vars]` section, make sure + you set the 'minio_network_interface' to the name of the interface + you want minio nodes to talk to each other on. The default from the + playbook is not going to be correct for your machine. For example: +- In your 'hosts.ini' file, in the `[minio:vars]` section, ensure you + set minio_access_key and minio_secret key. +- If you intend to use a `deep link` to configure your clients to + talk to the backend, you need to specify your domain (and optionally + your prefix), so that links to your deep link json file are generated + correctly. By configuring these values, you fill in the blanks of + `https://{{ prefix }}assets.{{ domain }}`. + +```ini +[minio:vars] +minio_access_key = "REPLACE_THIS_WITH_THE_DESIRED_SECRET_KEY" +minio_secret_key = "REPLACE_THIS_WITH_THE_DESIRED_SECRET_KEY" +# if you want to use deep links for client configuration: +#minio_deeplink_prefix = "" +#minio_deeplink_domain = "example.com" + +[all:vars] +# Default first interface on ubuntu on kvm: +minio_network_interface=ens3 +``` + +- Use ansible, and deploy Minio: + +``` +ansible-playbook -i hosts.ini minio.yml -vv +``` + +### Restund + +For instructions on how to install Restund, see {ref}`this page `. + +### IMPORTANT checks + +> After running the above playbooks, it is important to ensure that everything is setup correctly. Please have a look at the post install checks in the section {ref}`checks` + +``` +ansible-playbook -i hosts.ini cassandra-verify-ntp.yml -vv +``` + +### Installing helm charts - prerequisites + +The `helm_external.yml` playbook is used to write or update the IPs of the +databases servers in the `values/-external/values.yaml` files, and +thus make them available for helm and the `-external` charts (e.g. +`cassandra-external`, `elasticsearch-external`, etc). + +Due to limitations in the playbook, make sure that you have defined the +network interfaces for each of the database services in your hosts.ini, +even if they are running on the same interface that you connect to via SSH. +In your hosts.ini under `[all:vars]`: + +```ini +[all:vars] +minio_network_interface = ... +cassandra_network_interface = ... +elasticsearch_network_interface = ... +# if you're using redis external... +redis_network_interface = ... +``` + +Now run the helm_external.yml playbook, to populate network values for helm: + +``` +ansible-playbook -i hosts.ini -vv --diff helm_external.yml +``` + +You can now can install the helm charts. + +#### Next steps for high-available production installation + +Your next step will be {ref}`helm-prod` diff --git a/docs/src/how-to/install/ansible-VMs.rst b/docs/src/how-to/install/ansible-VMs.rst deleted file mode 100644 index 46c818f211..0000000000 --- a/docs/src/how-to/install/ansible-VMs.rst +++ /dev/null @@ -1,277 +0,0 @@ -.. _ansible_vms: - -Installing kubernetes and databases on VMs with ansible -======================================================= - -Introduction ------------- - -In a production environment, some parts of the wire-server -infrastructure (such as e.g. cassandra databases) are best configured -outside kubernetes. Additionally, kubernetes can be rapidly set up with -kubespray, via ansible. This section covers installing VMs with ansible. - -Assumptions ------------ - -- A bare-metal setup (no cloud provider) -- All machines run ubuntu 18.04 -- All machines have static IP addresses -- Time on all machines is being kept in sync -- You have the following virtual machines: - -.. include:: includes/vm-table.rst - -(It's up to you how you create these machines - kvm on a bare metal -machine, VM on a cloud provider, real physical machines, etc.) - -Preparing to run ansible ------------------------- - -.. _adding-ips-to-hostsini: - -.. TODO: section header unifications/change - -Adding IPs to hosts.ini -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Go to your checked-out wire-server-deploy/ansible folder:: - - cd wire-server-deploy/ansible - -Copy the example hosts file:: - - cp hosts.example.ini hosts.ini - -- Edit the hosts.ini, setting the permanent IPs of the hosts you are - setting up wire on. -- On each of the lines declaring a database service node ( - lines in the ``[all]`` section beginning with cassandra, elasticsearch, - or minio) replace the ``ansible_host`` values (``X.X.X.X``) with the - IPs of the nodes that you can connect to via SSH. these are the - 'internal' addresses of the machines, not what a client will be - connecting to. -- On each of the lines declaring a kubernetes node (lines in the ``[all]`` - section starting with 'kubenode') replace the ``ip`` values - (``Y.Y.Y.Y``) with the IPs which you wish kubernetes to provide - services to clients on, and replace the ``ansible_host`` values - (``X.X.X.X``) with the IPs of the nodes that you can connect to via - SSH. If the IP you want to provide services on is the same IP that - you use to connect, remove the ``ip=Y.Y.Y.Y`` completely. -- On each of the lines declaring an ``etcd`` node (lines in the ``[all]`` - section starting with etcd), use the same values as you used on the - coresponding kubenode lines in the prior step. -- If you are deploying Restund for voice/video services then on each of the - lines declaring a ``restund`` node (lines in the ``[all]`` section - beginning with restund), replace the ``ansible_host`` values (``X.X.X.X``) - with the IPs of the nodes that you can connect to via SSH. -- Edit the minio variables in ``[minio:vars]`` (``prefix``, ``domain`` and ``deeplink_title``) - by replacing ``example.com`` with your own domain. - -There are more settings in this file that we will set in later steps. - -.. TODO: remove this warning, and remove the hostname run from the cassandra playbook, or find another way to deal with it. - -.. warning:: - - Some of these playbooks mess with the hostnames of their targets. You - MUST pick different hosts for playbooks that rename the host. If you - e.g. attempt to run Cassandra and k8s on the same 3 machines, the - hostnames will be overwritten by the second installation playbook, - breaking the first. - - At the least, we know that the cassandra, kubernetes and restund playbooks are - guilty of hostname manipulation. - -Authentication -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. include:: includes/ansible-authentication-blob.rst - -Running ansible to install software on your machines ------------------------------------------------------ - -You can install kubernetes, cassandra, restund, etc in any order. - -.. note:: - - In case you only have a single network interface with public IPs but wish to protect inter-database communication, you may use the ``tinc.yml`` playbook to create a private network interface. In this case, ensure tinc is setup BEFORE running any other playbook. See :ref:`tinc` - -Installing kubernetes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Kubernetes is installed via ansible. - -To install kubernetes: - -From ``wire-server-deploy/ansible``:: - - ansible-playbook -i hosts.ini kubernetes.yml -vv - -When the playbook finishes correctly (which can take up to 20 minutes), you should have a folder ``artifacts`` containing a file ``admin.conf``. Copy this file:: - - mkdir -p ~/.kube - cp artifacts/admin.conf ~/.kube/config - -Make sure you can reach the server:: - - kubectl version - -should give output similar to this:: - - Client Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.7", GitCommit:"1dd5338295409edcfff11505e7bb246f0d325d15", GitTreeState:"clean", BuildDate:"2021-01-13T13:23:52Z", GoVersion:"go1.15.5", Compiler:"gc", Platform:"linux/amd64"} - Server Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.7", GitCommit:"1dd5338295409edcfff11505e7bb246f0d325d15", GitTreeState:"clean", BuildDate:"2021-01-13T13:15:20Z", GoVersion:"go1.15.5", Compiler:"gc", Platform:"linux/amd64"} - -Cassandra -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -- If you would like to change the name of the cluster, in your - 'hosts.ini' file, in the ``[cassandra:vars]`` section, uncomment - the line that changes 'cassandra_clustername', and change default - to be the name you want the cluster to have. -- If you want cassandra nodes to talk to each other on a specific - network interface, rather than the one you use to connect via SSH, - In your 'hosts.ini' file, in the ``[all:vars]`` section, - uncomment, and set 'cassandra_network_interface' to the name of - the ethernet interface you want cassandra nodes to talk to each - other on. For example: - -.. code:: ini - - [cassandra:vars] - # cassandra_clustername: default - - [all:vars] - ## set to True if using AWS - is_aws_environment = False - ## Set the network interface name for cassandra to bind to if you have more than one network interface - cassandra_network_interface = eth0 - -(see -`defaults/main.yml `__ -for a full list of variables to change if necessary) - -- Use ansible to deploy Cassandra: - -:: - - ansible-playbook -i hosts.ini cassandra.yml -vv - -ElasticSearch -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -- In your 'hosts.ini' file, in the ``[all:vars]`` section, uncomment - and set 'elasticsearch_network_interface' to the name of the - interface you want elasticsearch nodes to talk to each other on. -- If you are performing an offline install, or for some other reason - are using an APT mirror other than the default to retrieve - elasticsearch-oss packages from, you need to specify that mirror - by setting 'es_apt_key' and 'es_apt_url' in the ``[all:vars]`` - section of your hosts.ini file. - -.. code:: ini - - [all:vars] - # default first interface on ubuntu on kvm: - elasticsearch_network_interface=ens3 - - ## Set these in order to use an APT mirror other than the default. - # es_apt_key = "https:///linux/ubuntu/gpg" - # es_apt_url = "deb [trusted=yes] https:///apt bionic stable" - -- Use ansible and deploy ElasticSearch: - -:: - - ansible-playbook -i hosts.ini elasticsearch.yml -vv - -Minio -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Minio is used for asset storage, in the case that you are not -running on AWS infrastructure, or feel uncomfortable storing assets -in S3 in encrypted form. If you are using S3 instead of Minio, skip -this step. - - -- In your 'hosts.ini' file, in the ``[all:vars]`` section, make sure - you set the 'minio_network_interface' to the name of the interface - you want minio nodes to talk to each other on. The default from the - playbook is not going to be correct for your machine. For example: -- In your 'hosts.ini' file, in the ``[minio:vars]`` section, ensure you - set minio_access_key and minio_secret key. -- If you intend to use a ``deep link`` to configure your clients to - talk to the backend, you need to specify your domain (and optionally - your prefix), so that links to your deep link json file are generated - correctly. By configuring these values, you fill in the blanks of - ``https://{{ prefix }}assets.{{ domain }}``. - -.. code:: ini - - [minio:vars] - minio_access_key = "REPLACE_THIS_WITH_THE_DESIRED_SECRET_KEY" - minio_secret_key = "REPLACE_THIS_WITH_THE_DESIRED_SECRET_KEY" - # if you want to use deep links for client configuration: - #minio_deeplink_prefix = "" - #minio_deeplink_domain = "example.com" - - [all:vars] - # Default first interface on ubuntu on kvm: - minio_network_interface=ens3 - -- Use ansible, and deploy Minio: - -:: - - ansible-playbook -i hosts.ini minio.yml -vv - -Restund -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -For instructions on how to install Restund, see :ref:`this page `. - - -IMPORTANT checks -^^^^^^^^^^^^^^^^ - - After running the above playbooks, it is important to ensure that everything is setup correctly. Please have a look at the post install checks in the section :ref:`checks` - -:: - - ansible-playbook -i hosts.ini cassandra-verify-ntp.yml -vv - -Installing helm charts - prerequisites -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The ``helm_external.yml`` playbook is used to write or update the IPs of the -databases servers in the ``values/-external/values.yaml`` files, and -thus make them available for helm and the ``-external`` charts (e.g. -``cassandra-external``, ``elasticsearch-external``, etc). - -Due to limitations in the playbook, make sure that you have defined the -network interfaces for each of the database services in your hosts.ini, -even if they are running on the same interface that you connect to via SSH. -In your hosts.ini under ``[all:vars]``: - -.. code:: ini - - [all:vars] - minio_network_interface = ... - cassandra_network_interface = ... - elasticsearch_network_interface = ... - # if you're using redis external... - redis_network_interface = ... - - -Now run the helm_external.yml playbook, to populate network values for helm: - -:: - - ansible-playbook -i hosts.ini -vv --diff helm_external.yml - -You can now can install the helm charts. - -Next steps for high-available production installation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Your next step will be :ref:`helm_prod` diff --git a/docs/src/how-to/install/ansible-authentication.md b/docs/src/how-to/install/ansible-authentication.md new file mode 100644 index 0000000000..9943d7514b --- /dev/null +++ b/docs/src/how-to/install/ansible-authentication.md @@ -0,0 +1,63 @@ +(ansible-authentication)= + +# Manage ansible authentication settings + +Ansible works best if + +- you use ssh keys, not passwords +- the user you use to ssh is either `root` or can become `root` (can run `sudo su -`) without entering a password + +However, other options are possible, see below: + +## How to use password authentication when you ssh to a machine with ansible + +If, instead of using ssh keys to ssh to a remote machine, you want to use passwords: + +``` +sudo apt install sshpass +``` + +- in hosts.ini, uncomment the 'ansible_user = ...' line, and change '...' to the user you want to login as. +- in hosts.ini, uncomment the 'ansible_ssh_pass = ...' line, and change '...' to the password for the user you are logging in as. +- in hosts.ini, uncomment the 'ansible_become_pass = ...' line, and change the ... to the password you'd enter to sudo. + +## Configuring SSH keys + +(from ) If you +want a bit higher security, you can copy SSH keys between the machine +you are administrating with, and the machines you are managing with +ansible. + +- Create an SSH key. + +``` +ssh-keygen -t rsa +``` + +- Install your SSH key on each of the machines you are managing with + ansible, so that you can SSH into them without a password: + +``` +ssh-copy-id -i ~/.ssh/id_rsa.pub $USERNAME@$IP +``` + +Replace `$USERNAME` with the username of the account you set up when +you installed the machine. + +## Sudo without password + +Ansible can be configured to use a password for switching from the +unpriviledged \$USERNAME to the root user. This involves having the +password lying about, so has security problems. If you want ansible to +not be prompted for any administrative command (a different security +problem!): + +- As root on each of the nodes, add the following line at the end of + the /etc/sudoers file: + +``` + ALL=(ALL) NOPASSWD:ALL +``` + +Replace `` with the username of the account +you set up when you installed the machine. diff --git a/docs/src/how-to/install/ansible-authentication.rst b/docs/src/how-to/install/ansible-authentication.rst deleted file mode 100644 index 8e549fb64c..0000000000 --- a/docs/src/how-to/install/ansible-authentication.rst +++ /dev/null @@ -1,66 +0,0 @@ -.. _ansible-authentication: - -Manage ansible authentication settings -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Ansible works best if - -* you use ssh keys, not passwords -* the user you use to ssh is either ``root`` or can become ``root`` (can run ``sudo su -``) without entering a password - -However, other options are possible, see below: - - -How to use password authentication when you ssh to a machine with ansible -'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' - -If, instead of using ssh keys to ssh to a remote machine, you want to use passwords:: - - sudo apt install sshpass - -* in hosts.ini, uncomment the 'ansible_user = ...' line, and change '...' to the user you want to login as. -* in hosts.ini, uncomment the 'ansible_ssh_pass = ...' line, and change '...' to the password for the user you are logging in as. -* in hosts.ini, uncomment the 'ansible_become_pass = ...' line, and change the ... to the password you'd enter to sudo. - -Configuring SSH keys -'''''''''''''''''''' - -(from https://linoxide.com/how-tos/ssh-login-with-public-key/) If you -want a bit higher security, you can copy SSH keys between the machine -you are administrating with, and the machines you are managing with -ansible. - -- Create an SSH key. - -:: - - ssh-keygen -t rsa - -- Install your SSH key on each of the machines you are managing with - ansible, so that you can SSH into them without a password: - -:: - - ssh-copy-id -i ~/.ssh/id_rsa.pub $USERNAME@$IP - -Replace ``$USERNAME`` with the username of the account you set up when -you installed the machine. - -Sudo without password -''''''''''''''''''''' - -Ansible can be configured to use a password for switching from the -unpriviledged $USERNAME to the root user. This involves having the -password lying about, so has security problems. If you want ansible to -not be prompted for any administrative command (a different security -problem!): - -- As root on each of the nodes, add the following line at the end of - the /etc/sudoers file: - -:: - - ALL=(ALL) NOPASSWD:ALL - -Replace ```` with the username of the account -you set up when you installed the machine. diff --git a/docs/src/how-to/install/ansible-tinc.md b/docs/src/how-to/install/ansible-tinc.md new file mode 100644 index 0000000000..294c1faa99 --- /dev/null +++ b/docs/src/how-to/install/ansible-tinc.md @@ -0,0 +1,54 @@ +(tinc)= + +# tinc + +Installing [tinc mesh vpn](http://tinc-vpn.org/) is *optional and +experimental*. It allows having a private network interface `vpn0` on +the target VMs. + +```{warning} +We currently only use tinc for test clusters and have not made sure if the default settings it comes with provide adequate security to protect your data. If using tinc and the following tinc.yml playbook, make your own checks first! +``` + +```{note} +Ensure to run the tinc.yml playbook first if you use tinc, before +other playbooks. +``` + +From `wire-server-deploy/ansible`, where you created a `hosts.ini` file. + +- Add a `vpn_ip=Z.Z.Z.Z` item to each entry in the hosts file with a + (fresh) IP range if you wish to use tinc. +- Add a group `vpn`: + +```ini +# this is a minimal example +[all] +server1 ansible_host=X.X.X.X vpn_ip=10.10.1.XXX +server2 ansible_host=X.X.X.X vpn_ip=10.10.1.YYY + +[cassandra] +server1 +server2 + +[vpn:children] +cassandra +# add other server groups here as necessary +``` + +Also ensure subsequent playbooks make use of the newly-created interface by setting: + +```ini +[all:vars] +minio_network_interface = vpn0 +cassandra_network_interface = vpn0 +elasticsearch_network_interface = vpn0 +redis_network_interface = vpn0 +``` + +Configure the physical network interface inside tinc.yml if it is not +`eth0`. Then: + +``` +ansible-playbook -i hosts.ini tinc.yml -vv +``` diff --git a/docs/src/how-to/install/ansible-tinc.rst b/docs/src/how-to/install/ansible-tinc.rst deleted file mode 100644 index ca5698b7ab..0000000000 --- a/docs/src/how-to/install/ansible-tinc.rst +++ /dev/null @@ -1,54 +0,0 @@ -.. _tinc: - -tinc ----- - -Installing `tinc mesh vpn `__ is *optional and -experimental*. It allows having a private network interface ``vpn0`` on -the target VMs. - -.. warning:: - We currently only use tinc for test clusters and have not made sure if the default settings it comes with provide adequate security to protect your data. If using tinc and the following tinc.yml playbook, make your own checks first! - -.. note:: - - Ensure to run the tinc.yml playbook first if you use tinc, before - other playbooks. - -From ``wire-server-deploy/ansible``, where you created a `hosts.ini` file. - -- Add a ``vpn_ip=Z.Z.Z.Z`` item to each entry in the hosts file with a - (fresh) IP range if you wish to use tinc. -- Add a group ``vpn``: - -.. code:: ini - - # this is a minimal example - [all] - server1 ansible_host=X.X.X.X vpn_ip=10.10.1.XXX - server2 ansible_host=X.X.X.X vpn_ip=10.10.1.YYY - - [cassandra] - server1 - server2 - - [vpn:children] - cassandra - # add other server groups here as necessary - -Also ensure subsequent playbooks make use of the newly-created interface by setting: - -.. code:: ini - - [all:vars] - minio_network_interface = vpn0 - cassandra_network_interface = vpn0 - elasticsearch_network_interface = vpn0 - redis_network_interface = vpn0 - -Configure the physical network interface inside tinc.yml if it is not -``eth0``. Then: - -:: - - ansible-playbook -i hosts.ini tinc.yml -vv diff --git a/docs/src/how-to/install/aws-prod.md b/docs/src/how-to/install/aws-prod.md new file mode 100644 index 0000000000..0359d98d71 --- /dev/null +++ b/docs/src/how-to/install/aws-prod.md @@ -0,0 +1,36 @@ +(aws-prod)= + +# Configuring AWS and wire-server (production) components + +## Introduction + +The following procedures are for configuring wire-server on top of AWS. They are not required to use wire-server in AWS, but they may be a good idea, depending on the AWS features you are comfortable using. + +## Using real AWS services for SNS + +AWS SNS is required to send notification events to clients via [FCM](https://firebase.google.com/docs/cloud-messaging/)/[APNS](https://developer.apple.com/notifications/) . These notification channels are useable only for clients that are connected from the public internet. Using these vendor provided communication channels allows client devices (phones) running a wire client to save a considerable amount of battery life, compared to the websockets approach. + +For details on how to set up SNS in cooperation with us (We - Wire - will proxy push notifications through Amazon for you), see {ref}`push-sns`. + +## Using real AWS services for SES / SQS + +AWS SES and SQS are used for delivering emails to clients, and for receiving notifications of bounced emails. SQS is also used internally, in order to facilitate batch user deletion. + +FIXME: detail this step. + +## Using real AWS services for S3 + +S3-style services are used by cargohold to store encrypted files that users are sharing amongst each other, profile pics, etc. + +Defining S3 services: +Create an S3 bucket in the region you are hosting your wire servers in. For example terraform code, see: + +The S3 bucket you create should have it's contents downloadable from the internet, as clients get the content directly from S3, rather than having to talk through the wire backend. + +Using S3 services: + +There are three values in the `cargohold.config.aws` section of your 'values.yaml' that you need to provide while deploying wire-server: + +- s3Bucket: the name of the S3 bucket you have created. +- s3Endpoint: the S3 service endpoint cargohold should talk to, to place files in the S3 bucket. On AWS, this takes the form of: `https://.s3-.amazonaws.com`. +- s3DownloadEndpoint: The URL base that clients should use to get contents from the S3 bucket. On AWS, this takes the form of: `https://s3..amazonaws.com`. diff --git a/docs/src/how-to/install/aws-prod.rst b/docs/src/how-to/install/aws-prod.rst deleted file mode 100644 index 0cf147bc20..0000000000 --- a/docs/src/how-to/install/aws-prod.rst +++ /dev/null @@ -1,39 +0,0 @@ -.. _aws_prod: - -Configuring AWS and wire-server (production) components -======================================================= - -Introduction ------------- - -The following procedures are for configuring wire-server on top of AWS. They are not required to use wire-server in AWS, but they may be a good idea, depending on the AWS features you are comfortable using. - -Using real AWS services for SNS --------------------------------------------------------- -AWS SNS is required to send notification events to clients via `FCM `__/`APNS `__ . These notification channels are useable only for clients that are connected from the public internet. Using these vendor provided communication channels allows client devices (phones) running a wire client to save a considerable amount of battery life, compared to the websockets approach. - -For details on how to set up SNS in cooperation with us (We - Wire - will proxy push notifications through Amazon for you), see :ref:`pushsns`. - -Using real AWS services for SES / SQS ---------------------------------------------- -AWS SES and SQS are used for delivering emails to clients, and for receiving notifications of bounced emails. SQS is also used internally, in order to facilitate batch user deletion. - -FIXME: detail this step. - -Using real AWS services for S3 ------------------------------- -S3-style services are used by cargohold to store encrypted files that users are sharing amongst each other, profile pics, etc. - -Defining S3 services: -Create an S3 bucket in the region you are hosting your wire servers in. For example terraform code, see: https://github.com/wireapp/wire-server-deploy/tree/develop/terraform/modules/aws-cargohold-asset-storage - -The S3 bucket you create should have it's contents downloadable from the internet, as clients get the content directly from S3, rather than having to talk through the wire backend. - -Using S3 services: - -There are three values in the ``cargohold.config.aws`` section of your 'values.yaml' that you need to provide while deploying wire-server: - -* s3Bucket: the name of the S3 bucket you have created. -* s3Endpoint: the S3 service endpoint cargohold should talk to, to place files in the S3 bucket. On AWS, this takes the form of: ``https://.s3-.amazonaws.com``. -* s3DownloadEndpoint: The URL base that clients should use to get contents from the S3 bucket. On AWS, this takes the form of: ``https://s3..amazonaws.com``. - diff --git a/docs/src/how-to/install/configuration-options.md b/docs/src/how-to/install/configuration-options.md new file mode 100644 index 0000000000..647ac5f0ee --- /dev/null +++ b/docs/src/how-to/install/configuration-options.md @@ -0,0 +1,1048 @@ +(configuration-options)= + +# Part 3 - configuration options in a production setup + +This contains instructions to configure specific aspects of your production setup depending on your needs. + +Depending on your use-case and requirements, you may need to +configure none, or only a subset of the following sections. + +## Redirect some traffic through a http(s) proxy + +In case you wish to use http(s) proxies, you can add a configuration like this to the wire-server services in question: + +Assuming your proxy can be reached from within Kubernetes at `http://proxy:8080`, add the following for each affected service (e.g. `gundeck`) to your Helm overrides in `values/wire-server/values.yaml` : + +```yaml +gundeck: + # ... + config: + # ... + proxy: + httpProxy: "http://proxy:8080" + httpsProxy: "http://proxy:8080" + noProxyList: + - "localhost" + - "127.0.0.1" + - "10.0.0.0/8" + - "elasticsearch-external" + - "cassandra-external" + - "redis-ephemeral" + - "fake-aws-sqs" + - "fake-aws-dynamodb" + - "fake-aws-sns" + - "brig" + - "cargohold" + - "galley" + - "gundeck" + - "proxy" + - "spar" + - "federator" + - "cannon" + - "cannon-0.cannon.default" + - "cannon-1.cannon.default" + - "cannon-2.cannon.default" +``` + +Depending on your setup, you may need to repeat this for the other services like `brig` as well. + +(push-sns)= + +## Enable push notifications using the public appstore / playstore mobile Wire clients + +1. You need to get in touch with us. Please talk to sales or customer support - see +2. If a contract agreement has been reached, we can set up a separate AWS account for you containing the necessary AWS SQS/SNS setup to route push notifications through to the mobile apps. We will then forward some configuration / access credentials that looks like: + +```yaml +gundeck: + config: + aws: + account: "" + arnEnv: "" + queueName: "-gundeck-events" + region: "" + snsEndpoint: "https://sns..amazonaws.com" + sqsEndpoint: "https://sqs..amazonaws.com" + secrets: + awsKeyId: "" + awsSecretKey: "" +``` + +To make use of those, first test the credentials are correct, e.g. using the `aws` command-line tool (for more information on how to configure credentials, please refer to the [official docs](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-precedence)): + +``` +AWS_REGION= +AWS_ACCESS_KEY_ID=<...> +AWS_SECRET_ACCESS_KEY=<...> +ENV= #e.g staging + +aws sqs get-queue-url --queue-name "$ENV-gundeck-events" +``` + +You should get a result like this: + +``` +{ + "QueueUrl": "https://.queue.amazonaws.com//-gundeck-events" +} +``` + +Then add them to your gundeck configuration overrides. + +Keys below `gundeck.config` belong into `values/wire-server/values.yaml`: + +```yaml +gundeck: + # ... + config: + aws: + queueName: # e.g. staging-gundeck-events + account: # , e.g. 123456789 + region: # e.g. eu-central-1 + snsEndpoint: # e.g. https://sns.eu-central-1.amazonaws.com + sqsEndpoint: # e.g. https://sqs.eu-central-1.amazonaws.com + arnEnv: # e.g. staging - this must match the environment name (first part of queueName) +``` + +Keys below `gundeck.secrets` belong into `values/wire-server/secrets.yaml`: + +```yaml +gundeck: + # ... + secrets: + awsKeyId: CHANGE-ME + awsSecretKey: CHANGE-ME +``` + +After making this change and applying it to gundeck (ensure gundeck pods have restarted to make use of the updated configuration - that should happen automatically), make sure to reset the push token on any mobile devices that you may have in use. + +Next, if you want, you can stop using the `fake-aws-sns` pods in case you ran them before: + +```yaml +# inside override values/fake-aws/values.yaml +fake-aws-sns: + enabled: false +``` + +## Controlling the speed of websocket draining during cannon pod replacement + +The 'cannon' component is responsible for persistent websocket connections. +Normally the default options would slowly and gracefully drain active websocket +connections over a maximum of `(amount of cannon replicas * 30 seconds)` during +the deployment of a new wire-server version. This will lead to a very brief +interruption for Wire clients when their client has to re-connect on the +websocket. + +You're not expected to need to change these settings. + +The following options are only relevant during the restart of cannon itself. +During a restart of nginz or ingress-controller, all websockets will get +severed. If this is to be avoided, see section {ref}`separate-websocket-traffic` + +`drainOpts`: Drain websockets in a controlled fashion when cannon receives a +SIGTERM or SIGINT (this happens when a pod is terminated e.g. during rollout +of a new version). Instead of waiting for connections to close on their own, +the websockets are now severed at a controlled pace. This allows for quicker +rollouts of new versions. + +There is no way to entirely disable this behaviour, two extreme examples below + +- the quickest way to kill cannon is to set `gracePeriodSeconds: 1` and + `minBatchSize: 100000` which would sever all connections immediately; but it's + not recommended as you could DDoS yourself by forcing all active clients to + reconnect at the same time. With this, cannon pod replacement takes only 1 + second per pod. +- the slowest way to roll out a new version of cannon without severing websocket + connections for a long time is to set `minBatchSize: 1`, + `millisecondsBetweenBatches: 86400000` and `gracePeriodSeconds: 86400` + which would lead to one single websocket connection being closed immediately, + and all others only after 1 day. With this, cannon pod replacement takes a + full day per pod. + +```yaml +# overrides for wire-server/values.yaml +cannon: + drainOpts: + # The following defaults drain a minimum of 400 connections/second + # for a total of 10000 over 25 seconds + # (if cannon holds more connections, draining will happen at a faster pace) + gracePeriodSeconds: 25 + millisecondsBetweenBatches: 50 + minBatchSize: 20 +``` + +## Control nginz upstreams (routes) into the Kubernetes cluster + +Open unterminated upstreams (routes) into the Kubernetes cluster are a potential +security issue. To prevent this, there are fine-grained settings in the nginz +configuration defining which upstreams should exist. + +### Default upstreams + +Upstreams for services that exist in (almost) every Wire installation are +enabled by default. These are: + +- `brig` +- `cannon` +- `cargohold` +- `galley` +- `gundeck` +- `spar` + +For special setups (as e.g. described in [separate-websocket-traffic]) the +upstreams of these services can be ignored (disabled) with the setting +`nginz.nginx_conf.ignored_upstreams`. + +The most common example is to disable the upstream of `cannon`: + +```yaml +nginz: + nginx_conf: + ignored_upstreams: ["cannon"] +``` + +### Optional upstreams + +There are some services that are usually not deployed on most Wire installations +or are specific to the Wire cloud: + +- `ibis` +- `galeb` +- `calling-test` +- `proxy` + +The upstreams for those are disabled by default and can be enabled by the +setting `nginz.nginx_conf.enabled_extra_upstreams`. + +The most common example is to enable the (extra) upstream of `proxy`: + +```yaml +nginz: + nginx_conf: + enabled_extra_upstreams: ["proxy"] +``` + +### Combining default and extra upstream configurations + +Default and extra upstream configurations are independent of each other. I.e. +`nginz.nginx_conf.ignored_upstreams` and +`nginz.nginx_conf.enabled_extra_upstreams` can be combined in the same +configuration: + +```yaml +nginz: + nginx_conf: + ignored_upstreams: ["cannon"] + enabled_extra_upstreams: ["proxy"] +``` + +(separate-websocket-traffic)= + +## Separate incoming websocket network traffic from the rest of the https traffic + +By default, incoming network traffic for websockets comes through these network +hops: + +Internet -> LoadBalancer -> kube-proxy -> nginx-ingress-controller -> nginz -> cannon + +In order to have graceful draining of websockets when something gets restarted, as it is not easily +possible to implement the graceful draining on nginx-ingress-controller or nginz by itself, there is +a configuration option to get the following network hops: + +Internet -> separate LoadBalancer for cannon only -> kube-proxy -> \[nginz->cannon (2 containers in the same pod)\] + +```yaml +# example on AWS when using cert-manager for TLS certificates and external-dns for DNS records +# (see wire-server/charts/cannon/values.yaml for more possible options) + +# in your wire-server/values.yaml overrides: +cannon: + service: + nginz: + enabled: true + hostname: "nginz-ssl.example.com" + externalDNS: + enabled: true + certManager: + enabled: true + annotations: + service.beta.kubernetes.io/aws-load-balancer-type: "nlb" + service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing" +nginz: + nginx_conf: + ignored_upstreams: ["cannon"] +``` + +```yaml +# in your wire-server/secrets.yaml overrides: +cannon: + secrets: + nginz: + zAuth: + publicKeys: ... # same values as in nginz.secrets.zAuth.publicKeys +``` + +```yaml +# in your nginx-ingress-services/values.yaml overrides: +websockets: + enabled: false +``` + +## Blocking creation of personal users, new teams + +### In Brig + +There are some unauthenticated end-points that allow arbitrary users on the open internet to do things like create a new team. This is desired in the cloud, but if you run an on-prem setup that is open to the world, you may want to block this. + +Brig has a server option for this: + +```yaml +optSettings: + setRestrictUserCreation: true +``` + +If `setRestrictUserCreation` is `true`, creating new personal users or new teams on your instance from outside your backend installation is impossible. (If you want to be more technical: requests to `/register` that create a new personal account or a new team are answered with `403 forbidden`.) + +On instances with restricted user creation, the site operator with access to the internal REST API can still circumvent the restriction: just log into a brig service pod via ssh and follow the steps in `hack/bin/create_test_team_admins.sh.` + +```{note} +Once the creation of new users and teams has been disabled, it will still be possible to use the [team creation process](https://support.wire.com/hc/en-us/articles/115003858905-Create-a-team) (enter the new team name, email, password, etc), but it will fail/refuse creation late in the creation process (after the «Create team» button is clicked). +``` + +### In the WebApp + +Another way of disabling user registration is by this webapp setting, in `values.yaml`, changing this value from `true` to `false`: + +```yaml +FEATURE_ENABLE_ACCOUNT_REGISTRATION: "false" +``` + +```{note} +If you only disable the creation of users in the webapp, but do not do so in Brig/the backend, a malicious user would be able to use the API to create users, so make sure to disable both. +``` + +## You may want + +- more server resources to ensure + [high-availability](#persistence-and-high-availability) +- an email/SMTP server to send out registration emails +- depending on your required functionality, you may or may not need an + [AWS account](https://aws.amazon.com/). See details about + limitations without an AWS account in the following sections. +- one or more people able to maintain the installation +- official support by Wire ([contact us](https://wire.com/pricing/)) + +```{warning} +As of 2020-08-10, the documentation sections below are partially out of date and need to be updated. +``` + +## Metrics/logging + +- {ref}`monitoring` +- {ref}`logging` + +## SMTP server + +**Assumptions**: none + +**Provides**: + +- full control over email sending + +**You need**: + +- SMTP credentials (to allow for email sending; prerequisite for + registering users and running the smoketest) + +**How to configure**: + +- *if using a gmail account, ensure to enable* ['less secure + apps'](https://support.google.com/accounts/answer/6010255?hl=en) +- Add user, SMTP server, connection type to `values/wire-server`'s + values file under `brig.config.smtp` +- Add password in `secrets/wire-server`'s secrets file under + `brig.secrets.smtpPassword` + +## Load balancer on bare metal servers + +**Assumptions**: + +- You installed kubernetes on bare metal servers or virtual machines + that can bind to a public IP address. +- **If you are using AWS or another cloud provider, see**[Creating a + cloudprovider-based load + balancer](#load-balancer-on-cloud-provider)**instead** + +**Provides**: + +- Allows using a provided Load balancer for incoming traffic +- SSL termination is done on the ingress controller +- You can access your wire-server backend with given DNS names, over + SSL and from anywhere in the internet + +**You need**: + +- A kubernetes node with a *public* IP address (or internal, if you do + not plan to expose the Wire backend over the Internet but we will + assume you are using a public IP address) + +- DNS records for the different exposed addresses (the ingress depends + on the usage of virtual hosts), namely: + + - `nginz-https.` + - `nginz-ssl.` + - `assets.` + - `webapp.` + - `account.` + - `teams.` + +- A wildcard certificate for the different hosts (`*.`) - we + assume you want to do SSL termination on the ingress controller + +**Caveats**: + +- Note that there can be only a *single* load balancer, otherwise your + cluster might become + [unstable](https://metallb.universe.tf/installation/) + +**How to configure**: + +``` +cp values/metallb/demo-values.example.yaml values/metallb/demo-values.yaml +cp values/nginx-ingress-services/demo-values.example.yaml values/nginx-ingress-services/demo-values.yaml +cp values/nginx-ingress-services/demo-secrets.example.yaml values/nginx-ingress-services/demo-secrets.yaml +``` + +- Adapt `values/metallb/demo-values.yaml` to provide a list of public + IP address CIDRs that your kubernetes nodes can bind to. +- Adapt `values/nginx-ingress-services/demo-values.yaml` with correct URLs +- Put your TLS cert and key into + `values/nginx-ingress-services/demo-secrets.yaml`. + +Install `metallb` (for more information see the +[docs](https://metallb.universe.tf)): + +```sh +helm upgrade --install --namespace metallb-system metallb wire/metallb \ + -f values/metallb/demo-values.yaml \ + --wait --timeout 1800 +``` + +Install `nginx-ingress-[controller,services]`: + +:: +: helm upgrade --install --namespace demo demo-nginx-ingress-controller wire/nginx-ingress-controller + + : --wait + + helm upgrade --install --namespace demo demo-nginx-ingress-services wire/nginx-ingress-services + + : -f values/nginx-ingress-services/demo-values.yaml -f values/nginx-ingress-services/demo-secrets.yaml --wait + +Now, create DNS records for the URLs configured above. + +## Load Balancer on cloud-provider + +### AWS + +[Upload the required +certificates](https://aws.amazon.com/premiumsupport/knowledge-center/import-ssl-certificate-to-iam/). +Create and configure `values/aws-ingress/demo-values.yaml` from the +examples. + +``` +helm upgrade --install --namespace demo demo-aws-ingress wire/aws-ingress \ + -f values/aws-ingress/demo-values.yaml \ + --wait +``` + +To give your load balancers public DNS names, create and edit +`values/external-dns/demo-values.yaml`, then run +[external-dns](https://github.com/helm/charts/tree/master/stable/external-dns): + +``` +helm repo update +helm upgrade --install --namespace demo demo-external-dns stable/external-dns \ + --version 1.7.3 \ + -f values/external-dns/demo-values.yaml \ + --wait +``` + +Things to note about external-dns: + +- There can only be a single external-dns chart installed (one per + kubernetes cluster, not one per namespace). So if you already have + one running for another namespace you probably don't need to do + anything. +- You have to add the appropriate IAM permissions to your cluster (see + the + [README](https://github.com/helm/charts/tree/master/stable/external-dns)). +- Alternatively, use the AWS route53 console. + +### Other cloud providers + +This information is not yet available. If you'd like to contribute by +adding this information for your cloud provider, feel free to read the +[contributing guidelines](https://github.com/wireapp/wire-server-deploy/blob/master/CONTRIBUTING.md) and open a PR. + +## Real AWS services + +**Assumptions**: + +- You installed kubernetes and wire-server on AWS + +**Provides**: + +- Better availability guarantees and possibly better functionality of + AWS services such as SQS and dynamoDB. +- You can use ELBs in front of nginz for higher availability. +- instead of using a smtp server and connect with SMTP, you may use + SES. See configuration of brig and the `useSES` toggle. + +**You need**: + +- An AWS account + +**How to configure**: + +- Instead of using fake-aws charts, you need to set up the respective + services in your account, create queues, tables etc. Have a look at + the fake-aws-\* charts; you'll need to replicate a similar setup. + + - Once real AWS resources are created, adapt the configuration in + the values and secrets files for wire-server to use real endpoints + and real AWS keys. Look for comments including + `if using real AWS`. + +- Creating AWS resources in a way that is easy to create and delete + could be done using either [terraform](https://www.terraform.io/) + or [pulumi](https://pulumi.io/). If you'd like to contribute by + creating such automation, feel free to read the [contributing + guidelines](https://github.com/wireapp/wire-server-deploy/blob/master/CONTRIBUTING.md) and open a PR. + +## Persistence and high-availability + +Currently, due to the way kubernetes and cassandra +[interact](https://github.com/kubernetes/kubernetes/issues/28969), +cassandra cannot reliably be installed on kubernetes. Some people have +tried, e.g. [this +project](https://github.com/instaclustr/cassandra-operator) though at +the time of writing (Nov 2018), this does not yet work as advertised. We +recommend therefore to install cassandra, (possibly also elasticsearch +and redis) separately, i.e. outside of kubernetes (using 3 nodes each). + +For further higher-availability: + +- scale your kubernetes cluster to have separate etcd and master nodes + (3 nodes each) +- use 3 instead of 1 replica of each wire-server chart + +## Security + +For a production deployment, you should, as a minimum: + +- Ensure traffic between kubernetes nodes, etcd and databases are + confined to a private network +- Ensure kubernetes API is unreachable from the public internet (e.g. + put behind VPN/bastion host or restrict IP range) to prevent + [kubernetes + vulnerabilities](https://www.cvedetails.com/vulnerability-list/vendor_id-15867/product_id-34016/Kubernetes-Kubernetes.html) + from affecting you +- Ensure your operating systems get security updates automatically +- Restrict ssh access / harden sshd configuration +- Ensure no other pods with public access than the main ingress are + deployed on your cluster, since, in the current setup, pods have + access to etcd values (and thus any secrets stored there, including + secrets from other pods) +- Ensure developers encrypt any secrets.yaml files + +Additionally, you may wish to build, sign, and host your own docker +images to have increased confidence in those images. We haved "signed +container images" on our roadmap. + +## Sign up with a phone number (Sending SMS) + +**Provides**: + +- Registering accounts with a phone number + +**You need**: + +- a [Nexmo](https://www.nexmo.com/) account +- a [Twilio](https://www.twilio.com/) account + +**How to configure**: + +See the `brig` chart for configuration. + +(rd-party-proxying)= + +## 3rd-party proxying + +You need Giphy/Google/Spotify/Soundcloud API keys (if you want to +support previews by proxying these services) + +See the `proxy` chart for configuration. + +## Routing traffic to other namespaces via nginz + +If you have some components running in namespaces different from nginz. For +instance, the billing service (`ibis`) could be deployed to a separate +namespace, say `integrations`. But it still needs to get traffic via +`nginz`. When this is needed, the helm config can be adjusted like this: + +```yaml +# in your wire-server/values.yaml overrides: +nginz: + nginx_conf: + upstream_namespace: + ibis: integrations +``` + +## Marking an installation as self-hosted + +In case your wire installation is self-hosted (on-premise, demo installs), it needs to be aware that it is through a configuration option. As of release chart 4.15.0, `"true"` is the default behavior, and nothing needs to be done. + +If that option is not set, team-settings will prompt users about "wire for free" and associated functions. + +With that option set, all payment related functionality is disabled. + +The option is `IS_SELF_HOSTED`, and you set it in your `values.yaml` file (originally a copy of `prod-values.example.yaml` found in `wire-server-deploy/values/wire-server/`). + +In case of a demo install, replace `prod` with `demo`. + +First set the option under the `team-settings` section, `envVars` sub-section: + +```yaml +# NOTE: Only relevant if you want team-settings +team-settings: + envVars: + IS_SELF_HOSTED: "true" +``` + +Second, also set the option under the `account-pages` section: + +```yaml +# NOTE: Only relevant if you want account-pages +account-pages: + envVars: + IS_SELF_HOSTED: "true" +``` + +(auth-cookie-config)= + +## Configuring authentication cookie throttling + +Authentication cookies and the related throttling mechanism is described in the *Client API documentation*: +{ref}`login-cookies` + +The maximum number of cookies per account and type is defined by the brig option +`setUserCookieLimit`. Its default is `32`. + +Throttling is configured by the brig option `setUserCookieThrottle`. It is an +object that contains two fields: + +`stdDev` + +: The minimal standard deviation of cookie creation timestamps in + Seconds. (Default: `3000`, + [Wikipedia: Standard deviation](https://en.wikipedia.org/wiki/Standard_deviation)) + +`retryAfter` + +: Wait time in Seconds when `stdDev` is violated. (Default: `86400`) + +The default values are fine for most use cases. (Generally, you don't have to +configure them for your installation.) + +Condensed example: + +```yaml +brig: + optSettings: + setUserCookieLimit: 32 + setUserCookieThrottle: + stdDev: 3000 + retryAfter: 86400 +``` + +## Configuring searchability + +You can configure how search is limited or not based on user membership in a given team. + +There are two types of searches based on the direction of search: + +- **Inbound** searches mean that somebody is searching for you. Configuring the inbound search visibility means that you (or some admin) can configure whether others can find you or not. +- **Outbound** searches mean that you are searching for somebody. Configuring the outbound search visibility means that some admin can configure whether you can find other users or not. + +There are different types of matches: + +- **Exact handle** search means that the user is found only if the search query is exactly the user handle (e.g. searching for `mc` will find `@mc` but not `@mccaine`). This search returns zero or one results. +- **Full text** search means that the user is found if the search query contains some subset of the user display name and handle. (e.g. the query `mar` will find `Marco C`, `Omar`, `@amaro`) + +### Searching users on the same backend + +Search visibility is controlled by three parameters on the backend: + +- A team outbound configuration flag, `TeamSearchVisibility` with possible values `SearchVisibilityStandard`, `SearchVisibilityNoNameOutsideTeam` + + - `SearchVisibilityStandard` means that the user can find other people outside of the team, if the searched-person inbound search allows it + - `SearchVisibilityNoNameOutsideTeam` means that the user can not find any user outside the team by full text search (but exact handle search still works) + +- A team inbound configuration flag, `SearchVisibilityInbound` with possible values `SearchableByOwnTeam`, `SearchableByAllTeams` + + - `SearchableByOwnTeam` means that the user can be found only by users in their own team. + - `SearchableByAllTeams` means that the user can be found by users in any/all teams. + +- A server configuration flag `searchSameTeamOnly` with possible values true, false. + + - `Note`: For the same backend, this affects inbound and outbound searches (simply because all teams will be subject to this behavior) + - Setting this to `true` means that the all teams on that backend can only find users that belong to their team + +These flag are set on the backend and the clients do not need to be aware of them. + +The flags will influence the behavior of the search API endpoint; clients will only need to parse the results, that are already filtered for them by the backend. + +#### Table of possible outcomes + +```{eval-rst} ++------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ +| Is search-er (`uA`) in team (tA)? | Is search-ed (`uB`) in a team? | Backend flag `searchSameTeamOnly` | Team `tA`'s flag `TeamSearchVisibility` | Team tB's flag `SearchVisibilityInbound` | Result of exact search for `uB` | Result of full-text search for `uB` | ++====================================+=================================+====================================+==========================================+===========================================+==================================+======================================+ +| **Search within the same team** | ++------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ +| Yes, `tA` | Yes, the same team `tA` | Irrelevant | Irrelevant | Irrelevant | Found | Found | ++------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ +| **Outbound search unrestricted** | ++------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ +| Yes, `tA` | Yes, another team tB | false | `SearchVisibilityStandard` | `SearchableByAllTeams` | Found | Found | ++------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ +| Yes, `tA` | Yes, another team tB | false | `SearchVisibilityStandard` | `SearchableByOwnTeam` | Found | Not found | ++------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ +| **Outbound search restricted** | ++------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ +| Yes, `tA` | Yes, another team tB | true | Irrelevant | Irrelevant | Not found | Not found | ++------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ +| Yes, `tA` | Yes, another team tB | false | `SearchVisibilityNoNameOutsideTeam` | Irrelevant | Found | Not found | ++------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ +| Yes, `tA` | No | false | `SearchVisibilityNoNameOutsideTeam` | There’s no team B | Found | Not found | ++------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ +``` + +#### Changing the configuration on the server + +To change the `searchSameTeamOnly` setting on the backend, edit the `values.yaml.gotmpl` file for the wire-server chart at this nested level of the configuration: + +```yaml +brig: + # ... + config: + # ... + optSettings: + # ... + setSearchSameTeamOnly: true +``` + +If `setSearchSameTeamOnly` is set to `true` then `TeamSearchVisibility` is forced be in the `SearchVisibilityNoNameOutsideTeam` setting for all teams. + +#### Changing the default configuration for all teams + +If `setSearchSameTeamOnly` is set to `false` (or missing from the configuration) then the default value `TeamSearchVisibility` can be configured at this level of the configuration of the `value.yaml.gotmpl` file of the wire-server chart: + +```yaml +galley: + #... + config: + #... + settings: + #... + featureFlags: + #... + teamSearchVisibility: enabled-by-default +``` + +This default value applies to all teams for which no explicit configuration of the `TeamSearchVisibility` has been set. + +### Searching users on another (federated) backend + +For federated search the table above does not apply, see following table. + +```{note} +Incoming federated searches (i.e. searches from one backend to another) are considered always as being performed from a team user, even if they are performed from a personal user. + +This is because the incoming search request does not carry the information whether the user performing the search was in a team or not. + +So we have to make one assumption, and we assume that they were in a team. +``` + +Allowing search is done at the backend configuration level by the sysadmin: + +- Outbound search restrictions (`searchSameTeamOnly`, `TeamSearchVisibility`) do not apply to federated searches + +- A configuration setting `FederatedUserSearchPolicy` per federating domain with these possible values: + + - `no_search` The federating backend is not allowed to search any users (either by exact handle or full-text). + - `exact_handle_search` The federating backend may only search by exact handle + - `full_search` The federating backend may search users by full text search on display name and handle. The search search results are additionally affected by `SearchVisibilityInbound` setting of each team on the backend. + +- The `SearchVisibilityInbound` setting applies. Since the default value for teams is `SearchableByOwnTeam` this means that for a team to be full-text searchable by users on a federating backend both + + - `FederatedUserSearchPolicy` needs to be set to to full_search for the federating backend + - Any team that wants to be full-text searchable needs to be set to `SearchableByAllTeams` + +The configuration value `FederatedUserSearchPolicy` is per federated domain, e.g. in the values of the wire-server chart: + +```yaml +brig: + config: + optSettings: + setFederationDomainConfigs: + - domain: a.example.com + search_policy: no_search + - domain: a.example.com + search_policy: full_search +``` + +#### Table of possible outcomes + +In the following table, user `uA` on backend A is searching for user `uB` on team `tB` on backend B. + +Any of the flags set for searching users on the same backend are ignored. + +It’s worth nothing that if two users are on two separate backend, they are also guaranteed to be on two separate teams, as teams can not spread across backends. + +| Who is searching | Backend B flag `FederatedUserSearchPolicy` | Team `tB`'s flag `SearchVisibilityInbound` | Result of exact search for `uB` | Result of full-text search for `uB` | +| ---------------------- | ------------------------------------------ | ------------------------------------------ | ------------------------------- | ----------------------------------- | +| user `uA` on backend A | `no_search` | Irrelevant | Not found | Not found | +| user `uA` on backend A | `exact_handle_search` | Irrelevant | Found | Not found | +| user `uA` on backend A | `full_search` | SearchableByOwnTeam | Found | Not found | +| user `uA` on backend A | `full_search` | SearchableByAllTeams | Found | Found | + +### Changing the settings for a given team + +If you need to change searchabilility for a specific team (rather than the entire backend, as above), you need to make specific calls to the API. + +#### Team searchVisibility + +The team flag `searchVisibility` affects the outbound search of user searches. + +If it is set to `no-name-outside-team` for a team then all users of that team will no longer be able to find users that are not part of their team when searching. + +This also includes finding other users by by providing their exact handle. By default it is set to `standard`, which doesn't put any additional restrictions to outbound searches. + +The setting can be changed via endpoint (for more details on how to make the API calls with `curl`, read further): + +``` +GET /teams/{tid}/search-visibility + -- Shows the current TeamSearchVisibility value for the given team + +PUT /teams/{tid}/search-visibility + -- Set specific search visibility for the team + +pull-down-menu "body": + "standard" + "no-name-outside-team" +``` + +The team feature flag `teamSearchVisibility` determines whether it is allowed to change the `searchVisibility` setting or not. + +The default is `disabled-by-default`. + +```{note} +Whenever this feature setting is disabled the `searchVisibility` will be reset to standard. +``` + +The default setting that applies to all teams on the instance can be defined at configuration + +```yaml +settings: + featureFlags: + teamSearchVisibility: disabled-by-default # or enabled-by-default +``` + +#### TeamFeature searchVisibilityInbound + +The team feature flag `searchVisibilityInbound` affects if the team's users are searchable by users from other teams. + +The default setting is `searchable-by-own-team` which hides users from search results by users from other teams. + +If it is set to `searchable-by-all-teams` then users of this team may be included in the results of search queries by other users. + +```{note} +The configuration of this flag does not affect search results when the search query matches the handle exactly. + +If the handle is provdided then any user on the instance can find users. +``` + +This team feature flag can only by toggled by site-administrators with direct access to the galley instance (for more details on how to make the API calls with `curl`, read further): + +``` +PUT /i/teams/{tid}/features/search-visibility-inbound +``` + +With JSON body: + +```json +{"status": "enabled"} +``` + +or + +```json +{"status": "disabled"} +``` + +Where `enabled` is equivalent to `searchable-by-all-teams` and `disabled` is equivalent to `searchable-by-own-team`. + +The default setting that applies to all teams on the instance can be defined at configuration. + +```yaml +searchVisibilityInbound: + defaults: + status: enabled # OR disabled +``` + +Individual teams can overwrite the default setting with API calls as per above. + +#### Making the API calls + +To make API calls to set an explicit configuration for\` TeamSearchVisibilityInbound\` per team, you first need to know the Team ID, which can be found in the team settings app. + +It is an `UUID` which has format like this `dcbedf9a-af2a-4f43-9fd5-525953a919e1`. + +In the following we will be using this Team ID as an example, please replace it with your own team id. + +Next find the name of a `galley` pod by looking at the output of running this command: + +```sh +kubectl -n wire get pods +``` + +The output will look something like this: + +``` +... +galley-5f4787fdc7-9l64n ... +galley-migrate-data-lzz5j ... +... +``` + +Select any of the galley pods, for example we will use `galley-5f4787fdc7-9l64n`. + +Next, set up a port-forwarding from your local machine's port `9000` to the galley's port `8080` by running: + +```sh +kubectl port-forward -n wire galley-5f4787fdc7-9l64n 9000:8080 +``` + +Keep this command running until the end of these instuctions. + +Please run the following commands in a seperate terminal while keeping the terminal which establishes the port-forwarding open. + +To see team's current setting run: + +```sh +curl -XGET http://localhost:9000/i/teams/dcbedf9a-af2a-4f43-9fd5-525953a919e1/features/searchVisibilityInbound + +# {"lockStatus":"unlocked","status":"disabled"} +``` + +Where `disabled` corresponds to `SearchableByOwnTeam` and enabled corresponds to `SearchableByAllTeams`. + +To change the `TeamSearchVisibilityInbound` to `SearchableByAllTeams` for the team run: + +```sh +curl -XPUT -H 'Content-Type: application/json' -d "{\"status\": \"enabled\"}" http://localhost:9000/i/teams/dcbedf9a-af2a-4f43-9fd5-525953a919e1/features/searchVisibilityInbound +``` + +To change the TeamSearchVisibilityInbound to SearchableByOwnTeam for the team run: + +```sh +curl -XPUT -H 'Content-Type: application/json' -d "{\"status\": \"disabled\"}" http://localhost:9000/i/teams/dcbedf9a-af2a-4f43-9fd5-525953a919e1/features/searchVisibilityInbound +``` + +## Configuring classified domains + +As a backend administrator, if you want to control which other backends (identified by their domain) are "classified", + +change the following `galley` configuration in the `value.yaml.gotmpl` file of the wire-server chart: + +```yaml +galley: + replicaCount: 1 + config: + ... + featureFlags: + ... + classifiedDomains: + status: enabled + config: + domains: ["domain-that-is-classified.link"] + ... +``` + +This is not only a `backend` configuration, but also a `team` configuration/feature. + +This means that different combinations of configurations will have different results. + +Here is a table to navigate the possible configurations: + +| Backend Config enabled/disabled | Backend Config Domains | Team Config enabled/disabled | Team Config Domains | User's view | +| ------------------------------- | ---------------------------------------------- | ---------------------------- | ----------------------- | -------------------------------- | +| Enabled | \[domain1.example.com\] | Not configured | Not configured | Enabled, \[domain1.example.com\] | +| Enabled | \[domain1.example.com\]\[domain1.example.com\] | Enabled | Not configured | Enabled, \[domain1.example.com\] | +| Enabled | \[domain1.example.com\] | Enabled | \[domain2.example.com\] | Enabled, Undefined | +| Enabled | \[domain1.example.com\] | Disabled | Anything | Undefined | +| Disabled | Anything | Not configured | Not configured | Disabled, no domains | +| Disabled | Anything | Enabled | \[domain2.example.com\] | Undefined | + +The table assumes the following: + +- When backend level config says that this feature is enabled, it is illegal to not specify domains at the backend level. +- When backend level config says that this feature is disabled, the list of domains is ignored. +- When team level feature is disabled, the accompanying domains are ignored. + +## S3 Addressing Style + +S3 can either by addressed in path style, i.e. +`https:////`, or vhost style, i.e. +`https://./`. AWS's S3 offering has deprecated +path style addressing for S3 and completely disabled it for buckets created +after 30 Sep 2020: + + +However other object storage providers (specially self-deployed ones like MinIO) +may not support vhost style addressing yet (or ever?). Users of such buckets +should configure this option to "path": + +```yaml +cargohold: + aws: + s3AddressingStyle: path +``` + +Installations using S3 service provided by AWS, should use "auto", this option +will ensure that vhost style is only used when it is possible to construct a +valid hostname from the bucket name and the bucket name doesn't contain a '.'. +Having a '.' in the bucket name causes TLS validation to fail, hence it is not +used by default: + +```yaml +cargohold: + aws: + s3AddressingStyle: auto +``` + +Using "virtual" as an option is only useful in situations where vhost style +addressing must be used even if it is not possible to construct a valid hostname +from the bucket name or the S3 service provider can ensure correct certificate +is issued for bucket which contain one or more '.'s in the name: + +```yaml +cargohold: + aws: + s3AddressingStyle: virtual +``` + +When this option is unspecified, wire-server defaults to path style addressing +to ensure smooth transition for older deployments. diff --git a/docs/src/how-to/install/configuration-options.rst b/docs/src/how-to/install/configuration-options.rst deleted file mode 100644 index 869dc6c603..0000000000 --- a/docs/src/how-to/install/configuration-options.rst +++ /dev/null @@ -1,1106 +0,0 @@ -.. _configuration_options: - -Part 3 - configuration options in a production setup -==================================================================== - -This contains instructions to configure specific aspects of your production setup depending on your needs. - -Depending on your use-case and requirements, you may need to -configure none, or only a subset of the following sections. - -Redirect some traffic through a http(s) proxy ---------------------------------------------- - -In case you wish to use http(s) proxies, you can add a configuration like this to the wire-server services in question: - -Assuming your proxy can be reached from within Kubernetes at ``http://proxy:8080``, add the following for each affected service (e.g. ``gundeck``) to your Helm overrides in ``values/wire-server/values.yaml`` : - -.. code:: yaml - - gundeck: - # ... - config: - # ... - proxy: - httpProxy: "http://proxy:8080" - httpsProxy: "http://proxy:8080" - noProxyList: - - "localhost" - - "127.0.0.1" - - "10.0.0.0/8" - - "elasticsearch-external" - - "cassandra-external" - - "redis-ephemeral" - - "fake-aws-sqs" - - "fake-aws-dynamodb" - - "fake-aws-sns" - - "brig" - - "cargohold" - - "galley" - - "gundeck" - - "proxy" - - "spar" - - "federator" - - "cannon" - - "cannon-0.cannon.default" - - "cannon-1.cannon.default" - - "cannon-2.cannon.default" - -Depending on your setup, you may need to repeat this for the other services like ``brig`` as well. - -.. _pushsns: - -Enable push notifications using the public appstore / playstore mobile Wire clients ------------------------------------------------------------------------------------ - -1. You need to get in touch with us. Please talk to sales or customer support - see https://wire.com -2. If a contract agreement has been reached, we can set up a separate AWS account for you containing the necessary AWS SQS/SNS setup to route push notifications through to the mobile apps. We will then forward some configuration / access credentials that looks like: - -.. code:: yaml - - gundeck: - config: - aws: - account: "" - arnEnv: "" - queueName: "-gundeck-events" - region: "" - snsEndpoint: "https://sns..amazonaws.com" - sqsEndpoint: "https://sqs..amazonaws.com" - secrets: - awsKeyId: "" - awsSecretKey: "" - -To make use of those, first test the credentials are correct, e.g. using the ``aws`` command-line tool (for more information on how to configure credentials, please refer to the `official docs `__): - -.. code:: - - AWS_REGION= - AWS_ACCESS_KEY_ID=<...> - AWS_SECRET_ACCESS_KEY=<...> - ENV= #e.g staging - - aws sqs get-queue-url --queue-name "$ENV-gundeck-events" - -You should get a result like this: - -.. code:: - - { - "QueueUrl": "https://.queue.amazonaws.com//-gundeck-events" - } - -Then add them to your gundeck configuration overrides. - -Keys below ``gundeck.config`` belong into ``values/wire-server/values.yaml``: - -.. code:: yaml - - gundeck: - # ... - config: - aws: - queueName: # e.g. staging-gundeck-events - account: # , e.g. 123456789 - region: # e.g. eu-central-1 - snsEndpoint: # e.g. https://sns.eu-central-1.amazonaws.com - sqsEndpoint: # e.g. https://sqs.eu-central-1.amazonaws.com - arnEnv: # e.g. staging - this must match the environment name (first part of queueName) - -Keys below ``gundeck.secrets`` belong into ``values/wire-server/secrets.yaml``: - -.. code:: yaml - - gundeck: - # ... - secrets: - awsKeyId: CHANGE-ME - awsSecretKey: CHANGE-ME - - -After making this change and applying it to gundeck (ensure gundeck pods have restarted to make use of the updated configuration - that should happen automatically), make sure to reset the push token on any mobile devices that you may have in use. - -Next, if you want, you can stop using the `fake-aws-sns` pods in case you ran them before: - -.. code:: yaml - - # inside override values/fake-aws/values.yaml - fake-aws-sns: - enabled: false - -Controlling the speed of websocket draining during cannon pod replacement -------------------------------------------------------------------------- - -The 'cannon' component is responsible for persistent websocket connections. -Normally the default options would slowly and gracefully drain active websocket -connections over a maximum of ``(amount of cannon replicas * 30 seconds)`` during -the deployment of a new wire-server version. This will lead to a very brief -interruption for Wire clients when their client has to re-connect on the -websocket. - -You're not expected to need to change these settings. - -The following options are only relevant during the restart of cannon itself. -During a restart of nginz or ingress-controller, all websockets will get -severed. If this is to be avoided, see section :ref:`separate-websocket-traffic` - -``drainOpts``: Drain websockets in a controlled fashion when cannon receives a -SIGTERM or SIGINT (this happens when a pod is terminated e.g. during rollout -of a new version). Instead of waiting for connections to close on their own, -the websockets are now severed at a controlled pace. This allows for quicker -rollouts of new versions. - -There is no way to entirely disable this behaviour, two extreme examples below - -* the quickest way to kill cannon is to set ``gracePeriodSeconds: 1`` and - ``minBatchSize: 100000`` which would sever all connections immediately; but it's - not recommended as you could DDoS yourself by forcing all active clients to - reconnect at the same time. With this, cannon pod replacement takes only 1 - second per pod. -* the slowest way to roll out a new version of cannon without severing websocket - connections for a long time is to set ``minBatchSize: 1``, - ``millisecondsBetweenBatches: 86400000`` and ``gracePeriodSeconds: 86400`` - which would lead to one single websocket connection being closed immediately, - and all others only after 1 day. With this, cannon pod replacement takes a - full day per pod. - -.. code:: yaml - - # overrides for wire-server/values.yaml - cannon: - drainOpts: - # The following defaults drain a minimum of 400 connections/second - # for a total of 10000 over 25 seconds - # (if cannon holds more connections, draining will happen at a faster pace) - gracePeriodSeconds: 25 - millisecondsBetweenBatches: 50 - minBatchSize: 20 - - -Control nginz upstreams (routes) into the Kubernetes cluster ------------------------------------------------------------- - -Open unterminated upstreams (routes) into the Kubernetes cluster are a potential -security issue. To prevent this, there are fine-grained settings in the nginz -configuration defining which upstreams should exist. - -Default upstreams -^^^^^^^^^^^^^^^^^ - -Upstreams for services that exist in (almost) every Wire installation are -enabled by default. These are: - -- ``brig`` -- ``cannon`` -- ``cargohold`` -- ``galley`` -- ``gundeck`` -- ``spar`` - -For special setups (as e.g. described in separate-websocket-traffic_) the -upstreams of these services can be ignored (disabled) with the setting -``nginz.nginx_conf.ignored_upstreams``. - -The most common example is to disable the upstream of ``cannon``: - -.. code:: yaml - - nginz: - nginx_conf: - ignored_upstreams: ["cannon"] - - -Optional upstreams -^^^^^^^^^^^^^^^^^^ - -There are some services that are usually not deployed on most Wire installations -or are specific to the Wire cloud: - -- ``ibis`` -- ``galeb`` -- ``calling-test`` -- ``proxy`` - -The upstreams for those are disabled by default and can be enabled by the -setting ``nginz.nginx_conf.enabled_extra_upstreams``. - -The most common example is to enable the (extra) upstream of ``proxy``: - -.. code:: yaml - - nginz: - nginx_conf: - enabled_extra_upstreams: ["proxy"] - - -Combining default and extra upstream configurations -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Default and extra upstream configurations are independent of each other. I.e. -``nginz.nginx_conf.ignored_upstreams`` and -``nginz.nginx_conf.enabled_extra_upstreams`` can be combined in the same -configuration: - -.. code:: yaml - - nginz: - nginx_conf: - ignored_upstreams: ["cannon"] - enabled_extra_upstreams: ["proxy"] - - -.. _separate-websocket-traffic: - -Separate incoming websocket network traffic from the rest of the https traffic -------------------------------------------------------------------------------- - -By default, incoming network traffic for websockets comes through these network -hops: - -Internet -> LoadBalancer -> kube-proxy -> nginx-ingress-controller -> nginz -> cannon - -In order to have graceful draining of websockets when something gets restarted, as it is not easily -possible to implement the graceful draining on nginx-ingress-controller or nginz by itself, there is -a configuration option to get the following network hops: - -Internet -> separate LoadBalancer for cannon only -> kube-proxy -> [nginz->cannon (2 containers in the same pod)] - -.. code:: yaml - - # example on AWS when using cert-manager for TLS certificates and external-dns for DNS records - # (see wire-server/charts/cannon/values.yaml for more possible options) - - # in your wire-server/values.yaml overrides: - cannon: - service: - nginz: - enabled: true - hostname: "nginz-ssl.example.com" - externalDNS: - enabled: true - certManager: - enabled: true - annotations: - service.beta.kubernetes.io/aws-load-balancer-type: "nlb" - service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing" - nginz: - nginx_conf: - ignored_upstreams: ["cannon"] - -.. code:: yaml - - # in your wire-server/secrets.yaml overrides: - cannon: - secrets: - nginz: - zAuth: - publicKeys: ... # same values as in nginz.secrets.zAuth.publicKeys - -.. code:: yaml - - # in your nginx-ingress-services/values.yaml overrides: - websockets: - enabled: false - - -Blocking creation of personal users, new teams ----------------------------------------------- - -In Brig -~~~~~~~ - -There are some unauthenticated end-points that allow arbitrary users on the open internet to do things like create a new team. This is desired in the cloud, but if you run an on-prem setup that is open to the world, you may want to block this. - -Brig has a server option for this: - -.. code:: yaml - - optSettings: - setRestrictUserCreation: true - -If `setRestrictUserCreation` is `true`, creating new personal users or new teams on your instance from outside your backend installation is impossible. (If you want to be more technical: requests to `/register` that create a new personal account or a new team are answered with `403 forbidden`.) - -On instances with restricted user creation, the site operator with access to the internal REST API can still circumvent the restriction: just log into a brig service pod via ssh and follow the steps in `hack/bin/create_test_team_admins.sh.` - -.. note:: - Once the creation of new users and teams has been disabled, it will still be possible to use the `team creation process `__ (enter the new team name, email, password, etc), but it will fail/refuse creation late in the creation process (after the «Create team» button is clicked). - -In the WebApp -~~~~~~~~~~~~~ - -Another way of disabling user registration is by this webapp setting, in `values.yaml`, changing this value from `true` to `false`: - -.. code:: yaml - - FEATURE_ENABLE_ACCOUNT_REGISTRATION: "false" - -.. note:: - If you only disable the creation of users in the webapp, but do not do so in Brig/the backend, a malicious user would be able to use the API to create users, so make sure to disable both. - -You may want ------------- - -- more server resources to ensure - `high-availability <#persistence-and-high-availability>`__ -- an email/SMTP server to send out registration emails -- depending on your required functionality, you may or may not need an - `AWS account `__. See details about - limitations without an AWS account in the following sections. -- one or more people able to maintain the installation -- official support by Wire (`contact us `__) - -.. warning:: - - As of 2020-08-10, the documentation sections below are partially out of date and need to be updated. - -Metrics/logging ---------------- - -* :ref:`monitoring` -* :ref:`logging` - -SMTP server ------------ - -**Assumptions**: none - -**Provides**: - -- full control over email sending - -**You need**: - -- SMTP credentials (to allow for email sending; prerequisite for - registering users and running the smoketest) - -**How to configure**: - -- *if using a gmail account, ensure to enable* `'less secure - apps' `__ -- Add user, SMTP server, connection type to ``values/wire-server``'s - values file under ``brig.config.smtp`` -- Add password in ``secrets/wire-server``'s secrets file under - ``brig.secrets.smtpPassword`` - -Load balancer on bare metal servers ------------------------------------ - -**Assumptions**: - -- You installed kubernetes on bare metal servers or virtual machines - that can bind to a public IP address. -- **If you are using AWS or another cloud provider, see**\ `Creating a - cloudprovider-based load - balancer <#load-balancer-on-cloud-provider>`__\ **instead** - -**Provides**: - -- Allows using a provided Load balancer for incoming traffic -- SSL termination is done on the ingress controller -- You can access your wire-server backend with given DNS names, over - SSL and from anywhere in the internet - -**You need**: - -- A kubernetes node with a *public* IP address (or internal, if you do - not plan to expose the Wire backend over the Internet but we will - assume you are using a public IP address) -- DNS records for the different exposed addresses (the ingress depends - on the usage of virtual hosts), namely: - - - ``nginz-https.`` - - ``nginz-ssl.`` - - ``assets.`` - - ``webapp.`` - - ``account.`` - - ``teams.`` - -- A wildcard certificate for the different hosts (``*.``) - we - assume you want to do SSL termination on the ingress controller - -**Caveats**: - -- Note that there can be only a *single* load balancer, otherwise your - cluster might become - `unstable `__ - -**How to configure**: - -:: - - cp values/metallb/demo-values.example.yaml values/metallb/demo-values.yaml - cp values/nginx-ingress-services/demo-values.example.yaml values/nginx-ingress-services/demo-values.yaml - cp values/nginx-ingress-services/demo-secrets.example.yaml values/nginx-ingress-services/demo-secrets.yaml - -- Adapt ``values/metallb/demo-values.yaml`` to provide a list of public - IP address CIDRs that your kubernetes nodes can bind to. -- Adapt ``values/nginx-ingress-services/demo-values.yaml`` with correct URLs -- Put your TLS cert and key into - ``values/nginx-ingress-services/demo-secrets.yaml``. - -Install ``metallb`` (for more information see the -`docs `__): - -.. code:: sh - - helm upgrade --install --namespace metallb-system metallb wire/metallb \ - -f values/metallb/demo-values.yaml \ - --wait --timeout 1800 - -Install ``nginx-ingress-[controller,services]``: - -:: - helm upgrade --install --namespace demo demo-nginx-ingress-controller wire/nginx-ingress-controller \ - --wait - - helm upgrade --install --namespace demo demo-nginx-ingress-services wire/nginx-ingress-services \ - -f values/nginx-ingress-services/demo-values.yaml \ - -f values/nginx-ingress-services/demo-secrets.yaml \ - --wait - -Now, create DNS records for the URLs configured above. - - -Load Balancer on cloud-provider -------------------------------- - -AWS -~~~ - -`Upload the required -certificates `__. -Create and configure ``values/aws-ingress/demo-values.yaml`` from the -examples. - -:: - - helm upgrade --install --namespace demo demo-aws-ingress wire/aws-ingress \ - -f values/aws-ingress/demo-values.yaml \ - --wait - -To give your load balancers public DNS names, create and edit -``values/external-dns/demo-values.yaml``, then run -`external-dns `__: - -:: - - helm repo update - helm upgrade --install --namespace demo demo-external-dns stable/external-dns \ - --version 1.7.3 \ - -f values/external-dns/demo-values.yaml \ - --wait - -Things to note about external-dns: - -- There can only be a single external-dns chart installed (one per - kubernetes cluster, not one per namespace). So if you already have - one running for another namespace you probably don't need to do - anything. -- You have to add the appropriate IAM permissions to your cluster (see - the - `README `__). -- Alternatively, use the AWS route53 console. - -Other cloud providers -~~~~~~~~~~~~~~~~~~~~~ - -This information is not yet available. If you'd like to contribute by -adding this information for your cloud provider, feel free to read the -`contributing guidelines `__ and open a PR. - -Real AWS services ------------------ - -**Assumptions**: - -- You installed kubernetes and wire-server on AWS - -**Provides**: - -- Better availability guarantees and possibly better functionality of - AWS services such as SQS and dynamoDB. -- You can use ELBs in front of nginz for higher availability. -- instead of using a smtp server and connect with SMTP, you may use - SES. See configuration of brig and the ``useSES`` toggle. - -**You need**: - -- An AWS account - -**How to configure**: - -- Instead of using fake-aws charts, you need to set up the respective - services in your account, create queues, tables etc. Have a look at - the fake-aws-\* charts; you'll need to replicate a similar setup. - - - Once real AWS resources are created, adapt the configuration in - the values and secrets files for wire-server to use real endpoints - and real AWS keys. Look for comments including - ``if using real AWS``. - -- Creating AWS resources in a way that is easy to create and delete - could be done using either `terraform `__ - or `pulumi `__. If you'd like to contribute by - creating such automation, feel free to read the `contributing - guidelines `__ and open a PR. - -Persistence and high-availability ---------------------------------- - -Currently, due to the way kubernetes and cassandra -`interact `__, -cassandra cannot reliably be installed on kubernetes. Some people have -tried, e.g. `this -project `__ though at -the time of writing (Nov 2018), this does not yet work as advertised. We -recommend therefore to install cassandra, (possibly also elasticsearch -and redis) separately, i.e. outside of kubernetes (using 3 nodes each). - -For further higher-availability: - -- scale your kubernetes cluster to have separate etcd and master nodes - (3 nodes each) -- use 3 instead of 1 replica of each wire-server chart - -Security --------- - -For a production deployment, you should, as a minimum: - -- Ensure traffic between kubernetes nodes, etcd and databases are - confined to a private network -- Ensure kubernetes API is unreachable from the public internet (e.g. - put behind VPN/bastion host or restrict IP range) to prevent - `kubernetes - vulnerabilities `__ - from affecting you -- Ensure your operating systems get security updates automatically -- Restrict ssh access / harden sshd configuration -- Ensure no other pods with public access than the main ingress are - deployed on your cluster, since, in the current setup, pods have - access to etcd values (and thus any secrets stored there, including - secrets from other pods) -- Ensure developers encrypt any secrets.yaml files - -Additionally, you may wish to build, sign, and host your own docker -images to have increased confidence in those images. We haved "signed -container images" on our roadmap. - -Sign up with a phone number (Sending SMS) ------------------------------------------ - -**Provides**: - -- Registering accounts with a phone number - -**You need**: - -- a `Nexmo `__ account -- a `Twilio `__ account - -**How to configure**: - -See the ``brig`` chart for configuration. - -.. _3rd-party-proxying: - -3rd-party proxying ------------------- - -You need Giphy/Google/Spotify/Soundcloud API keys (if you want to -support previews by proxying these services) - -See the ``proxy`` chart for configuration. - -Routing traffic to other namespaces via nginz ---------------------------------------------- - -If you have some components running in namespaces different from nginz. For -instance, the billing service (``ibis``) could be deployed to a separate -namespace, say ``integrations``. But it still needs to get traffic via -``nginz``. When this is needed, the helm config can be adjusted like this: - -.. code:: yaml - - # in your wire-server/values.yaml overrides: - nginz: - nginx_conf: - upstream_namespace: - ibis: integrations - -Marking an installation as self-hosted --------------------------------------- - -In case your wire installation is self-hosted (on-premise, demo installs), it needs to be aware that it is through a configuration option. As of release chart 4.15.0, `"true"` is the default behavior, and nothing needs to be done. - -If that option is not set, team-settings will prompt users about "wire for free" and associated functions. - -With that option set, all payment related functionality is disabled. - -The option is `IS_SELF_HOSTED`, and you set it in your `values.yaml` file (originally a copy of `prod-values.example.yaml` found in `wire-server-deploy/values/wire-server/`). - -In case of a demo install, replace `prod` with `demo`. - -First set the option under the `team-settings` section, `envVars` sub-section: - -.. code:: yaml - - # NOTE: Only relevant if you want team-settings - team-settings: - envVars: - IS_SELF_HOSTED: "true" - -Second, also set the option under the `account-pages` section: - -.. code:: yaml - - # NOTE: Only relevant if you want account-pages - account-pages: - envVars: - IS_SELF_HOSTED: "true" - -.. _auth-cookie-config: - -Configuring authentication cookie throttling --------------------------------------------- - -Authentication cookies and the related throttling mechanism is described in the *Client API documentation*: -:ref:`login-cookies` - -The maximum number of cookies per account and type is defined by the brig option -``setUserCookieLimit``. Its default is ``32``. - -Throttling is configured by the brig option ``setUserCookieThrottle``. It is an -object that contains two fields: - -``stdDev`` - The minimal standard deviation of cookie creation timestamps in - Seconds. (Default: ``3000``, - `Wikipedia: Standard deviation `_) - -``retryAfter`` - Wait time in Seconds when ``stdDev`` is violated. (Default: ``86400``) - -The default values are fine for most use cases. (Generally, you don't have to -configure them for your installation.) - -Condensed example: - - -.. code:: yaml - - brig: - optSettings: - setUserCookieLimit: 32 - setUserCookieThrottle: - stdDev: 3000 - retryAfter: 86400 - - -Configuring searchability -------------------------- - -You can configure how search is limited or not based on user membership in a given team. - -There are two types of searches based on the direction of search: - -* **Inbound** searches mean that somebody is searching for you. Configuring the inbound search visibility means that you (or some admin) can configure whether others can find you or not. -* **Outbound** searches mean that you are searching for somebody. Configuring the outbound search visibility means that some admin can configure whether you can find other users or not. - -There are different types of matches: - -* **Exact handle** search means that the user is found only if the search query is exactly the user handle (e.g. searching for `mc` will find `@mc` but not `@mccaine`). This search returns zero or one results. -* **Full text** search means that the user is found if the search query contains some subset of the user display name and handle. (e.g. the query `mar` will find `Marco C`, `Omar`, `@amaro`) - -Searching users on the same backend -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Search visibility is controlled by three parameters on the backend: - -* A team outbound configuration flag, `TeamSearchVisibility` with possible values `SearchVisibilityStandard`, `SearchVisibilityNoNameOutsideTeam` - - * `SearchVisibilityStandard` means that the user can find other people outside of the team, if the searched-person inbound search allows it - * `SearchVisibilityNoNameOutsideTeam` means that the user can not find any user outside the team by full text search (but exact handle search still works) - -* A team inbound configuration flag, `SearchVisibilityInbound` with possible values `SearchableByOwnTeam`, `SearchableByAllTeams` - - * `SearchableByOwnTeam` means that the user can be found only by users in their own team. - * `SearchableByAllTeams` means that the user can be found by users in any/all teams. - -* A server configuration flag `searchSameTeamOnly` with possible values true, false. - - * ``Note``: For the same backend, this affects inbound and outbound searches (simply because all teams will be subject to this behavior) - * Setting this to `true` means that the all teams on that backend can only find users that belong to their team - -These flag are set on the backend and the clients do not need to be aware of them. - -The flags will influence the behavior of the search API endpoint; clients will only need to parse the results, that are already filtered for them by the backend. - -Table of possible outcomes -.......................... - -+------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -| Is search-er (`uA`) in team (tA)? | Is search-ed (`uB`) in a team? | Backend flag `searchSameTeamOnly` | Team `tA`'s flag `TeamSearchVisibility` | Team tB's flag `SearchVisibilityInbound` | Result of exact search for `uB` | Result of full-text search for `uB` | -+====================================+=================================+====================================+==========================================+===========================================+==================================+======================================+ -| **Search within the same team** | -+------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -| Yes, `tA` | Yes, the same team `tA` | Irrelevant | Irrelevant | Irrelevant | Found | Found | -+------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -| **Outbound search unrestricted** | -+------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -| Yes, `tA` | Yes, another team tB | false | `SearchVisibilityStandard` | `SearchableByAllTeams` | Found | Found | -+------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -| Yes, `tA` | Yes, another team tB | false | `SearchVisibilityStandard` | `SearchableByOwnTeam` | Found | Not found | -+------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -| **Outbound search restricted** | -+------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -| Yes, `tA` | Yes, another team tB | true | Irrelevant | Irrelevant | Not found | Not found | -+------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -| Yes, `tA` | Yes, another team tB | false | `SearchVisibilityNoNameOutsideTeam` | Irrelevant | Found | Not found | -+------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -| Yes, `tA` | No | false | `SearchVisibilityNoNameOutsideTeam` | There’s no team B | Found | Not found | -+------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ - - -Changing the configuration on the server -........................................ - -To change the `searchSameTeamOnly` setting on the backend, edit the `values.yaml.gotmpl` file for the wire-server chart at this nested level of the configuration: - -.. code:: yaml - - brig: - # ... - config: - # ... - optSettings: - # ... - setSearchSameTeamOnly: true - -If `setSearchSameTeamOnly` is set to `true` then `TeamSearchVisibility` is forced be in the `SearchVisibilityNoNameOutsideTeam` setting for all teams. - -Changing the default configuration for all teams -................................................ - -If `setSearchSameTeamOnly` is set to `false` (or missing from the configuration) then the default value `TeamSearchVisibility` can be configured at this level of the configuration of the `value.yaml.gotmpl` file of the wire-server chart: - - -.. code:: yaml - - galley: - #... - config: - #... - settings: - #... - featureFlags: - #... - teamSearchVisibility: enabled-by-default - -This default value applies to all teams for which no explicit configuration of the `TeamSearchVisibility` has been set. - - -Searching users on another (federated) backend -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -For federated search the table above does not apply, see following table. - -.. note:: - - Incoming federated searches (i.e. searches from one backend to another) are considered always as being performed from a team user, even if they are performed from a personal user. - - This is because the incoming search request does not carry the information whether the user performing the search was in a team or not. - - So we have to make one assumption, and we assume that they were in a team. - -Allowing search is done at the backend configuration level by the sysadmin: - -* Outbound search restrictions (`searchSameTeamOnly`, `TeamSearchVisibility`) do not apply to federated searches -* A configuration setting `FederatedUserSearchPolicy` per federating domain with these possible values: - - * `no_search` The federating backend is not allowed to search any users (either by exact handle or full-text). - * `exact_handle_search` The federating backend may only search by exact handle - * `full_search` The federating backend may search users by full text search on display name and handle. The search search results are additionally affected by `SearchVisibilityInbound` setting of each team on the backend. -* The `SearchVisibilityInbound` setting applies. Since the default value for teams is `SearchableByOwnTeam` this means that for a team to be full-text searchable by users on a federating backend both - - * `FederatedUserSearchPolicy` needs to be set to to full_search for the federating backend - * Any team that wants to be full-text searchable needs to be set to `SearchableByAllTeams` - -The configuration value `FederatedUserSearchPolicy` is per federated domain, e.g. in the values of the wire-server chart: - -.. code:: yaml - - brig: - config: - optSettings: - setFederationDomainConfigs: - - domain: a.example.com - search_policy: no_search - - domain: a.example.com - search_policy: full_search - -Table of possible outcomes -.......................... - -In the following table, user `uA` on backend A is searching for user `uB` on team `tB` on backend B. - -Any of the flags set for searching users on the same backend are ignored. - -It’s worth nothing that if two users are on two separate backend, they are also guaranteed to be on two separate teams, as teams can not spread across backends. - -+-------------------------+---------------------------------------------+---------------------------------------------+----------------------------------+--------------------------------------+ -| Who is searching | Backend B flag `FederatedUserSearchPolicy` | Team `tB`'s flag `SearchVisibilityInbound` | Result of exact search for `uB` | Result of full-text search for `uB` | -+=========================+=============================================+=============================================+==================================+======================================+ -| user `uA` on backend A | `no_search` | Irrelevant | Not found | Not found | -+-------------------------+---------------------------------------------+---------------------------------------------+----------------------------------+--------------------------------------+ -| user `uA` on backend A | `exact_handle_search` | Irrelevant | Found | Not found | -+-------------------------+---------------------------------------------+---------------------------------------------+----------------------------------+--------------------------------------+ -| user `uA` on backend A | `full_search` | SearchableByOwnTeam | Found | Not found | -+-------------------------+---------------------------------------------+---------------------------------------------+----------------------------------+--------------------------------------+ -| user `uA` on backend A | `full_search` | SearchableByAllTeams | Found | Found | -+-------------------------+---------------------------------------------+---------------------------------------------+----------------------------------+--------------------------------------+ - -Changing the settings for a given team -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you need to change searchabilility for a specific team (rather than the entire backend, as above), you need to make specific calls to the API. - -Team searchVisibility -..................... - -The team flag `searchVisibility` affects the outbound search of user searches. - -If it is set to `no-name-outside-team` for a team then all users of that team will no longer be able to find users that are not part of their team when searching. - -This also includes finding other users by by providing their exact handle. By default it is set to `standard`, which doesn't put any additional restrictions to outbound searches. - -The setting can be changed via endpoint (for more details on how to make the API calls with `curl`, read further): - -.. code:: - - GET /teams/{tid}/search-visibility - -- Shows the current TeamSearchVisibility value for the given team - - PUT /teams/{tid}/search-visibility - -- Set specific search visibility for the team - - pull-down-menu "body": - "standard" - "no-name-outside-team" - -The team feature flag `teamSearchVisibility` determines whether it is allowed to change the `searchVisibility` setting or not. - -The default is `disabled-by-default`. - -.. note:: - - Whenever this feature setting is disabled the `searchVisibility` will be reset to standard. - -The default setting that applies to all teams on the instance can be defined at configuration - -.. code:: yaml - - settings: - featureFlags: - teamSearchVisibility: disabled-by-default # or enabled-by-default - -TeamFeature searchVisibilityInbound -................................... - -The team feature flag `searchVisibilityInbound` affects if the team's users are searchable by users from other teams. - -The default setting is `searchable-by-own-team` which hides users from search results by users from other teams. - -If it is set to `searchable-by-all-teams` then users of this team may be included in the results of search queries by other users. - -.. note:: - - The configuration of this flag does not affect search results when the search query matches the handle exactly. - - If the handle is provdided then any user on the instance can find users. - -This team feature flag can only by toggled by site-administrators with direct access to the galley instance (for more details on how to make the API calls with `curl`, read further): - -.. code:: - - PUT /i/teams/{tid}/features/search-visibility-inbound - -With JSON body: - -.. code:: json - - {"status": "enabled"} - -or - -.. code:: json - - {"status": "disabled"} - -Where `enabled` is equivalent to `searchable-by-all-teams` and `disabled` is equivalent to `searchable-by-own-team`. - -The default setting that applies to all teams on the instance can be defined at configuration. - -.. code:: yaml - - searchVisibilityInbound: - defaults: - status: enabled # OR disabled - -Individual teams can overwrite the default setting with API calls as per above. - -Making the API calls -.................... - -To make API calls to set an explicit configuration for` TeamSearchVisibilityInbound` per team, you first need to know the Team ID, which can be found in the team settings app. - -It is an `UUID` which has format like this `dcbedf9a-af2a-4f43-9fd5-525953a919e1`. - -In the following we will be using this Team ID as an example, please replace it with your own team id. - -Next find the name of a `galley` pod by looking at the output of running this command: - -.. code:: sh - - kubectl -n wire get pods - -The output will look something like this: - -.. code:: - - ... - galley-5f4787fdc7-9l64n ... - galley-migrate-data-lzz5j ... - ... - -Select any of the galley pods, for example we will use `galley-5f4787fdc7-9l64n`. - -Next, set up a port-forwarding from your local machine's port `9000` to the galley's port `8080` by running: - -.. code:: sh - - kubectl port-forward -n wire galley-5f4787fdc7-9l64n 9000:8080 - -Keep this command running until the end of these instuctions. - -Please run the following commands in a seperate terminal while keeping the terminal which establishes the port-forwarding open. - -To see team's current setting run: - -.. code:: sh - - curl -XGET http://localhost:9000/i/teams/dcbedf9a-af2a-4f43-9fd5-525953a919e1/features/searchVisibilityInbound - - # {"lockStatus":"unlocked","status":"disabled"} - -Where `disabled` corresponds to `SearchableByOwnTeam` and enabled corresponds to `SearchableByAllTeams`. - -To change the `TeamSearchVisibilityInbound` to `SearchableByAllTeams` for the team run: - -.. code:: sh - - curl -XPUT -H 'Content-Type: application/json' -d "{\"status\": \"enabled\"}" http://localhost:9000/i/teams/dcbedf9a-af2a-4f43-9fd5-525953a919e1/features/searchVisibilityInbound - -To change the TeamSearchVisibilityInbound to SearchableByOwnTeam for the team run: - -.. code:: sh - - curl -XPUT -H 'Content-Type: application/json' -d "{\"status\": \"disabled\"}" http://localhost:9000/i/teams/dcbedf9a-af2a-4f43-9fd5-525953a919e1/features/searchVisibilityInbound - - - -Configuring classified domains ------------------------------- - -As a backend administrator, if you want to control which other backends (identified by their domain) are "classified", - -change the following `galley` configuration in the `value.yaml.gotmpl` file of the wire-server chart: - -.. code:: yaml - - galley: - replicaCount: 1 - config: - ... - featureFlags: - ... - classifiedDomains: - status: enabled - config: - domains: ["domain-that-is-classified.link"] - ... - -This is not only a `backend` configuration, but also a `team` configuration/feature. - -This means that different combinations of configurations will have different results. - -Here is a table to navigate the possible configurations: - -+----------------------------------+---------------------------------------------+-------------------------------+------------------------+---------------------------------+ -| Backend Config enabled/disabled | Backend Config Domains | Team Config enabled/disabled | Team Config Domains | User's view | -+==================================+=============================================+===============================+========================+=================================+ -| Enabled | [domain1.example.com] | Not configured | Not configured | Enabled, [domain1.example.com] | -+----------------------------------+---------------------------------------------+-------------------------------+------------------------+---------------------------------+ -| Enabled | [domain1.example.com][domain1.example.com] | Enabled | Not configured | Enabled, [domain1.example.com] | -+----------------------------------+---------------------------------------------+-------------------------------+------------------------+---------------------------------+ -| Enabled | [domain1.example.com] | Enabled | [domain2.example.com] | Enabled, Undefined | -+----------------------------------+---------------------------------------------+-------------------------------+------------------------+---------------------------------+ -| Enabled | [domain1.example.com] | Disabled | Anything | Undefined | -+----------------------------------+---------------------------------------------+-------------------------------+------------------------+---------------------------------+ -| Disabled | Anything | Not configured | Not configured | Disabled, no domains | -+----------------------------------+---------------------------------------------+-------------------------------+------------------------+---------------------------------+ -| Disabled | Anything | Enabled | [domain2.example.com] | Undefined | -+----------------------------------+---------------------------------------------+-------------------------------+------------------------+---------------------------------+ - -The table assumes the following: - -* When backend level config says that this feature is enabled, it is illegal to not specify domains at the backend level. -* When backend level config says that this feature is disabled, the list of domains is ignored. -* When team level feature is disabled, the accompanying domains are ignored. - -S3 Addressing Style -------------------- - -S3 can either by addressed in path style, i.e. -`https:////`, or vhost style, i.e. -`https://./`. AWS's S3 offering has deprecated -path style addressing for S3 and completely disabled it for buckets created -after 30 Sep 2020: -https://aws.amazon.com/blogs/aws/amazon-s3-path-deprecation-plan-the-rest-of-the-story/ - -However other object storage providers (specially self-deployed ones like MinIO) -may not support vhost style addressing yet (or ever?). Users of such buckets -should configure this option to "path": - -.. code:: yaml - - cargohold: - aws: - s3AddressingStyle: path - -Installations using S3 service provided by AWS, should use "auto", this option -will ensure that vhost style is only used when it is possible to construct a -valid hostname from the bucket name and the bucket name doesn't contain a '.'. -Having a '.' in the bucket name causes TLS validation to fail, hence it is not -used by default: - -.. code:: yaml - - cargohold: - aws: - s3AddressingStyle: auto - - -Using "virtual" as an option is only useful in situations where vhost style -addressing must be used even if it is not possible to construct a valid hostname -from the bucket name or the S3 service provider can ensure correct certificate -is issued for bucket which contain one or more '.'s in the name: - -.. code:: yaml - - cargohold: - aws: - s3AddressingStyle: virtual - -When this option is unspecified, wire-server defaults to path style addressing -to ensure smooth transition for older deployments. diff --git a/docs/src/how-to/install/dependencies.md b/docs/src/how-to/install/dependencies.md new file mode 100644 index 0000000000..43ad7f7d90 --- /dev/null +++ b/docs/src/how-to/install/dependencies.md @@ -0,0 +1,69 @@ +(dependencies)= + +# Dependencies on operator's machine + +In order to operate a wire-server installation, you'll need a bunch of software +like Ansible, `kubectl` and Helm. + +Together with a matching checkout of the wire-server-deploy repository, +containing the Ansible Roles and Playbooks, you should be good to go. + +Checkout the repository, including its submodules: + +``` +git clone --branch master https://github.com/wireapp/wire-server-deploy.git +cd wire-server-deploy +git submodule update --init --recursive +``` + +We provide a container containing all needed tools for setting up and +interacting with a wire-server cluster. + +Ensure you have Docker >= 20.10.14 installed, as the glibc version used is +incompatible with older container runtimes. + +Your Distro might ship an older version, so best see [how to install docker](https://docker.com). + +To bring the tools in scope, we run the container, and mount the local `wire-server-deploy` +checkout into it. + +Replace the container image tag with the commit id your `wire-server-deploy` +checkout is pointing to. + +``` +WSD_COMMIT_ID=cdc1c84c1a10a4f5f1b77b51ee5655d0da7f9518 # set me +WSD_CONTAINER=quay.io/wire/wire-server-deploy:$WSD_COMMIT_ID + +sudo docker run -it --network=host \ + -v ${SSH_AUTH_SOCK:-nonexistent}:/ssh-agent \ + -v $HOME/.ssh:/root/.ssh \ + -v $PWD:/wire-server-deploy \ + -e SSH_AUTH_SOCK=/ssh-agent \ + $WSD_CONTAINER bash + +# Inside the container +bash-4.4# ansible --version +ansible 2.9.12 +``` + +Once you're in there, you can move on to {ref}`installing kubernetes `. + +## (Alternative) Installing dependencies using Direnv and Nix + +```{warning} +This is an alternative approach to the above "wrapping container" one, which you should only use if you can't get above setup to work. +``` + +1. [Install Nix](https://nixos.org/download.html) +2. [Install Direnv](https://direnv.net/docs/installation.html) +3. [Optionally install the Wire cachix cache to download binaries](https://app.cachix.org/cache/wire-server) + +Now, enabling `direnv` should install all the dependencies and add them to your `PATH`. Every time you `cd` into +the `wire-server-deploy` directory, the right dependencies will be available. + +``` +direnv allow + +ansible --version +ansible 2.9.12 +``` diff --git a/docs/src/how-to/install/dependencies.rst b/docs/src/how-to/install/dependencies.rst deleted file mode 100644 index 4c50f38d25..0000000000 --- a/docs/src/how-to/install/dependencies.rst +++ /dev/null @@ -1,74 +0,0 @@ -.. _dependencies: - -Dependencies on operator's machine --------------------------------------------------------------------- - -In order to operate a wire-server installation, you'll need a bunch of software -like Ansible, ``kubectl`` and Helm. - -Together with a matching checkout of the wire-server-deploy repository, -containing the Ansible Roles and Playbooks, you should be good to go. - -Checkout the repository, including its submodules: - -:: - - git clone --branch master https://github.com/wireapp/wire-server-deploy.git - cd wire-server-deploy - git submodule update --init --recursive - - -We provide a container containing all needed tools for setting up and -interacting with a wire-server cluster. - -Ensure you have Docker >= 20.10.14 installed, as the glibc version used is -incompatible with older container runtimes. - -Your Distro might ship an older version, so best see `how to install docker -`__. - -To bring the tools in scope, we run the container, and mount the local ``wire-server-deploy`` -checkout into it. - -Replace the container image tag with the commit id your ``wire-server-deploy`` -checkout is pointing to. - -:: - - WSD_COMMIT_ID=cdc1c84c1a10a4f5f1b77b51ee5655d0da7f9518 # set me - WSD_CONTAINER=quay.io/wire/wire-server-deploy:$WSD_COMMIT_ID - - sudo docker run -it --network=host \ - -v ${SSH_AUTH_SOCK:-nonexistent}:/ssh-agent \ - -v $HOME/.ssh:/root/.ssh \ - -v $PWD:/wire-server-deploy \ - -e SSH_AUTH_SOCK=/ssh-agent \ - $WSD_CONTAINER bash - - # Inside the container - bash-4.4# ansible --version - ansible 2.9.12 - -Once you're in there, you can move on to `installing kubernetes `__ - - -(Alternative) Installing dependencies using Direnv and Nix -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. warning:: - - This is an alternative approach to the above "wrapping container" one, which you should only use if you can't get above setup to work. - -1. `Install Nix `__ -2. `Install Direnv `__ -3. `Optionally install the Wire cachix cache to download binaries `__ - -Now, enabling ``direnv`` should install all the dependencies and add them to your ``PATH``. Every time you ``cd`` into -the ``wire-server-deploy`` directory, the right dependencies will be available. - -:: - - direnv allow - - ansible --version - ansible 2.9.12 diff --git a/docs/src/how-to/install/helm-prod.md b/docs/src/how-to/install/helm-prod.md new file mode 100644 index 0000000000..29045f1818 --- /dev/null +++ b/docs/src/how-to/install/helm-prod.md @@ -0,0 +1,208 @@ +(helm-prod)= + +# Installing wire-server (production) components using Helm + +```{note} +Code in this repository should be considered *beta*. As of 2020, we do not (yet) +run our production infrastructure on Kubernetes (but plan to do so soon). +``` + +## Introduction + +The following will install a version of all the wire-server components. These instructions are for reference, and may not set up what you would consider a production environment, due to the fact that there are varying definitions of 'production ready'. These instructions will cover what we consider to be a useful overlap of our users' production needs. They do not cover load balancing/distributing, using multiple datacenters, federating wire, or other forms of intercontinental/interplanetary distribution of the wire service infrastructure. If you deviate from these directions and need to contact us for support, please provide the deviations you made to fit your production environment along with your support request. + +Some of the instructions here will present you with two options: No AWS, and with AWS. The 'No AWS' instructions will not require any AWS infrastructure, but may have a reduced feature set. The 'with AWS' instructions will assume you have completed the setup procedures in {ref}`aws-prod`. + +### What will be installed? + +- wire-server (API) + : - user accounts, authentication, conversations + - assets handling (images, files, ...) + - notifications over websocket +- wire-webapp, a fully functioning web client (like `https://app.wire.com/`) +- wire-account-pages, user account management (a few pages relating to e.g. password reset procedures) + +### What will not be installed? + +- team-settings page +- SSO Capabilities + +Additionally, if you opt to do the 'No AWS' route, you will not get: + +- notifications over native push notifications via [FCM](https://firebase.google.com/docs/cloud-messaging/)/[APNS](https://developer.apple.com/notifications/) + +## Prerequisites + +You need to have access to a Kubernetes cluster running a Kubernetes version , and the `helm` local binary on your PATH. +Your Kubernetes cluster needs to have internal DNS services, so that wire-server can find it's databases. +You need to have docker on the machine you are using to perform this installation with, or a secure data path to a machine that runs docker. You will be using docker to generate security credentials for your wire installation. + +- If you want calling services, you need to have + + - FIXME + +- If you don't have a Kubernetes cluster, you have two options: + + - You can get access to a managed Kubernetes cluster with the cloud provider of your choice. + - You can install one if you have ssh access to a set of sufficiently large virtual machines, see {ref}`ansible-kubernetes` + +- If you don't have `helm` yet, see [Installing helm](https://helm.sh/docs/using_helm/#installing-helm). If you followed the instructions in {ref}`dependencies` should have helm installed already. + +Type `helm version`, you should, if everything is configured correctly, see a result similar this: + +``` +version.BuildInfo{Version:"v3.1.1", GitCommit:"afe70585407b420d0097d07b21c47dc511525ac8", GitTreeState:"clean", GoVersion:"go1.13.8"} +``` + +In case `kubectl version` shows both Client and Server versions, but `helm version` does not show a Server version, you may need to run `helm init`. The exact version matters less as long as both Client and Server versions match (or are very close). + +## Preparing to install charts from the internet with Helm + +If your environment is online, you need to add the remote wire Helm repository, to download wire charts. + +To enable the wire charts helm repository: + +```shell +helm repo add wire https://s3-eu-west-1.amazonaws.com/public.wire.com/charts +``` + +(You can see available helm charts by running `helm search repo wire/`. To see +new versions as time passes, you may need to run `helm repo update`) + +Great! Now you can start installing. + +There is a shell script for doing a version of the following procedure with Helm 22. For reference, examine [prod-setup.sh](https://github.com/wireapp/wire-server-deploy/blob/develop/bin/prod-setup.sh). + +## Watching changes as they happen + +Open a terminal and run: + +```shell +kubectl get pods -w +``` + +This will block your terminal and show some things happening as you proceed through this guide. Keep this terminal open and open a second terminal. + +## General installation notes + +```{note} +All helm and kubectl commands below can also take an extra `--namespace ` if you don't want to install into the default Kubernetes namespace. +``` + +## How to install charts that provide access to external databases + +Before you can deploy the helm charts that tell wire where external services +are, you need the 'values' and 'secrets' files for those charts to be +configured. Values and secrets YAML files provide helm charts with the settings +that are installed in Kubernetes. + +Assuming you have followed the procedures in the previous document, the values +and secrets files for cassandra, elasticsearch, and minio (if you are using it) +will have been filled in automatically. If not, examine the +`prod-values.example.yaml` files for each of these services in +values/\/, copy them to `values.yaml`, and then edit them. + +Once the values and secrets files for your databases have been configured, you +have to write a `values/databases-ephemeral/values.yaml` file to tell +databases-ephemeral what external database services you are using, and what +services you want databases-ephemeral to configure. We recommend you use the +'redis' component from this only, as the contents of redis are in fact +ephemeral. Look at the `values/databases-ephemeral/prod-values.example.yaml` +file + +Once you have values and secrets for your environment, open a terminal and run: + +```shell +helm upgrade --install cassandra-external wire/cassandra-external -f values/cassandra-external/values.yaml --wait +helm upgrade --install elasticsearch-external wire/elasticsearch-external -f values/elasticsearch-external/values.yaml --wait +helm upgrade --install databases-ephemeral wire/databases-ephemeral -f values/databases-ephemeral/values.yaml --wait +``` + +If you are using minio instead of AWS S3, you should also run: + +```shell +helm upgrade --install minio-external wire/minio-external -f values/minio-external/values.yaml --wait +``` + +## How to install fake AWS services for SNS / SQS + +AWS SNS is required to send notifications to clients. SQS is used to get notified of any devices that have discontinued using Wire (e.g. if you uninstall the app, the push notification token is removed, and the wire-server will get feedback for that using SQS). + +Note: *for using real SQS for real native push notifications instead, see also :ref:\`pushsns\`.* + +If you use the fake-aws version, clients will use the websocket method to receive notifications, which keeps connections to the servers open, draining battery. + +Open a terminal and run: + +```shell +cp values/fake-aws/prod-values.example.yaml values/fake-aws/values.yaml +helm upgrade --install fake-aws wire/fake-aws -f values/fake-aws/values.yaml --wait +``` + +You should see some pods being created in your first terminal as the above command completes. + +## Preparing to install wire-server + +As part of configuring wire-server, we need to change some values, and provide some secrets. We're going to copy the files for this to a new folder, so that you always have the originals for reference. + +```{note} +This part of the process makes use of overrides for helm charts. You may wish to read {ref}`understand-helm-overrides` first. +``` + +```shell +mkdir -p my-wire-server +cp values/wire-server/prod-secrets.example.yaml my-wire-server/secrets.yaml +cp values/wire-server/prod-values.example.yaml my-wire-server/values.yaml +``` + +## How to configure real SMTP (email) services + +In order for users to interact with their wire account, they need to receive mail from your wire server. + +If you are using a mail server, you will need to provide your authentication credentials before setting up wire. + +- Add your SMTP username in my-wire-server/values.yaml under `brig.config.smtp.username`. You may need to add an entry for username. +- Add your SMTP password is my-wire-server/secrets.yaml under `brig.secrets.smtpPassword`. + +## How to install fake SMTP (email) services + +If you are not making use of mail services, and are adding your users via some other means, you can use demo-smtp, as a placeholder. + +```shell +cp values/demo-smtp/prod-values.example.yaml values/demo-smtp/values.yaml +helm upgrade --install smtp wire/demo-smtp -f values/demo-smtp/values.yaml +``` + +You should see some pods being created in your first terminal as the above command completes. + +## How to install wire-server itself + +Open `my-wire-server/values.yaml` and replace `example.com` and other domains and subdomains with domains of your choosing. Look for the `# change this` comments. You can try using `sed -i 's/example.com//g' values.yaml`. + +1. If you are not using team settings, comment out `teamSettings` under `brig.config.externalURLs`. + +Generate some secrets: + +```shell +openssl rand -base64 64 | env LC_CTYPE=C tr -dc a-zA-Z0-9 | head -c 42 > my-wire-server/restund.txt +apt install docker-ce +sudo docker run --rm quay.io/wire/alpine-intermediate /dist/zauth -m gen-keypair -i 1 > my-wire-server/zauth.txt +``` + +1. Add the generated secret from my-wire-server/restund.txt to my-wire-serwer/secrets.yaml under `brig.secrets.turn.secret` +2. add **both** the public and private parts from zauth.txt to secrets.yaml under `brig.secrets.zAuth` +3. Add the public key from zauth.txt to secrets.yaml under `nginz.secrets.zAuth.publicKeys` + +Great, now try the installation: + +```shell +helm upgrade --install wire-server wire/wire-server -f my-wire-server/values.yaml -f my-wire-server/secrets.yaml --wait +``` + +(helmdns)= + +## DNS records + +```{eval-rst} +.. include:: includes/helm_dns-ingress-troubleshooting.inc.rst +``` diff --git a/docs/src/how-to/install/helm-prod.rst b/docs/src/how-to/install/helm-prod.rst deleted file mode 100644 index fb9b81841d..0000000000 --- a/docs/src/how-to/install/helm-prod.rst +++ /dev/null @@ -1,225 +0,0 @@ -.. _helm_prod: - -Installing wire-server (production) components using Helm -========================================================= - -.. note:: - - Code in this repository should be considered *beta*. As of 2020, we do not (yet) - run our production infrastructure on Kubernetes (but plan to do so soon). - -Introduction ------------- - -The following will install a version of all the wire-server components. These instructions are for reference, and may not set up what you would consider a production environment, due to the fact that there are varying definitions of 'production ready'. These instructions will cover what we consider to be a useful overlap of our users' production needs. They do not cover load balancing/distributing, using multiple datacenters, federating wire, or other forms of intercontinental/interplanetary distribution of the wire service infrastructure. If you deviate from these directions and need to contact us for support, please provide the deviations you made to fit your production environment along with your support request. - -Some of the instructions here will present you with two options: No AWS, and with AWS. The 'No AWS' instructions will not require any AWS infrastructure, but may have a reduced feature set. The 'with AWS' instructions will assume you have completed the setup procedures in :ref:`aws_prod`. - -What will be installed? -^^^^^^^^^^^^^^^^^^^^^^^ - -- wire-server (API) - - user accounts, authentication, conversations - - assets handling (images, files, ...) - - notifications over websocket -- wire-webapp, a fully functioning web client (like ``https://app.wire.com/``) -- wire-account-pages, user account management (a few pages relating to e.g. password reset procedures) - -What will not be installed? -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -- team-settings page -- SSO Capabilities - -Additionally, if you opt to do the 'No AWS' route, you will not get: - -- notifications over native push notifications via `FCM `__/`APNS `__ - -Prerequisites -------------- - -You need to have access to a Kubernetes cluster running a Kubernetes version , and the ``helm`` local binary on your PATH. -Your Kubernetes cluster needs to have internal DNS services, so that wire-server can find it's databases. -You need to have docker on the machine you are using to perform this installation with, or a secure data path to a machine that runs docker. You will be using docker to generate security credentials for your wire installation. - -* If you want calling services, you need to have - - * FIXME - -* If you don't have a Kubernetes cluster, you have two options: - - * You can get access to a managed Kubernetes cluster with the cloud provider of your choice. - * You can install one if you have ssh access to a set of sufficiently large virtual machines, see :ref:`ansible-kubernetes` - -* If you don't have ``helm`` yet, see `Installing helm `__. If you followed the instructions in :ref:`dependencies` should have helm installed already. - - -Type ``helm version``, you should, if everything is configured correctly, see a result similar this: - -:: - - version.BuildInfo{Version:"v3.1.1", GitCommit:"afe70585407b420d0097d07b21c47dc511525ac8", GitTreeState:"clean", GoVersion:"go1.13.8"} - -In case ``kubectl version`` shows both Client and Server versions, but ``helm version`` does not show a Server version, you may need to run ``helm init``. The exact version matters less as long as both Client and Server versions match (or are very close). - - -Preparing to install charts from the internet with Helm -------------------------------------------------------- -If your environment is online, you need to add the remote wire Helm repository, to download wire charts. - -To enable the wire charts helm repository: - -.. code:: shell - - helm repo add wire https://s3-eu-west-1.amazonaws.com/public.wire.com/charts - -(You can see available helm charts by running ``helm search repo wire/``. To see -new versions as time passes, you may need to run ``helm repo update``) - -Great! Now you can start installing. - -There is a shell script for doing a version of the following procedure with Helm 22. For reference, examine `prod-setup.sh `__. - -Watching changes as they happen -------------------------------- - -Open a terminal and run: - -.. code:: shell - - kubectl get pods -w - -This will block your terminal and show some things happening as you proceed through this guide. Keep this terminal open and open a second terminal. - -General installation notes --------------------------- - -.. note:: - - All helm and kubectl commands below can also take an extra ``--namespace `` if you don't want to install into the default Kubernetes namespace. - -How to install charts that provide access to external databases ---------------------------------------------------------------- - -Before you can deploy the helm charts that tell wire where external services -are, you need the 'values' and 'secrets' files for those charts to be -configured. Values and secrets YAML files provide helm charts with the settings -that are installed in Kubernetes. - -Assuming you have followed the procedures in the previous document, the values -and secrets files for cassandra, elasticsearch, and minio (if you are using it) -will have been filled in automatically. If not, examine the -``prod-values.example.yaml`` files for each of these services in -values//, copy them to ``values.yaml``, and then edit them. - -Once the values and secrets files for your databases have been configured, you -have to write a ``values/databases-ephemeral/values.yaml`` file to tell -databases-ephemeral what external database services you are using, and what -services you want databases-ephemeral to configure. We recommend you use the -'redis' component from this only, as the contents of redis are in fact -ephemeral. Look at the ``values/databases-ephemeral/prod-values.example.yaml`` -file - -Once you have values and secrets for your environment, open a terminal and run: - -.. code:: shell - - helm upgrade --install cassandra-external wire/cassandra-external -f values/cassandra-external/values.yaml --wait - helm upgrade --install elasticsearch-external wire/elasticsearch-external -f values/elasticsearch-external/values.yaml --wait - helm upgrade --install databases-ephemeral wire/databases-ephemeral -f values/databases-ephemeral/values.yaml --wait - -If you are using minio instead of AWS S3, you should also run: - -.. code:: shell - - helm upgrade --install minio-external wire/minio-external -f values/minio-external/values.yaml --wait - -How to install fake AWS services for SNS / SQS ----------------------------------------------- - -AWS SNS is required to send notifications to clients. SQS is used to get notified of any devices that have discontinued using Wire (e.g. if you uninstall the app, the push notification token is removed, and the wire-server will get feedback for that using SQS). - -Note: *for using real SQS for real native push notifications instead, see also :ref:`pushsns`.* - -If you use the fake-aws version, clients will use the websocket method to receive notifications, which keeps connections to the servers open, draining battery. - -Open a terminal and run: - -.. code:: shell - - cp values/fake-aws/prod-values.example.yaml values/fake-aws/values.yaml - helm upgrade --install fake-aws wire/fake-aws -f values/fake-aws/values.yaml --wait - -You should see some pods being created in your first terminal as the above command completes. - - -Preparing to install wire-server --------------------------------- -As part of configuring wire-server, we need to change some values, and provide some secrets. We're going to copy the files for this to a new folder, so that you always have the originals for reference. - -.. note:: - - This part of the process makes use of overrides for helm charts. You may wish to read :ref:`understand-helm-overrides` first. - - -.. code:: shell - - mkdir -p my-wire-server - cp values/wire-server/prod-secrets.example.yaml my-wire-server/secrets.yaml - cp values/wire-server/prod-values.example.yaml my-wire-server/values.yaml - - -How to configure real SMTP (email) services -------------------------------------------- -In order for users to interact with their wire account, they need to receive mail from your wire server. - -If you are using a mail server, you will need to provide your authentication credentials before setting up wire. - -- Add your SMTP username in my-wire-server/values.yaml under ``brig.config.smtp.username``. You may need to add an entry for username. -- Add your SMTP password is my-wire-server/secrets.yaml under ``brig.secrets.smtpPassword``. - - -How to install fake SMTP (email) services ------------------------------------------ -If you are not making use of mail services, and are adding your users via some other means, you can use demo-smtp, as a placeholder. - -.. code:: shell - - cp values/demo-smtp/prod-values.example.yaml values/demo-smtp/values.yaml - helm upgrade --install smtp wire/demo-smtp -f values/demo-smtp/values.yaml - - -You should see some pods being created in your first terminal as the above command completes. - -How to install wire-server itself ---------------------------------- - -Open ``my-wire-server/values.yaml`` and replace ``example.com`` and other domains and subdomains with domains of your choosing. Look for the ``# change this`` comments. You can try using ``sed -i 's/example.com//g' values.yaml``. - -1. If you are not using team settings, comment out ``teamSettings`` under ``brig.config.externalURLs``. - - -Generate some secrets: - -.. code:: shell - - openssl rand -base64 64 | env LC_CTYPE=C tr -dc a-zA-Z0-9 | head -c 42 > my-wire-server/restund.txt - apt install docker-ce - sudo docker run --rm quay.io/wire/alpine-intermediate /dist/zauth -m gen-keypair -i 1 > my-wire-server/zauth.txt - -1. Add the generated secret from my-wire-server/restund.txt to my-wire-serwer/secrets.yaml under ``brig.secrets.turn.secret`` -2. add **both** the public and private parts from zauth.txt to secrets.yaml under ``brig.secrets.zAuth`` -3. Add the public key from zauth.txt to secrets.yaml under ``nginz.secrets.zAuth.publicKeys`` - -Great, now try the installation: - -.. code:: shell - - helm upgrade --install wire-server wire/wire-server -f my-wire-server/values.yaml -f my-wire-server/secrets.yaml --wait - -.. _helmdns: - -DNS records ------------ - -.. include:: includes/helm_dns-ingress-troubleshooting.inc.rst diff --git a/docs/src/how-to/install/helm.md b/docs/src/how-to/install/helm.md new file mode 100644 index 0000000000..75ce93eda2 --- /dev/null +++ b/docs/src/how-to/install/helm.md @@ -0,0 +1,145 @@ +(helm)= + +# Installing wire-server (demo) components using helm + +## Introduction + +The following will install a demo version of all the wire-server components including the databases. This setup is not recommended in production but will get you started. + +Demo version means + +- easy setup - only one single machine with kubernetes is needed (make sure you have at least 4 CPU cores and 8 GB of memory available) +- no data persistence (everything stored in memory, will be lost) + +### What will be installed? + +- wire-server (API) + \- user accounts, authentication, conversations + \- assets handling (images, files, ...) + \- notifications over websocket +- wire-webapp, a fully functioning web client (like `https://app.wire.com`) +- wire-account-pages, user account management (a few pages relating to e.g. password reset) + +### What will not be installed? + +- notifications over native push notifications via [FCM](https://firebase.google.com/docs/cloud-messaging/)/[APNS](https://developer.apple.com/notifications/) +- audio/video calling servers using {ref}`understand-restund`) +- team-settings page + +## Prerequisites + +You need to have access to a kubernetes cluster, and the `helm` local binary on your PATH. + +- If you don't have a kubernetes cluster, you have two options: + + - You can get access to a managed kubernetes cluster with the cloud provider of your choice. + - You can install one if you have ssh access to a virtual machine, see {ref}`ansible-kubernetes` + +- If you don't have `helm` yet, see [Installing helm](https://helm.sh/docs/using_helm/#installing-helm). + +Type `helm version`, you should, if everything is configured correctly, see a result like this: + +``` +version.BuildInfo{Version:"v3.1.1", GitCommit:"afe70585407b420d0097d07b21c47dc511525ac8", GitTreeState:"clean", GoVersion:"go1.13.8"} +``` + +In case `kubectl version` shows both Client and Server versions, but `helm version` does not show a Server version, you may need to run `helm init`. The exact version (assuming `v2.X.X` - at the time of writing v3 is not yet supported) matters less as long as both Client and Server versions match (or are very close). + +## How to start installing charts from wire + +Enable the wire charts helm repository: + +```shell +helm repo add wire https://s3-eu-west-1.amazonaws.com/public.wire.com/charts +``` + +(You can see available helm charts by running `helm search repo wire/`. To see +new versions as time passes, you may need to run `helm repo update`) + +Great! Now you can start installing. + +```{note} +all commands below can also take an extra `--namespace ` if you don't want to install into the default kubernetes namespace. +``` + +## Watching changes as they happen + +Open a terminal and run + +```shell +kubectl get pods -w +``` + +This will block your terminal and show some things happening as you proceed through this guide. Keep this terminal open and open a second terminal. + +## How to install in-memory databases and external components + +In your second terminal, first install databases: + +```shell +helm upgrade --install databases-ephemeral wire/databases-ephemeral --wait +``` + +You should see some pods being created in your first terminal as the above command completes. + +You can do the following two steps (mock aws services and demo smtp +server) in parallel with the above in two more terminals, or +sequentially after database-ephemeral installation has succeeded. + +```shell +helm upgrade --install fake-aws wire/fake-aws --wait +helm upgrade --install smtp wire/demo-smtp --wait +``` + +## How to install wire-server itself + +```{note} +The following makes use of overrides for helm charts. You may wish to read {ref}`understand-helm-overrides` first. +``` + +Change back to the wire-server-deploy directory. Copy example demo values and secrets: + +```shell +mkdir -p wire-server && cd wire-server +cp ../values/wire-server/demo-secrets.example.yaml secrets.yaml +cp ../values/wire-server/demo-values.example.yaml values.yaml +``` + +Or, if you are not in wire-server-deploy, download example demo values and secrets: + +```shell +mkdir -p wire-server && cd wire-server +curl -sSL https://raw.githubusercontent.com/wireapp/wire-server-deploy/master/values/wire-server/demo-secrets.example.yaml > secrets.yaml +curl -sSL https://raw.githubusercontent.com/wireapp/wire-server-deploy/master/values/wire-server/demo-values.example.yaml > values.yaml +``` + +Open `values.yaml` and replace `example.com` and other domains and subdomains with domains of your choosing. Look for the `# change this` comments. You can try using `sed -i 's/example.com//g' values.yaml`. + +Generate some secrets (if you are using the docker image from {ref}`ansible-kubernetes`, you should open a shell on the host system for this): + +```shell +openssl rand -base64 64 | env LC_CTYPE=C tr -dc a-zA-Z0-9 | head -c 42 > restund.txt +docker run --rm quay.io/wire/alpine-intermediate /dist/zauth -m gen-keypair -i 1 > zauth.txt +``` + +1. Add the generated secret from restund.txt to secrets.yaml under `brig.secrets.turn.secret` +2. add **both** the public and private parts from zauth.txt to secrets.yaml under `brig.secrets.zAuth` +3. Add the public key from zauth.txt **also** to secrets.yaml under `nginz.secrets.zAuth.publicKeys` + +You can do this with an editor, or using sed: + +```shell +sed -i 's/secret:$/secret: content_of_restund.txt_file/' secrets.yaml +sed -i 's/publicKeys: ""/publicKeys: "public_key_from_zauth.txt_file"/' secrets.yaml +sed -i 's/privateKeys: ""/privateKeys: "private_key_from_zauth.txt_file"/' secrets.yaml +``` + +Great, now try the installation: + +```shell +helm upgrade --install wire-server wire/wire-server -f values.yaml -f secrets.yaml --wait +``` + +```{eval-rst} +.. include:: includes/helm_dns-ingress-troubleshooting.inc.rst +``` diff --git a/docs/src/how-to/install/helm.rst b/docs/src/how-to/install/helm.rst deleted file mode 100644 index 695a4c95a3..0000000000 --- a/docs/src/how-to/install/helm.rst +++ /dev/null @@ -1,154 +0,0 @@ -.. _helm: - -Installing wire-server (demo) components using helm -====================================================== - -Introduction ------------------ - -The following will install a demo version of all the wire-server components including the databases. This setup is not recommended in production but will get you started. - -Demo version means - -* easy setup - only one single machine with kubernetes is needed (make sure you have at least 4 CPU cores and 8 GB of memory available) -* no data persistence (everything stored in memory, will be lost) - -What will be installed? -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -- wire-server (API) - - user accounts, authentication, conversations - - assets handling (images, files, ...) - - notifications over websocket - -- wire-webapp, a fully functioning web client (like ``https://app.wire.com``) -- wire-account-pages, user account management (a few pages relating to e.g. password reset) - -What will not be installed? -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -- notifications over native push notifications via `FCM `__/`APNS `__ -- audio/video calling servers using :ref:`understand-restund`) -- team-settings page - -Prerequisites --------------------------------- - -You need to have access to a kubernetes cluster, and the ``helm`` local binary on your PATH. - -* If you don't have a kubernetes cluster, you have two options: - - * You can get access to a managed kubernetes cluster with the cloud provider of your choice. - * You can install one if you have ssh access to a virtual machine, see :ref:`ansible-kubernetes` - -* If you don't have ``helm`` yet, see `Installing helm `__. - -Type ``helm version``, you should, if everything is configured correctly, see a result like this: - -:: - - version.BuildInfo{Version:"v3.1.1", GitCommit:"afe70585407b420d0097d07b21c47dc511525ac8", GitTreeState:"clean", GoVersion:"go1.13.8"} - - -In case ``kubectl version`` shows both Client and Server versions, but ``helm version`` does not show a Server version, you may need to run ``helm init``. The exact version (assuming `v2.X.X` - at the time of writing v3 is not yet supported) matters less as long as both Client and Server versions match (or are very close). - -How to start installing charts from wire --------------------------------------------------- - -Enable the wire charts helm repository: - -.. code:: shell - - helm repo add wire https://s3-eu-west-1.amazonaws.com/public.wire.com/charts - -(You can see available helm charts by running ``helm search repo wire/``. To see -new versions as time passes, you may need to run ``helm repo update``) - -Great! Now you can start installing. - -.. note:: - - all commands below can also take an extra ``--namespace `` if you don't want to install into the default kubernetes namespace. - -Watching changes as they happen --------------------------------------------------- - -Open a terminal and run - -.. code:: shell - - kubectl get pods -w - -This will block your terminal and show some things happening as you proceed through this guide. Keep this terminal open and open a second terminal. - -How to install in-memory databases and external components --------------------------------------------------------------- - -In your second terminal, first install databases: - -.. code:: shell - - helm upgrade --install databases-ephemeral wire/databases-ephemeral --wait - -You should see some pods being created in your first terminal as the above command completes. - -You can do the following two steps (mock aws services and demo smtp -server) in parallel with the above in two more terminals, or -sequentially after database-ephemeral installation has succeeded. - -.. code:: shell - - helm upgrade --install fake-aws wire/fake-aws --wait - helm upgrade --install smtp wire/demo-smtp --wait - -How to install wire-server itself ---------------------------------------- - -.. note:: - - The following makes use of overrides for helm charts. You may wish to read :ref:`understand-helm-overrides` first. - -Change back to the wire-server-deploy directory. Copy example demo values and secrets: - -.. code:: shell - - mkdir -p wire-server && cd wire-server - cp ../values/wire-server/demo-secrets.example.yaml secrets.yaml - cp ../values/wire-server/demo-values.example.yaml values.yaml - -Or, if you are not in wire-server-deploy, download example demo values and secrets: - -.. code:: shell - - mkdir -p wire-server && cd wire-server - curl -sSL https://raw.githubusercontent.com/wireapp/wire-server-deploy/master/values/wire-server/demo-secrets.example.yaml > secrets.yaml - curl -sSL https://raw.githubusercontent.com/wireapp/wire-server-deploy/master/values/wire-server/demo-values.example.yaml > values.yaml - -Open ``values.yaml`` and replace ``example.com`` and other domains and subdomains with domains of your choosing. Look for the ``# change this`` comments. You can try using ``sed -i 's/example.com//g' values.yaml``. - -Generate some secrets (if you are using the docker image from :ref:`ansible-kubernetes`, you should open a shell on the host system for this): - -.. code:: shell - - openssl rand -base64 64 | env LC_CTYPE=C tr -dc a-zA-Z0-9 | head -c 42 > restund.txt - docker run --rm quay.io/wire/alpine-intermediate /dist/zauth -m gen-keypair -i 1 > zauth.txt - -1. Add the generated secret from restund.txt to secrets.yaml under ``brig.secrets.turn.secret`` -2. add **both** the public and private parts from zauth.txt to secrets.yaml under ``brig.secrets.zAuth`` -3. Add the public key from zauth.txt **also** to secrets.yaml under ``nginz.secrets.zAuth.publicKeys`` - -You can do this with an editor, or using sed: - -.. code:: shell - - sed -i 's/secret:$/secret: content_of_restund.txt_file/' secrets.yaml - sed -i 's/publicKeys: ""/publicKeys: "public_key_from_zauth.txt_file"/' secrets.yaml - sed -i 's/privateKeys: ""/privateKeys: "private_key_from_zauth.txt_file"/' secrets.yaml - -Great, now try the installation: - -.. code:: shell - - helm upgrade --install wire-server wire/wire-server -f values.yaml -f secrets.yaml --wait - -.. include:: includes/helm_dns-ingress-troubleshooting.inc.rst diff --git a/docs/src/how-to/install/includes/dns-federation.rst b/docs/src/how-to/install/includes/dns-federation.rst deleted file mode 100644 index c25184ffbe..0000000000 --- a/docs/src/how-to/install/includes/dns-federation.rst +++ /dev/null @@ -1,43 +0,0 @@ -DNS setup for federation ------------------------- - -SRV record -^^^^^^^^^^ - -One prerequisite to enable federation is an `SRV record `__ as defined in `RFC -2782 `__ that needs to be set up to allow the wire-server to be -discovered by other Wire backends. See the documentation on :ref:`discovery in federation` for more -information on the role of discovery in federation. - -The fields of the SRV record need to be populated as follows - -* ``service``: ``wire-server-federator`` -* ``proto``: ``tcp`` -* ``name``: -* ``TTL``: e.g. 600 (10 minutes) in an initial phase. This can be set to a higher value (e.g. 86400) if your systems are stable and DNS records don't change a lot. -* ``priority``: anything. A good default value would be 0 -* ``weight``: >0 for your server to be reachable. A good default value could be 10 -* ``port``: ``443`` -* ``target``: - -To give an example, assuming - -* your federation :ref:`Backend Domain ` is ``example.com`` -* your domains for other services already set up follow the convention ``.wire.example.org`` - -then your federation :ref:`Infra Domain ` would be ``federator.wire.example.org``. - -The SRV record would look as follows: - -.. code-block:: bash - - # _service._proto.name. ttl IN SRV priority weight port target. - _wire-server-federator._tcp.example.com. 600 IN SRV 0 10 443 federator.wire.example.org. - -DNS A record for the federator -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Background: ``federator`` is the server component responsible for incoming and outgoing requests to other backend; but it is proxied on -the incoming requests by the ingress component on kubernetes as shown in :ref:`Federation Architecture` - -As mentioned in :ref:`DNS setup for Helm`, you also need a ``federator.`` record, which, alongside your other DNS records that point to the ingress component, also needs to point to the IP of your ingress, i.e. the IP you want to provide services on. diff --git a/docs/src/how-to/install/includes/helm_dns-ingress-troubleshooting.inc.rst b/docs/src/how-to/install/includes/helm_dns-ingress-troubleshooting.inc.rst index 90b9e1f3b5..610ca8c784 100644 --- a/docs/src/how-to/install/includes/helm_dns-ingress-troubleshooting.inc.rst +++ b/docs/src/how-to/install/includes/helm_dns-ingress-troubleshooting.inc.rst @@ -143,8 +143,6 @@ Next, we want to redirect port 443 to the port the nginx https ingress nodeport * Option 2: Use ansible to do that, run the `iptables playbook `__ -.. include:: ./includes/dns-federation.rst - Trying things out ----------------- diff --git a/docs/src/how-to/install/index.md b/docs/src/how-to/install/index.md new file mode 100644 index 0000000000..183215c603 --- /dev/null +++ b/docs/src/how-to/install/index.md @@ -0,0 +1,30 @@ +# Installing wire-server + +```{toctree} +:glob: true +:maxdepth: 2 + +How to plan an installation +Version requirements +dependencies +(demo) How to install kubernetes +(demo) How to install wire-server using Helm +(production) Introduction +(production) How to install kubernetes and databases +(production) How to configure AWS services +(production) How to install wire-server using Helm +(production) How to monitor wire-server +(production) How to see centralized logs for wire-server +(production) Other configuration options +Server and team feature settings +Messaging Layer Security (MLS) +Web app settings +sft +restund +configure-federation +tls +How to install and set up Legal Hold +Managing authentication with ansible +Using tinc +Troubleshooting during installation +``` diff --git a/docs/src/how-to/install/index.rst b/docs/src/how-to/install/index.rst deleted file mode 100644 index 03802f43c7..0000000000 --- a/docs/src/how-to/install/index.rst +++ /dev/null @@ -1,30 +0,0 @@ -Installing wire-server -======================= - -.. toctree:: - :maxdepth: 2 - :glob: - - How to plan an installation - Version requirements - dependencies - (demo) How to install kubernetes - (demo) How to install wire-server using Helm - (production) Introduction - (production) How to install kubernetes and databases - (production) How to configure AWS services - (production) How to install wire-server using Helm - (production) How to monitor wire-server - (production) How to see centralized logs for wire-server - (production) Other configuration options - Server and team feature settings - Messaging Layer Security (MLS) - Web app settings - sft - restund - configure-federation - tls - How to install and set up Legal Hold - Managing authentication with ansible - Using tinc - Troubleshooting during installation diff --git a/docs/src/how-to/install/kubernetes.md b/docs/src/how-to/install/kubernetes.md new file mode 100644 index 0000000000..1c4430eefd --- /dev/null +++ b/docs/src/how-to/install/kubernetes.md @@ -0,0 +1,85 @@ +(ansible-kubernetes)= + +# Installing kubernetes for a demo installation (on a single virtual machine) + +## How to set up your hosts.ini file + +Assuming a single virtual machine with a public IP address running Ubuntu 18.04, with at least 5 CPU cores and at least 8 GB of memory. + +Move to `wire-server-deploy/ansible`: + +```shell +cd ansible/ +``` + +Then: + +```{eval-rst} +.. include:: includes/ansible-authentication-blob.rst +``` + +## Passwordless authentication + +Presuming a fresh default Ubuntu 18.04 installation, the following steps will enable the Ansible playbook to run without specifying passwords. + +This presumes you named your default Ubuntu user "wire", and X.X.X.X is the IP or domain name of the target server Ansible will install Kubernetes on. + +On the client (from `wire-server-deploy/ansible`), run: + +```shell +ssh-keygen -f /root/.ssh/id_rsa -t rsa -P +ssh-copy-id wire@X.X.X.X +sed -i 's/# ansible_user = .../ansible_user = wire/g' inventory/demo/hosts.ini +``` + +And on the server (X.X.X.X), run: + +```shell +echo 'wire ALL=(ALL) NOPASSWD:ALL' | sudo tee -a /etc/sudoers +``` + +Then on the client: + +```shell +cp inventory/demo/hosts.example.ini inventory/demo/hosts.ini +``` + +Open hosts.ini and replace `X.X.X.X` with the IP address of your virtual machine that you use for ssh access. You can try using: + +```shell +sed -i 's/X.X.X.X/1.2.3.4/g' inventory/demo/hosts.ini +``` + +## Minio setup + +In the `inventory/demo/hosts.ini` file, edit the minio variables in `[minio:vars]` (`prefix`, `domain` and `deeplink_title`) +by replacing `example.com` with your own domain. + +## How to install kubernetes + +From `wire-server-deploy/ansible`: + +``` +ansible-playbook -i inventory/demo/hosts.ini kubernetes.yml -vv +``` + +When the playbook finishes correctly (which can take up to 20 minutes), you should have a folder `artifacts` containing a file `admin.conf`. Copy this file: + +``` +mkdir -p ~/.kube +cp artifacts/admin.conf ~/.kube/config +KUBECONFIG=~/.kube/config +``` + +Make sure you can reach the server: + +``` +kubectl version +``` + +should give output similar to this: + +``` +Client Version: version.Info{Major:"1", Minor:"14", GitVersion:"v1.14.2", GitCommit:"66049e3b21efe110454d67df4fa62b08ea79a19b", GitTreeState:"clean", BuildDate:"2019-05-16T16:23:09Z", GoVersion:"go1.12.5", Compiler:"gc", Platform:"linux/amd64"} +Server Version: version.Info{Major:"1", Minor:"14", GitVersion:"v1.14.2", GitCommit:"66049e3b21efe110454d67df4fa62b08ea79a19b", GitTreeState:"clean", BuildDate:"2019-05-16T16:14:56Z", GoVersion:"go1.12.5", Compiler:"gc", Platform:"linux/amd64"} +``` diff --git a/docs/src/how-to/install/kubernetes.rst b/docs/src/how-to/install/kubernetes.rst deleted file mode 100644 index d4e423dfa4..0000000000 --- a/docs/src/how-to/install/kubernetes.rst +++ /dev/null @@ -1,83 +0,0 @@ -.. _ansible-kubernetes: - -Installing kubernetes for a demo installation (on a single virtual machine) -============================================================================ - - -How to set up your hosts.ini file -------------------------------------- - -Assuming a single virtual machine with a public IP address running Ubuntu 18.04, with at least 5 CPU cores and at least 8 GB of memory. - -Move to ``wire-server-deploy/ansible``: - -.. code:: shell - - cd ansible/ - -Then: - -.. include:: includes/ansible-authentication-blob.rst - -Passwordless authentication ---------------------------- - -Presuming a fresh default Ubuntu 18.04 installation, the following steps will enable the Ansible playbook to run without specifying passwords. - -This presumes you named your default Ubuntu user "wire", and X.X.X.X is the IP or domain name of the target server Ansible will install Kubernetes on. - -On the client (from ``wire-server-deploy/ansible``), run: - -.. code:: shell - - ssh-keygen -f /root/.ssh/id_rsa -t rsa -P - ssh-copy-id wire@X.X.X.X - sed -i 's/# ansible_user = .../ansible_user = wire/g' inventory/demo/hosts.ini - -And on the server (X.X.X.X), run: - -.. code:: shell - - echo 'wire ALL=(ALL) NOPASSWD:ALL' | sudo tee -a /etc/sudoers - -Then on the client: - -.. code:: shell - - cp inventory/demo/hosts.example.ini inventory/demo/hosts.ini - -Open hosts.ini and replace `X.X.X.X` with the IP address of your virtual machine that you use for ssh access. You can try using: - -.. code:: shell - - sed -i 's/X.X.X.X/1.2.3.4/g' inventory/demo/hosts.ini - -Minio setup ------------ - -In the ``inventory/demo/hosts.ini`` file, edit the minio variables in ``[minio:vars]`` (``prefix``, ``domain`` and ``deeplink_title``) -by replacing ``example.com`` with your own domain. - -How to install kubernetes --------------------------- - -From ``wire-server-deploy/ansible``:: - - ansible-playbook -i inventory/demo/hosts.ini kubernetes.yml -vv - -When the playbook finishes correctly (which can take up to 20 minutes), you should have a folder ``artifacts`` containing a file ``admin.conf``. Copy this file:: - - mkdir -p ~/.kube - cp artifacts/admin.conf ~/.kube/config - KUBECONFIG=~/.kube/config - -Make sure you can reach the server:: - - kubectl version - -should give output similar to this:: - - Client Version: version.Info{Major:"1", Minor:"14", GitVersion:"v1.14.2", GitCommit:"66049e3b21efe110454d67df4fa62b08ea79a19b", GitTreeState:"clean", BuildDate:"2019-05-16T16:23:09Z", GoVersion:"go1.12.5", Compiler:"gc", Platform:"linux/amd64"} - Server Version: version.Info{Major:"1", Minor:"14", GitVersion:"v1.14.2", GitCommit:"66049e3b21efe110454d67df4fa62b08ea79a19b", GitTreeState:"clean", BuildDate:"2019-05-16T16:14:56Z", GoVersion:"go1.12.5", Compiler:"gc", Platform:"linux/amd64"} - - diff --git a/docs/src/how-to/install/logging.rst b/docs/src/how-to/install/logging.md similarity index 60% rename from docs/src/how-to/install/logging.rst rename to docs/src/how-to/install/logging.md index 5d9368c83c..ca4ea9341d 100644 --- a/docs/src/how-to/install/logging.rst +++ b/docs/src/how-to/install/logging.md @@ -1,182 +1,164 @@ -.. _logging: +(logging)= -Installing centralized logging dashboards using Kibana -======================================================== +# Installing centralized logging dashboards using Kibana -Introduction ------------- +## Introduction This page shows you how to install Elasticsearch, Kibana, and fluent-bit to aggregate and visualize the logs from wire-server components. -Status -------- +## Status Logging support is in active development as of September 2019, some logs may not be visible yet, and certain parts are not fully automated yet. -Prerequisites -------------- +## Prerequisites You need to have wire-server installed, see either of -* :ref:`helm` -* :ref:`helm_prod`. +- {ref}`helm` +- {ref}`helm-prod`. +## Installing required helm charts -Installing required helm charts --------------------------------- - - -Deploying Elasticsearch -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +### Deploying Elasticsearch Elasticsearch indexes the logs and makes them searchable. The following elasticsearch-ephemeral chart makes use of the disk space the pod happens to run on. -:: - - $ helm install --namespace wire/elasticsearch-ephemeral +``` +$ helm install --namespace wire/elasticsearch-ephemeral +``` Note that since we are not specifying a release name during helm install, it generates a 'verb-noun' pair, and uses it. Elasticsearch's chart does not use the release name of the helm chart in the pod name, sadly. -Deploying Elasticsearch-Curator -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +### Deploying Elasticsearch-Curator Elasticsearch-curator trims the logs that are contained in elasticsearch, so that your elasticsearch pod does not get too large, crash, and need to be re-built. -:: - - $ helm install --namespace wire/elasticsearch-curator +``` +$ helm install --namespace wire/elasticsearch-curator +``` Note that since we are not specifying a release name during helm install, it generates a 'verb-noun' pair, and uses it. If you look at your pod names, you can see this name prepended to your pods in 'kubectl -n get pods'. -Deploying Kibana -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:: +### Deploying Kibana - $ helm install --namespace wire/kibana +``` +$ helm install --namespace wire/kibana +``` Note that since we are not specifying a release name during helm install, it generates a 'verb-noun' pair, and uses it. If you look at your pod names, you can see this name prepended to your pods in 'kubectl -n get pods'. -Deploying fluent-bit -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:: +### Deploying fluent-bit - $ helm install --namespace wire/fluent-bit +``` +$ helm install --namespace wire/fluent-bit +``` -Configuring fluent-bit ----------------------- +## Configuring fluent-bit -.. note:: +```{note} +The following makes use of overrides for helm charts. You may wish to read {ref}`understand-helm-overrides` first. +``` - The following makes use of overrides for helm charts. You may wish to read :ref:`understand-helm-overrides` first. - -Per pod-template, you can specify what parsers ``fluent-bit`` needs to +Per pod-template, you can specify what parsers `fluent-bit` needs to use to interpret the pod's logs in a structured way. By default, it just parses them as plain text. But, you can change this using a pod annotation. E.g.: -:: - - apiVersion: v1 - kind: Pod - metadata: - name: brig - labels: - app: brig - annotations: - fluentbit.io/parser: json - spec: - containers: - - name: apache - image: edsiper/apache_logs - -You can also define your own custom parsers in our ``fluent-bit`` -chart's ``values.yml``. For example, we have one defined for ``nginz``. +``` +apiVersion: v1 +kind: Pod +metadata: + name: brig + labels: + app: brig + annotations: + fluentbit.io/parser: json +spec: + containers: + - name: apache + image: edsiper/apache_logs +``` + +You can also define your own custom parsers in our `fluent-bit` +chart's `values.yml`. For example, we have one defined for `nginz`. For more info, see : -https://github.com/fluent/fluent-bit-docs/blob/master/filter/kubernetes.md#kubernetes-annotations + Alternately, if there is already fluent-bit deployed in your environment, get the helm name for the deployment (verb-noun prepended to the pod name), and -:: - - $ helm upgrade --namespace wire/fluent-bit +``` +$ helm upgrade --namespace wire/fluent-bit +``` Note that since we are not specifying a release name during helm install, it generates a 'verb-noun' pair, and uses it. if you look at your pod names, you can see this name prepended to your pods in 'kubectl -n get pods'. -.. _post-install-kibana-setup: +(post-install-kibana-setup)= -Post-install kibana setup --------------------------- +## Post-install kibana setup Get the pod name for your kibana instance (not the one set up with fluent-bit), and -:: - - $ kubectl -n port-forward 5601:5601 +``` +$ kubectl -n port-forward 5601:5601 +``` go to 127.0.0.1:5601 in your web browser. 1. Click on 'discover'. -2. Use ``kubernetes_cluster-*`` as the Index pattern. +2. Use `kubernetes_cluster-*` as the Index pattern. 3. Click on 'Next step' 4. Click on the 'Time Filter field name' dropdown, and select - '@timestamp'. + '. 5. Click on 'create index patern'. - -Usage after installation -------------------------- +## Usage after installation Get the pod name for your kibana instance (not the one set up with fluent-bit), and -:: - - $ kubectl -n port-forward 5601:5601 +``` +$ kubectl -n port-forward 5601:5601 +``` Go to 127.0.0.1:5601 in your web browser. Click on 'discover' to view data. -.. _nuking-it-all: +(nuking-it-all)= -Nuking it all. --------------- +## Nuking it all. -Find the names of the helm releases for your pods (look at ``helm ls --all`` -and ``kubectl -n get pods`` , and run -``helm del --purge`` for each of them. +Find the names of the helm releases for your pods (look at `helm ls --all` +and `kubectl -n get pods` , and run +`helm del --purge` for each of them. Note: Elasticsearch does not use the name of the helm chart, and therefore is harder to identify. -Debugging ---------- - -:: +## Debugging - kubectl -n logs +``` +kubectl -n logs +``` -How this was developed -^^^^^^^^^^^^^^^^^^^^^^^^ +### How this was developed First, we deployed elasticsearch with the elasticsearch-ephemeral chart, then kibana. then we deployed fluent-bit, which set up a kibana of it's diff --git a/docs/src/how-to/install/monitoring.rst b/docs/src/how-to/install/monitoring.md similarity index 58% rename from docs/src/how-to/install/monitoring.rst rename to docs/src/how-to/install/monitoring.md index ea900526cc..18f5a8865b 100644 --- a/docs/src/how-to/install/monitoring.rst +++ b/docs/src/how-to/install/monitoring.md @@ -1,21 +1,19 @@ -.. _monitoring: +(monitoring)= -Monitoring wire-server using Prometheus and Grafana -======================================================= +# Monitoring wire-server using Prometheus and Grafana All wire-server helm charts offering prometheus metrics expose a `metrics.serviceMonitor.enabled` option. If these are set to true, the helm charts will install `ServiceMonitor` resources, which can be used to mark services for scraping by -[Prometheus Operator](https://prometheus-operator.dev/), -[Grafana Agent Operator](https://grafana.com/docs/grafana-cloud/kubernetes-monitoring/agent-k8s/), +\[Prometheus Operator\](), +\[Grafana Agent Operator\](), or similar prometheus-compatible tools. Refer to their documentation for installation. -Dashboards ------------------ +## Dashboards -Grafana dashboard configurations are included as JSON inside the ``dashboards`` +Grafana dashboard configurations are included as JSON inside the `dashboards` directory. You may import these via Grafana's web UI. diff --git a/docs/src/how-to/install/planning.rst b/docs/src/how-to/install/planning.md similarity index 55% rename from docs/src/how-to/install/planning.rst rename to docs/src/how-to/install/planning.md index 29e84f97a6..1c3b1a5f44 100644 --- a/docs/src/how-to/install/planning.rst +++ b/docs/src/how-to/install/planning.md @@ -1,10 +1,8 @@ -Implementation plan -==================================== +# Implementation plan There are two types of implementation: demo and production. -Demo installation (trying functionality out) ------------------------------------------------ +## Demo installation (trying functionality out) Please note that there is no way to migrate data from a demo installation to a production installation - it is really meant as a way @@ -14,36 +12,36 @@ Please note your data will be in-memory only and may disappear at any given mome What you need: -- a way to create **DNS records** for your domain name (e.g. - ``wire.example.com``) -- a way to create **SSL/TLS certificates** for your domain name (to allow - connecting via ``https://``) -- Either one of the following: +- a way to create **DNS records** for your domain name (e.g. + `wire.example.com`) - - A kubernetes cluster (some cloud providers offer a managed - kubernetes cluster these days). - - One single virtual machine running ubuntu 18.04 with at least 20 GB of disk, 8 GB of memory, and 8 CPU cores. +- a way to create **SSL/TLS certificates** for your domain name (to allow + connecting via `https://`) -A demo installation will look a bit like this: +- Either one of the following: + + - A kubernetes cluster (some cloud providers offer a managed + kubernetes cluster these days). + - One single virtual machine running ubuntu 18.04 with at least 20 GB of disk, 8 GB of memory, and 8 CPU cores. -.. figure:: img/architecture-demo.png +A demo installation will look a bit like this: - Demo installation (1 VM) +```{figure} img/architecture-demo.png +Demo installation (1 VM) +``` -Next steps for demo installation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +### Next steps for demo installation -If you already have a kubernetes cluster, your next step will be :ref:`helm`, otherwise, your next step will be :ref:`ansible-kubernetes` +If you already have a kubernetes cluster, your next step will be {ref}`helm`, otherwise, your next step will be {ref}`ansible-kubernetes` -.. _planning_prod: +(planning-prod)= -Production installation (persistent data, high-availability) --------------------------------------------------------------- +## Production installation (persistent data, high-availability) What you need: -- a way to create **DNS records** for your domain name (e.g. ``wire.example.com``) -- a way to create **SSL/TLS certificates** for your domain name (to allow connecting via ``https://wire.example.com``) +- a way to create **DNS records** for your domain name (e.g. `wire.example.com`) +- a way to create **SSL/TLS certificates** for your domain name (to allow connecting via `https://wire.example.com`) - A **kubernetes cluster with at least 3 worker nodes and at least 3 etcd nodes** (some cloud providers offer a managed kubernetes cluster these days) - minimum **17 virtual machines** for components outside kubernetes (cassandra, minio, elasticsearch, redis, restund) @@ -51,13 +49,15 @@ A recommended installation of Wire-server in any regular data centre, configured with high-availability will require the following virtual servers: +```{eval-rst} .. include:: includes/vm-table.rst +``` A production installation will look a bit like this: -.. figure:: img/architecture-server-ha.png - - Production installation in High-Availability mode +```{figure} img/architecture-server-ha.png +Production installation in High-Availability mode +``` If you use a private datacenter (not a cloud provider), the easiest is to have three physical servers, each with one virtual machine for each @@ -71,7 +71,6 @@ Ensure that your VMs have IP addresses that do not change. Avoid `10.x.x.x` network address schemes, and instead use something like `192.168.x.x` or `172.x.x.x`. This is because internally, Kubernetes already uses a `10.x.x.x` address scheme, creating a potential conflict. -Next steps for high-available production installation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +### Next steps for high-available production installation -Your next step will be :ref:`ansible_vms` +Your next step will be {ref}`ansible-vms` diff --git a/docs/src/how-to/install/prod-intro.md b/docs/src/how-to/install/prod-intro.md new file mode 100644 index 0000000000..a908c14c30 --- /dev/null +++ b/docs/src/how-to/install/prod-intro.md @@ -0,0 +1,58 @@ +# Introduction + +```{warning} +It is *strongly recommended* to have followed and completed the demo installation {ref}`helm` before continuing with this page. The demo installation is simpler, and already makes you aware of a few things you need (TLS certs, DNS, a VM, ...). +``` + +```{note} +All required dependencies for doing an installation can be found here {ref}`dependencies`. +``` + +A production installation consists of several parts: + +Part 1 - you're on your own here, and need to create a set of VMs as detailed in {ref}`planning-prod` + +Part 2 ({ref}`ansible-vms`) deals with installing components directly on a set of virtual machines, such as kubernetes itself, as well as databases. It makes use of ansible to achieve that. + +Part 3 ({ref}`helm-prod`) is similar to the demo installation, and uses the tool `helm` to install software on top of kubernetes. + +Part 4 ({ref}`configuration-options`) details other possible configuration options and settings to fit your needs. + +## What will be installed by following these parts? + +- highly-available and persistent databases (cassandra, elasticsearch) + +- kubernetes + +- restund (audio/video calling) servers ( see also {ref}`understand-restund`) + +- wire-server (API) + \- user accounts, authentication, conversations + \- assets handling (images, files, ...) + \- notifications over websocket + \- single-sign-on with SAML + +- wire-webapp + + - fully functioning web client (like `https://app.wire.com`) + +- wire-account-pages + + - user account management (a few pages relating to e.g. password reset) + +## What will not be installed? + +- notifications over native push notification via [FCM](https://firebase.google.com/docs/cloud-messaging/)/[APNS](https://developer.apple.com/notifications/) + +## What will not be installed by default? + +- 3rd party proxying - requires accounts with third-party providers +- team-settings page for team management (including invitations, requires access to a private repository - get in touch with us for access) + +## Getting support + +[Get in touch](https://wire.com/pricing/). + +## Next steps for high-available production installation + +Your next step will be part 2, {ref}`ansible-vms` diff --git a/docs/src/how-to/install/prod-intro.rst b/docs/src/how-to/install/prod-intro.rst deleted file mode 100644 index 420b5fc296..0000000000 --- a/docs/src/how-to/install/prod-intro.rst +++ /dev/null @@ -1,60 +0,0 @@ -Introduction -============= - -.. warning:: - - It is *strongly recommended* to have followed and completed the demo installation :ref:`helm` before continuing with this page. The demo installation is simpler, and already makes you aware of a few things you need (TLS certs, DNS, a VM, ...). - -.. note:: - All required dependencies for doing an installation can be found here :ref:`dependencies`. - -A production installation consists of several parts: - -Part 1 - you're on your own here, and need to create a set of VMs as detailed in :ref:`planning_prod` - -Part 2 (:ref:`ansible_vms`) deals with installing components directly on a set of virtual machines, such as kubernetes itself, as well as databases. It makes use of ansible to achieve that. - -Part 3 (:ref:`helm_prod`) is similar to the demo installation, and uses the tool ``helm`` to install software on top of kubernetes. - -Part 4 (:ref:`configuration_options`) details other possible configuration options and settings to fit your needs. - -What will be installed by following these parts? -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -- highly-available and persistent databases (cassandra, elasticsearch) -- kubernetes -- restund (audio/video calling) servers ( see also :ref:`understand-restund`) -- wire-server (API) - - user accounts, authentication, conversations - - assets handling (images, files, ...) - - notifications over websocket - - single-sign-on with SAML - -- wire-webapp - - - fully functioning web client (like ``https://app.wire.com``) - -- wire-account-pages - - - user account management (a few pages relating to e.g. password reset) - -What will not be installed? -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -- notifications over native push notification via `FCM `__/`APNS `__ - -What will not be installed by default? -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -- 3rd party proxying - requires accounts with third-party providers -- team-settings page for team management (including invitations, requires access to a private repository - get in touch with us for access) - -Getting support -^^^^^^^^^^^^^^^^ - -`Get in touch `__. - -Next steps for high-available production installation -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Your next step will be part 2, :ref:`ansible_vms` diff --git a/docs/src/how-to/install/restund.md b/docs/src/how-to/install/restund.md new file mode 100644 index 0000000000..90a616b105 --- /dev/null +++ b/docs/src/how-to/install/restund.md @@ -0,0 +1,80 @@ +(install-restund)= + +# Installing Restund + +## Background + +Restund servers allow two users on different networks to have a Wire audio or video call. + +Please refer to the following {ref}`section to better understand Restund and how it works `. + +## Installation instructions + +To Install Restund, do the following: + +1. In your `hosts.ini` file, in the `[restund:vars]` section, set + the `restund_network_interface` to the name of the interface + you want restund to talk to clients on. This value defaults to the + `default_ipv4_address`, with a fallback to `eth0`. +2. (optional) `restund_peer_udp_advertise_addr=Y.Y.Y.Y`: set this to + the IP to advertise for other restund servers if different than the + ip on the 'restund_network_interface'. If using + 'restund_peer_udp_advertise_addr', make sure that UDP (!) traffic + from any restund server (including itself) can reach that IP (for + `restund <-> restund` communication). This should only be necessary + if you're installing restund on a VM that is reachable on a public IP + address but the process cannot bind to that public IP address + directly (e.g. on AWS VPC VM). If unset, `restund <-> restund` UDP + traffic will default to the IP in the `restund_network_interface`. + +```ini +[all] +(...) +restund01 ansible_host=X.X.X.X + +(...) + +[all:vars] +## Set the network interface name for restund to bind to if you have more than one network interface +## If unset, defaults to the ansible_default_ipv4 (if defined) otherwise to eth0 +restund_network_interface = eth0 + +(see `defaults/main.yml `__ for a full list of variables to change if necessary) +``` + +3. Place a copy of the PEM formatted certificate and key you are going + to use for TLS communication to the restund server in + `/tmp/tls_cert_and_priv_key.pem`. Remove it after you have + completed deploying restund with ansible. +4. Use Ansible to actually install using the restund playbook: + +```bash +ansible-playbook -i hosts.ini restund.yml -vv +``` + +For information on setting up and using ansible-playbook to install Wire components, see {ref}`this page `. + +### Private Subnets + +By default, Restund is configured with a firewall that filters-out CIDR networks. + +If you need to enable Restund to connect to a CIDR addressed host or network, you can specify a list of private subnets in [CIDR format](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing), which will override Restund's firewall's default settings of filtering-out CIDR networks. + +You do this by setting the `restund_allowed_private_network_cidrs` option of the `[restund:vars]` section of the ansible inventory file ([for example this file](https://github.com/wireapp/wire-server-deploy/blob/master/ansible/inventory/prod/hosts.example.ini#L72)): + +```ini +[restund:vars] +## Set the network interface name for restund to bind to if you have more than one network interface +## If unset, defaults to the ansible_default_ipv4 (if defined) otherwise to eth0 +# restund_network_interface = eth0 +restund_allowed_private_network_cidrs=192.168.0.1/32 +``` + +This is needed, for example, to allow talking to the logging server if it is on a separate network: + +The private subnets only need to override the RFC-defined private networks, which Wire firewalls off by default: + +- 192.168.x.x +- 10.x.x.x +- 172.16.x.x - 172.31.x.x +- Etc... diff --git a/docs/src/how-to/install/restund.rst b/docs/src/how-to/install/restund.rst deleted file mode 100644 index 732f0d0e26..0000000000 --- a/docs/src/how-to/install/restund.rst +++ /dev/null @@ -1,88 +0,0 @@ -.. _install-restund: - -Installing Restund -================== - -Background -~~~~~~~~~~ - -Restund servers allow two users on different networks to have a Wire audio or video call. - -Please refer to the following :ref:`section to better understand Restund and how it works `. - -Installation instructions -~~~~~~~~~~~~~~~~~~~~~~~~~ - -To Install Restund, do the following: - - -1. In your ``hosts.ini`` file, in the ``[restund:vars]`` section, set - the ``restund_network_interface`` to the name of the interface - you want restund to talk to clients on. This value defaults to the - ``default_ipv4_address``, with a fallback to ``eth0``. - -2. (optional) ``restund_peer_udp_advertise_addr=Y.Y.Y.Y``: set this to - the IP to advertise for other restund servers if different than the - ip on the 'restund_network_interface'. If using - 'restund_peer_udp_advertise_addr', make sure that UDP (!) traffic - from any restund server (including itself) can reach that IP (for - ``restund <-> restund`` communication). This should only be necessary - if you're installing restund on a VM that is reachable on a public IP - address but the process cannot bind to that public IP address - directly (e.g. on AWS VPC VM). If unset, ``restund <-> restund`` UDP - traffic will default to the IP in the ``restund_network_interface``. - -.. code:: ini - - [all] - (...) - restund01 ansible_host=X.X.X.X - - (...) - - [all:vars] - ## Set the network interface name for restund to bind to if you have more than one network interface - ## If unset, defaults to the ansible_default_ipv4 (if defined) otherwise to eth0 - restund_network_interface = eth0 - - (see `defaults/main.yml `__ for a full list of variables to change if necessary) - -3. Place a copy of the PEM formatted certificate and key you are going - to use for TLS communication to the restund server in - ``/tmp/tls_cert_and_priv_key.pem``. Remove it after you have - completed deploying restund with ansible. - -4. Use Ansible to actually install using the restund playbook: - -.. code:: bash - - ansible-playbook -i hosts.ini restund.yml -vv - -For information on setting up and using ansible-playbook to install Wire components, see :ref:`this page `. - -Private Subnets ---------------- - -By default, Restund is configured with a firewall that filters-out CIDR networks. - -If you need to enable Restund to connect to a CIDR addressed host or network, you can specify a list of private subnets in `CIDR format `__, which will override Restund's firewall's default settings of filtering-out CIDR networks. - -You do this by setting the ``restund_allowed_private_network_cidrs`` option of the ``[restund:vars]`` section of the ansible inventory file (`for example this file `__): - -.. code:: ini - - [restund:vars] - ## Set the network interface name for restund to bind to if you have more than one network interface - ## If unset, defaults to the ansible_default_ipv4 (if defined) otherwise to eth0 - # restund_network_interface = eth0 - restund_allowed_private_network_cidrs=192.168.0.1/32 - -This is needed, for example, to allow talking to the logging server if it is on a separate network: - -The private subnets only need to override the RFC-defined private networks, which Wire firewalls off by default: - -* 192.168.x.x -* 10.x.x.x -* 172.16.x.x - 172.31.x.x -* Etc... - diff --git a/docs/src/how-to/install/sft.rst b/docs/src/how-to/install/sft.md similarity index 67% rename from docs/src/how-to/install/sft.rst rename to docs/src/how-to/install/sft.md index 2824d6827a..e4560c7216 100644 --- a/docs/src/how-to/install/sft.rst +++ b/docs/src/how-to/install/sft.md @@ -1,125 +1,116 @@ -.. _install-sft: +(install-sft)= -Installing Conference Calling 2.0 (aka SFT) -=========================================== +# Installing Conference Calling 2.0 (aka SFT) -Background -~~~~~~~~~~ +## Background -Please refer to the following :ref:`section to better understand SFT and how it works `. +Please refer to the following {ref}`section to better understand SFT and how it works `. +### As part of the wire-server umbrella chart -As part of the wire-server umbrella chart ------------------------------------------ +`` sftd` `` will be installed as part of the `wire-server` umbrella chart if you set `tags.sftd: true` -`sftd`` will be installed as part of the ``wire-server`` umbrella chart if you set `tags.sftd: true` +In your `./values/wire-server/values.yaml` file you should set the following settings: -In your ``./values/wire-server/values.yaml`` file you should set the following settings: +```yaml +tags: + sftd: true -.. code:: yaml +sftd: + host: sftd.example.com # Replace example.com with your domain + allowOrigin: webapp.example.com # Should be the address you used for the webapp deployment +``` - tags: - sftd: true +In your `secrets.yaml` you should set the TLS keys for sftd domain: - sftd: - host: sftd.example.com # Replace example.com with your domain - allowOrigin: webapp.example.com # Should be the address you used for the webapp deployment +```yaml +sftd: + tls: + crt: | + + key: | + +``` -In your ``secrets.yaml`` you should set the TLS keys for sftd domain: +You should also make sure that you configure brig to know about the SFT server in your `./values/wire-server/values.yaml` file: -.. code:: yaml - - sftd: - tls: - crt: | - - key: | - - -You should also make sure that you configure brig to know about the SFT server in your ``./values/wire-server/values.yaml`` file: - -.. code:: yaml - - brig: - optSettings: - setSftStaticUrl: "https://sftd.example.com:443" +```yaml +brig: + optSettings: + setSftStaticUrl: "https://sftd.example.com:443" +``` Now you can deploy as usual: -.. code:: shell +```shell +helm upgrade wire-server wire/wire-server --values ./values/wire-server/values.yaml +``` - helm upgrade wire-server wire/wire-server --values ./values/wire-server/values.yaml - - -Standalone ----------- +### Standalone The SFT component is also shipped as a separate helm chart. Installation is similar to installing -the charts as in :ref:`helm_prod`. +the charts as in {ref}`helm-prod`. Some people might want to run SFT separately, because the deployment lifecycle for the SFT is a bit more intricate. For example, -if you want to avoid dropping calls during an upgrade, you'd set the ``terminationGracePeriodSeconds`` of the SFT to a high number, to wait -for calls to drain before updating to the new version (See `technical documentation `__). that would cause your otherwise snappy upgrade of the ``wire-server`` chart to now take a long time, as it waits for all -the SFT servers to drain. If this is a concern for you, we advice installing ``sftd`` as a separate chart. - -It is important that you disable ``sftd`` in the ``wire-server`` umbrella chart, by setting this in your ``./values/wire-server/values.yaml`` file +if you want to avoid dropping calls during an upgrade, you'd set the `terminationGracePeriodSeconds` of the SFT to a high number, to wait +for calls to drain before updating to the new version (See [technical documentation](https://github.com/wireapp/wire-server/blob/develop/charts/sftd/README.md)). that would cause your otherwise snappy upgrade of the `wire-server` chart to now take a long time, as it waits for all +the SFT servers to drain. If this is a concern for you, we advice installing `sftd` as a separate chart. -.. code:: yaml +It is important that you disable `sftd` in the `wire-server` umbrella chart, by setting this in your `./values/wire-server/values.yaml` file - tags: - sftd: false +```yaml +tags: + sftd: false +``` +By default `sftd` doesn't need to set that many options, so we define them inline. However, you could of course also set these values in a `values.yaml` file. -By default ``sftd`` doesn't need to set that many options, so we define them inline. However, you could of course also set these values in a ``values.yaml`` file. +SFT will deploy a Kubernetes Ingress on `$SFTD_HOST`. Make sure that the domain name `$SFTD_HOST` points to your ingress IP as set up in {ref}`helm-prod`. The SFT also needs to be made aware of the domain name of the webapp that you set up in {ref}`helm-prod` for setting up the appropriate CSP headers. -SFT will deploy a Kubernetes Ingress on ``$SFTD_HOST``. Make sure that the domain name ``$SFTD_HOST`` points to your ingress IP as set up in :ref:`helm_prod`. The SFT also needs to be made aware of the domain name of the webapp that you set up in :ref:`helm_prod` for setting up the appropriate CSP headers. - -.. code:: shell - - export SFTD_HOST=sftd.example.com - export WEBAPP_HOST=webapp.example.com +```shell +export SFTD_HOST=sftd.example.com +export WEBAPP_HOST=webapp.example.com +``` Now you can install the chart: -.. code:: shell - - helm upgrade --install sftd wire/sftd --set - helm install sftd wire/sftd \ - --set host=$SFTD_HOST \ - --set allowOrigin=https://$WEBAPP_HOST \ - --set-file tls.crt=/path/to/tls.crt \ - --set-file tls.key=/path/to/tls.key - -You should also make sure that you configure brig to know about the SFT server, in the ``./values/wire-server/values.yaml`` file: +```shell +helm upgrade --install sftd wire/sftd --set +helm install sftd wire/sftd \ + --set host=$SFTD_HOST \ + --set allowOrigin=https://$WEBAPP_HOST \ + --set-file tls.crt=/path/to/tls.crt \ + --set-file tls.key=/path/to/tls.key +``` -.. code:: yaml +You should also make sure that you configure brig to know about the SFT server, in the `./values/wire-server/values.yaml` file: - brig: - optSettings: - setSftStaticUrl: "https://sftd.example.com:443" +```yaml +brig: + optSettings: + setSftStaticUrl: "https://sftd.example.com:443" +``` -And then roll-out the change to the ``wire-server`` chart +And then roll-out the change to the `wire-server` chart -.. code:: shell +```shell +helm upgrade wire-server wire/wire-server --values ./values/wire-server/values.yaml +``` - helm upgrade wire-server wire/wire-server --values ./values/wire-server/values.yaml +For more advanced setups please refer to the [technical documentation](https://github.com/wireapp/wire-server/blob/develop/charts/sftd/README.md). -For more advanced setups please refer to the `technical documentation `__. +(install-sft-firewall-rules)= +### Firewall rules -.. _install-sft-firewall-rules: - -Firewall rules --------------- - -The SFT allocates media addresses in the UDP :ref:`default port range `. Ingress and +The SFT allocates media addresses in the UDP {ref}`default port range `. Ingress and egress traffic should be allowed for this range. Furthermore the SFT needs to be -able to reach the :ref:`Restund server `, as it uses STUN and TURN in cases the client +able to reach the {ref}`Restund server `, as it uses STUN and TURN in cases the client can not directly connect to the SFT. In practise this means the SFT should -allow ingress and egress traffic on the UDP :ref:`default port range ` from and -to both, clients and :ref:`Restund servers `. +allow ingress and egress traffic on the UDP {ref}`default port range ` from and +to both, clients and {ref}`Restund servers `. -*For more information on this port range, how to read and change it, and how to configure your firewall, please see* :ref:`this note `. +*For more information on this port range, how to read and change it, and how to configure your firewall, please see* {ref}`this note `. The SFT also has an HTTP interface for initializing (allocation) or joining (signaling) a call. This is exposed through the ingress controller as an HTTPS service. @@ -131,6 +122,7 @@ An SFT instance does **not** communicate with other SFT instances, TURN does tal Recapitulation table: +```{eval-rst} +----------------------------+-------------+-------------+-----------+----------+-----------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Name | Origin | Destination | Direction | Protocol | Ports | Action (Policy) | Description | +============================+=============+=============+===========+==========+=============================================================================+======================================+===============================================================================================================================================================================================+ @@ -146,6 +138,6 @@ Recapitulation table: +----------------------------+-------------+-------------+-----------+----------+-----------------------------------------------------------------------------+--------------------------------------+ | | Allowing SFT media egress | Here | Anny | Outgoing | UDP | 32768-61000 | Allow | | +----------------------------+-------------+-------------+-----------+----------+-----------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +``` - -*For more information, please refer to the source code of the Ansible role:* `sft-server `__. +*For more information, please refer to the source code of the Ansible role:* [sft-server](https://github.com/wireapp/ansible-sft/blob/develop/roles/sft-server/tasks/traffic.yml). diff --git a/docs/src/how-to/install/tls.md b/docs/src/how-to/install/tls.md new file mode 100644 index 0000000000..f3a044597a --- /dev/null +++ b/docs/src/how-to/install/tls.md @@ -0,0 +1,52 @@ +(tls)= + +# Configure TLS ciphers + +The following table lists recommended ciphers for TLS server setups, which should be used in wire deployments. + +| Cipher | Version | Wire default | [BSI TR-02102-2] | [Mozilla TLS Guideline] | +| ----------------------------- | ------- | ------------ | ---------------- | ----------------------- | +| ECDHE-ECDSA-AES128-GCM-SHA256 | TLSv1.2 | no | **yes** | intermediate | +| ECDHE-RSA-AES128-GCM-SHA256 | TLSv1.2 | no | **yes** | intermediate | +| ECDHE-ECDSA-AES256-GCM-SHA384 | TLSv1.2 | **yes** | **yes** | intermediate | +| ECDHE-RSA-AES256-GCM-SHA384 | TLSv1.2 | **yes** | **yes** | intermediate | +| ECDHE-ECDSA-CHACHA20-POLY1305 | TLSv1.2 | no | no | intermediate | +| ECDHE-RSA-CHACHA20-POLY1305 | TLSv1.2 | no | no | intermediate | +| TLS_AES_128_GCM_SHA256 | TLSv1.3 | **yes** | **yes** | **modern** | +| TLS_AES_256_GCM_SHA384 | TLSv1.3 | **yes** | **yes** | **modern** | +| TLS_CHACHA20_POLY1305_SHA256 | TLSv1.3 | no | no | **modern** | + +```{note} +If you enable TLSv1.3, openssl does always enable the three default cipher suites for TLSv1.3. +Therefore it is not necessary to add them to openssl based configurations. +``` + +(ingress-traffic)= + +## Ingress Traffic (wire-server) + +The list of TLS ciphers for incoming requests is limited by default to the [following](https://github.com/wireapp/wire-server/blob/master/charts/nginx-ingress-controller/values.yaml#L7) (for general server-certificates, both for federation and client API), and can be overridden on your installation if needed. + +## Egress Traffic (wire-server/federation) + +The list of TLS ciphers for outgoing federation requests is currently hardcoded, the list is [here](https://github.com/wireapp/wire-server/blob/master/services/federator/src/Federator/Remote.hs#L164-L180). + +## SFTD (ansible) + +The list of TLS ciphers for incoming SFT requests (and metrics) are defined in ansible templates [sftd.vhost.conf.j2](https://github.com/wireapp/ansible-sft/blob/develop/roles/sft-server/templates/sftd.vhost.conf.j2#L19) and [metrics.vhost.conf.j2](https://github.com/wireapp/ansible-sft/blob/develop/roles/sft-server/templates/metrics.vhost.conf.j2#L13). + +## SFTD (kubernetes) + +SFTD deployed via kubernetes uses `kubernetes.io/ingress` for ingress traffic, configured in [ingress.yaml](https://github.com/wireapp/wire-server/blob/develop/charts/sftd/templates/ingress.yaml). +Kubernetes based deployments make use of the settings from {ref}`ingress-traffic`. + +## Restund (ansible) + +The list of TLS ciphers for "TLS over TCP" TURN (and metrics) are defined in ansible templates [nginx-stream.conf.j2](https://github.com/wireapp/ansible-restund/blob/master/templates/nginx-stream.conf.j2#L25) and [nginx-metrics.conf.j2](https://github.com/wireapp/ansible-restund/blob/master/templates/nginx-metrics.conf.j2#L15). + +## Restund (kubernetes) + +[Kubernetes restund](https://github.com/wireapp/wire-server/tree/develop/charts/restund) deployment does not provide TLS connectivity. + +[bsi tr-02102-2]: https://www.bsi.bund.de/SharedDocs/Downloads/EN/BSI/Publications/TechGuidelines/TG02102/BSI-TR-02102-2.pdf +[mozilla tls guideline]: https://wiki.mozilla.org/Security/Server_Side_TLS diff --git a/docs/src/how-to/install/tls.rst b/docs/src/how-to/install/tls.rst deleted file mode 100644 index 8adac3d525..0000000000 --- a/docs/src/how-to/install/tls.rst +++ /dev/null @@ -1,60 +0,0 @@ -.. _tls: - -Configure TLS ciphers -======================= - -The following table lists recommended ciphers for TLS server setups, which should be used in wire deployments. - - -============================= ======= ============ ================= ======================== -Cipher Version Wire default `BSI TR-02102-2`_ `Mozilla TLS Guideline`_ -============================= ======= ============ ================= ======================== -ECDHE-ECDSA-AES128-GCM-SHA256 TLSv1.2 no **yes** intermediate -ECDHE-RSA-AES128-GCM-SHA256 TLSv1.2 no **yes** intermediate -ECDHE-ECDSA-AES256-GCM-SHA384 TLSv1.2 **yes** **yes** intermediate -ECDHE-RSA-AES256-GCM-SHA384 TLSv1.2 **yes** **yes** intermediate -ECDHE-ECDSA-CHACHA20-POLY1305 TLSv1.2 no no intermediate -ECDHE-RSA-CHACHA20-POLY1305 TLSv1.2 no no intermediate -TLS_AES_128_GCM_SHA256 TLSv1.3 **yes** **yes** **modern** -TLS_AES_256_GCM_SHA384 TLSv1.3 **yes** **yes** **modern** -TLS_CHACHA20_POLY1305_SHA256 TLSv1.3 no no **modern** -============================= ======= ============ ================= ======================== - - -.. _bsi tr-02102-2: https://www.bsi.bund.de/SharedDocs/Downloads/EN/BSI/Publications/TechGuidelines/TG02102/BSI-TR-02102-2.pdf -.. _mozilla tls guideline: https://wiki.mozilla.org/Security/Server_Side_TLS - -.. note:: - If you enable TLSv1.3, openssl does always enable the three default cipher suites for TLSv1.3. - Therefore it is not necessary to add them to openssl based configurations. - -.. _ingress traffic: - -Ingress Traffic (wire-server) ------------------------------ -The list of TLS ciphers for incoming requests is limited by default to the `following `_ (for general server-certificates, both for federation and client API), and can be overridden on your installation if needed. - - -Egress Traffic (wire-server/federation) ---------------------------------------- -The list of TLS ciphers for outgoing federation requests is currently hardcoded, the list is `here `_. - - -SFTD (ansible) --------------- -The list of TLS ciphers for incoming SFT requests (and metrics) are defined in ansible templates `sftd.vhost.conf.j2 `_ and `metrics.vhost.conf.j2 `_. - -SFTD (kubernetes) ------------------ -SFTD deployed via kubernetes uses ``kubernetes.io/ingress`` for ingress traffic, configured in `ingress.yaml `_. -Kubernetes based deployments make use of the settings from :ref:`ingress traffic`. - - -Restund (ansible) ------------------ - -The list of TLS ciphers for "TLS over TCP" TURN (and metrics) are defined in ansible templates `nginx-stream.conf.j2 `_ and `nginx-metrics.conf.j2 `_. - -Restund (kubernetes) --------------------- -`Kubernetes restund `_ deployment does not provide TLS connectivity. diff --git a/docs/src/how-to/install/troubleshooting.md b/docs/src/how-to/install/troubleshooting.md new file mode 100644 index 0000000000..7aa9f80479 --- /dev/null +++ b/docs/src/how-to/install/troubleshooting.md @@ -0,0 +1,265 @@ +# Troubleshooting during installation + +## Problems with CORS on the web based applications (webapp, team-settings, account-pages) + +If you have installed wire-server, but the web application page in your browser has connection problems and throws errors in the console such as `"Refused to connect to 'https://assets.example.com' because it violates the following Content Security Policies"`, make sure to check that you have configured the `CSP_EXTRA_` environment variables. + +In the file that you use as override when running `helm install/update -f ` (using the webapp as an example): + +```yaml +webapp: + # ... other settings... + envVars: + # ... other environment variables ... + CSP_EXTRA_CONNECT_SRC: "https://*.example.com, wss://*.example.com" + CSP_EXTRA_IMG_SRC: "https://*.example.com" + CSP_EXTRA_SCRIPT_SRC: "https://*.example.com" + CSP_EXTRA_DEFAULT_SRC: "https://*.example.com" + CSP_EXTRA_FONT_SRC: "https://*.example.com" + CSP_EXTRA_FRAME_SRC: "https://*.example.com" + CSP_EXTRA_MANIFEST_SRC: "https://*.example.com" + CSP_EXTRA_OBJECT_SRC: "https://*.example.com" + CSP_EXTRA_MEDIA_SRC: "https://*.example.com" + CSP_EXTRA_PREFETCH_SRC: "https://*.example.com" + CSP_EXTRA_STYLE_SRC: "https://*.example.com" + CSP_EXTRA_WORKER_SRC: "https://*.example.com" +``` + +For more info, you can have a look at respective charts values files, i.e.: + +> - [charts/account-pages/values.yaml](https://github.com/wireapp/wire-server/blob/develop/charts/account-pages/values.yaml) +> - [charts/team-settings/values.yaml](https://github.com/wireapp/wire-server/blob/develop/charts/team-settings/values.yaml) +> - [charts/webapp/values.yaml](https://github.com/wireapp/wire-server/blob/develop/charts/webapp/values.yaml) + +## Problems with ansible and python versions + +If for instance the following fails: + +``` +ansible all -i hosts.ini -m shell -a "echo hello" +``` + +If your target machine only has python 3 (not python 2.7), you can tell ansible to use python 3 by default, by specifying `ansible_python_interpreter`: + +```ini +# hosts.ini + +[all] +server1 ansible_host=1.2.3.4 + + +[all:vars] +ansible_python_interpreter=/usr/bin/python3 +``` + +(python 3 may not be supported by all ansible modules yet) + +## Flaky issues with Cassandra (failed QUORUMs, etc.) + +Cassandra is *very* picky about time! Ensure that NTP is properly set up on all nodes. Particularly for Cassandra *DO NOT* use anything else other than ntp. Here are some helpful blogs that explain why: + +> - +> - +> - + +How can I ensure that I have correctly setup NTP on my machine(s)? Have a look at [this ansible playbook](https://github.com/wireapp/wire-server-deploy/blob/develop/ansible/cassandra-verify-ntp.yml) + +## I deployed `demo-smtp` but I'm not receiving any verification emails + +1. Check whether brig deployed successfully (brig pod(s) should be in state *Running*) + + ``` + kubectl get pods -o wide + ``` + +2. Inspect Brig logs + + ``` + kubectl logs $BRING_POD_NAME + ``` + +3. The receiving email server might refuse to accept any email sent by the `demo-smtp` server, due to not being + a trusted origin. You may want to set up one of the following email verification mechanisms. + +- [SFP](https://en.wikipedia.org/wiki/Sender_Policy_Framework) +- [DKIM](https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail) +- [DMARC](https://en.wikipedia.org/wiki/DMARC) + +4. You may want to adjust the SMTP configuration for Brig (`wire-server/[values,secrets].yaml`). + +```yaml +brig: + config: + smtp: + host: 'demo-smtp' + port: 25 + connType: 'plain' +``` + +```yaml +brig: + secrets: + smtpPassword: dummyPassword +``` + +(Don't forget to apply the changes with `helm upgrade wire-server wire/wire-server -f values.yaml -f secrets.yaml`) + +## I deployed `demo-smtp` and I want to skip email configuration and retrieve verification codes directly + +If the only thing you need demo-smtp for is sending yourself verification codes to create a test account, it might be simpler and faster to just skip SMTP configuration, and simply retrieve the code internally right after it is sent, while it is in the outbound email queue. + +To do this, click create a user/account/team, or if you already have, click on `Resend Code`: + +```{figure} img/code-input.png +The code input interface +``` + +Then run the following command + +``` +kubectl exec $(kubectl get pod -lapp=demo-smtp | grep demo | awk '{print $1;}') -- sh -c 'cat /var/spool/exim4/input/* | grep -Po "^\\d{6}$" ' +``` + +Or step by step: + +1. Get the name of the pod + + ``` + kubectl get pod -lapp=demo-smtp + ``` + +Which will give you a result that looks something like this + +``` +> kubectl get pod -lapp=demo-smtp +NAME READY STATUS RESTARTS AGE +demo-smtp-85557f6877-qxk2p 1/1 Running 0 80m +``` + +In which case, the pod name is `demo-smtp-85557f6877-qxk2p`, which replaces \ in the next command. + +2. Then get the content of emails and extract the code + + ``` + kubectl exec -- sh -c 'head -n 15 /var/spool/exim4/input/* ' + ``` + +Which will give you the content of sent emails, including the code + +``` +> kubectl exec demo-smtp-85557f6877-qxk2p -- sh -c 'head -n 15 /var/spool/exim4/input/* ' +==> /var/spool/exim4/input/1mECxm-000068-28-D <== +1mECxm-000068-28-D +--Y3mymuwB5Y +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable +[https://wire=2Ecom/p/img/email/logo-email-black=2Epng] +VERIFY YOUR EMAIL +myemail@gmail=2Ecom was used to register on Wire=2E Enter this code to v= +erify your email and create your account=2E +022515 +``` + +This means the code is `022515`, simply enter it in the interface. + +If the email has already been sent out, it's possible the queue will be empty. + +If that is the case, simply click the "Resend Code" link in the interface, then quickly re-send the command, a new email should now be present. + +## Obtaining Brig logs, and the format of different team/user events + +To obtain brig logs, simply run + +``` +kubectl logs $(kubectl get pods | grep brig | awk '{print $1;}' | head -n 1) +``` + +You will get log entries for various different types of events that happen, for example: + +1. User creation + + ``` + {"user":"24bdd52e-af33-400c-8e47-d16bf8695dbd","request":"c0575ff5a2d61bfc2be21e77260fccab","msgs":["I","Creating user"]} + ``` + +2. Activation key creation + + ``` + {"activation.code":"949721","activation.key":"p8o032Ljqhjgcea9R0AAnOeiUniGm63BrY9q_aeS1Cc=","request":"c0575ff5a2d61bfc2be21e77260fccab","msgs":["I","Activating"]} + ``` + +3. Activation of a new user + + ``` + {"user":"24bdd52e-af33-400c-8e47-d16bf8695dbd","request":"c0575ff5a2d61bfc2be21e77260fccab","msgs":["I","User activated"]} + ``` + +4. User indexing + + ``` + {"user":"24bdd52e-af33-400c-8e47-d16bf8695dbd","logger":"index.brig","msgs":["I","Indexing user"]} + ``` + +5. Team creation + + ``` + {"email_sha256":"a7ca34df62e3aa18e071e6bd4740009ce7a25278869badc1ad8f6afda792d427","team":"6ef03a2b-34b5-4b65-8d72-1e4fc7697553","user":"24bdd52e-af33-400c-8e47-d16bf8695dbd","module":"Brig.API.Public","fn":"Brig.API.Public.createUser","request":"c0575ff5a2d61bfc2be21e77260fccab","msgs":["I","Sucessfully created user"]} + ``` + +6. Invitation sent + + ``` + {"invitation_code":"hJuh1C1PzMkgtesAYZZ4SZrP5xO-xM_m","email_sha256":"eef48a690436699c653110387455a4afe93ce29febc348acd20f6605787956e6","team":"6ef03a2b-34b5-4b65-8d72-1e4fc7697553","module":"Brig.Team.API","fn":"Brig.Team.API.createInvitationPublic","request":"c43440074629d802a199464dd892cd92","msgs":["I","Succesfully created invitation"]} + ``` + +## Diagnosing and addressing bad network/disconnect issues + +### Diagnosis + +If you are experiencing bad network/disconnection issues, here is how to obtain the cause from the client log files: + +In the Web client, the connection state handler logs the disconnected state as reported by WebRTC as: + +``` +flow(...): connection_handler: disconnected, starting disconnect timer +``` + +On mobile, the output in the log is slightly different: + +``` +pf(...): ice connection state: Disconnected +``` + +And when the timer expires and the connection is not re-established: + +``` +ecall(...): mf_restart_handler: triggering restart due to network drop +``` + +If the attempt to reconnect then fails you will likely see the following: + +``` +ecall(...): connection timeout after 10000 milliseconds +``` + +If the connection to the SFT ({ref}`understand-sft`) server is considered lost due to missing ping messages from a non-functionning or delayed data channel or a failure to receive/decrypt media you will see: + +``` +ccall(...): reconnect +``` + +Then followed by these values: + +``` +cp: received CONFPART message YES/NO +da: decrypt attempted YES/NO +ds: decrypt successful YES/NO +att: number of reconnect attempts +p: the expected ping (how many pings have not returned) +``` + +### Configuration + +Question: Are the connection values for bad networks/disconnect configurable on on-prem? + +Answer: The values are not currently configurable, they are built into the clients at compile time, we do have a mechanism for sending calling configs to the clients but these values are not currently there. diff --git a/docs/src/how-to/install/troubleshooting.rst b/docs/src/how-to/install/troubleshooting.rst deleted file mode 100644 index 79adc61f52..0000000000 --- a/docs/src/how-to/install/troubleshooting.rst +++ /dev/null @@ -1,255 +0,0 @@ -Troubleshooting during installation -------------------------------------- - -Problems with CORS on the web based applications (webapp, team-settings, account-pages) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you have installed wire-server, but the web application page in your browser has connection problems and throws errors in the console such as `"Refused to connect to 'https://assets.example.com' because it violates the following Content Security Policies"`, make sure to check that you have configured the ``CSP_EXTRA_`` environment variables. - -In the file that you use as override when running ``helm install/update -f `` (using the webapp as an example): - -.. code:: yaml - - webapp: - # ... other settings... - envVars: - # ... other environment variables ... - CSP_EXTRA_CONNECT_SRC: "https://*.example.com, wss://*.example.com" - CSP_EXTRA_IMG_SRC: "https://*.example.com" - CSP_EXTRA_SCRIPT_SRC: "https://*.example.com" - CSP_EXTRA_DEFAULT_SRC: "https://*.example.com" - CSP_EXTRA_FONT_SRC: "https://*.example.com" - CSP_EXTRA_FRAME_SRC: "https://*.example.com" - CSP_EXTRA_MANIFEST_SRC: "https://*.example.com" - CSP_EXTRA_OBJECT_SRC: "https://*.example.com" - CSP_EXTRA_MEDIA_SRC: "https://*.example.com" - CSP_EXTRA_PREFETCH_SRC: "https://*.example.com" - CSP_EXTRA_STYLE_SRC: "https://*.example.com" - CSP_EXTRA_WORKER_SRC: "https://*.example.com" - -For more info, you can have a look at respective charts values files, i.e.: - - * `charts/account-pages/values.yaml `__ - * `charts/team-settings/values.yaml `__ - * `charts/webapp/values.yaml `__ - -Problems with ansible and python versions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If for instance the following fails:: - - ansible all -i hosts.ini -m shell -a "echo hello" - -If your target machine only has python 3 (not python 2.7), you can tell ansible to use python 3 by default, by specifying `ansible_python_interpreter`: - -.. code:: ini - - # hosts.ini - - [all] - server1 ansible_host=1.2.3.4 - - - [all:vars] - ansible_python_interpreter=/usr/bin/python3 - -(python 3 may not be supported by all ansible modules yet) - - -Flaky issues with Cassandra (failed QUORUMs, etc.) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Cassandra is *very* picky about time! Ensure that NTP is properly set up on all nodes. Particularly for Cassandra *DO NOT* use anything else other than ntp. Here are some helpful blogs that explain why: - - * https://blog.rapid7.com/2014/03/14/synchronizing-clocks-in-a-cassandra-cluster-pt-1-the-problem/ - * https://blog.rapid7.com/2014/03/17/synchronizing-clocks-in-a-cassandra-cluster-pt-2-solutions/ - * https://www.digitalocean.com/community/tutorials/how-to-set-up-time-synchronization-on-ubuntu-16-04 - -How can I ensure that I have correctly setup NTP on my machine(s)? Have a look at `this ansible playbook `_ - - -I deployed ``demo-smtp`` but I'm not receiving any verification emails -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -1. Check whether brig deployed successfully (brig pod(s) should be in state *Running*) :: - - kubectl get pods -o wide - -2. Inspect Brig logs :: - - kubectl logs $BRING_POD_NAME - -3. The receiving email server might refuse to accept any email sent by the `demo-smtp` server, due to not being - a trusted origin. You may want to set up one of the following email verification mechanisms. - -* `SFP `__ -* `DKIM `__ -* `DMARC `__ - - -4. You may want to adjust the SMTP configuration for Brig (``wire-server/[values,secrets].yaml``). - -.. code:: yaml - - brig: - config: - smtp: - host: 'demo-smtp' - port: 25 - connType: 'plain' - - -.. code:: yaml - - brig: - secrets: - smtpPassword: dummyPassword - -(Don't forget to apply the changes with ``helm upgrade wire-server wire/wire-server -f values.yaml -f secrets.yaml``) - -I deployed ``demo-smtp`` and I want to skip email configuration and retrieve verification codes directly -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If the only thing you need demo-smtp for is sending yourself verification codes to create a test account, it might be simpler and faster to just skip SMTP configuration, and simply retrieve the code internally right after it is sent, while it is in the outbound email queue. - -To do this, click create a user/account/team, or if you already have, click on ``Resend Code``: - -.. figure:: img/code-input.png - - The code input interface - -Then run the following command :: - - kubectl exec $(kubectl get pod -lapp=demo-smtp | grep demo | awk '{print $1;}') -- sh -c 'cat /var/spool/exim4/input/* | grep -Po "^\\d{6}$" ' - -Or step by step: - -1. Get the name of the pod :: - - kubectl get pod -lapp=demo-smtp - -Which will give you a result that looks something like this :: - - > kubectl get pod -lapp=demo-smtp - NAME READY STATUS RESTARTS AGE - demo-smtp-85557f6877-qxk2p 1/1 Running 0 80m - -In which case, the pod name is ``demo-smtp-85557f6877-qxk2p``, which replaces in the next command. - -2. Then get the content of emails and extract the code :: - - kubectl exec -- sh -c 'head -n 15 /var/spool/exim4/input/* ' - -Which will give you the content of sent emails, including the code :: - - > kubectl exec demo-smtp-85557f6877-qxk2p -- sh -c 'head -n 15 /var/spool/exim4/input/* ' - ==> /var/spool/exim4/input/1mECxm-000068-28-D <== - 1mECxm-000068-28-D - --Y3mymuwB5Y - Content-Type: text/plain; charset=utf-8 - Content-Transfer-Encoding: quoted-printable - [https://wire=2Ecom/p/img/email/logo-email-black=2Epng] - VERIFY YOUR EMAIL - myemail@gmail=2Ecom was used to register on Wire=2E Enter this code to v= - erify your email and create your account=2E - 022515 - -This means the code is ``022515``, simply enter it in the interface. - -If the email has already been sent out, it's possible the queue will be empty. - -If that is the case, simply click the "Resend Code" link in the interface, then quickly re-send the command, a new email should now be present. - -Obtaining Brig logs, and the format of different team/user events -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To obtain brig logs, simply run :: - - kubectl logs $(kubectl get pods | grep brig | awk '{print $1;}' | head -n 1) - -You will get log entries for various different types of events that happen, for example: - -1. User creation :: - - {"user":"24bdd52e-af33-400c-8e47-d16bf8695dbd","request":"c0575ff5a2d61bfc2be21e77260fccab","msgs":["I","Creating user"]} - -2. Activation key creation ::  - - {"activation.code":"949721","activation.key":"p8o032Ljqhjgcea9R0AAnOeiUniGm63BrY9q_aeS1Cc=","request":"c0575ff5a2d61bfc2be21e77260fccab","msgs":["I","Activating"]} - -3. Activation of a new user :: - - {"user":"24bdd52e-af33-400c-8e47-d16bf8695dbd","request":"c0575ff5a2d61bfc2be21e77260fccab","msgs":["I","User activated"]} - -4. User indexing :: - - {"user":"24bdd52e-af33-400c-8e47-d16bf8695dbd","logger":"index.brig","msgs":["I","Indexing user"]} - -5. Team creation ::  - - {"email_sha256":"a7ca34df62e3aa18e071e6bd4740009ce7a25278869badc1ad8f6afda792d427","team":"6ef03a2b-34b5-4b65-8d72-1e4fc7697553","user":"24bdd52e-af33-400c-8e47-d16bf8695dbd","module":"Brig.API.Public","fn":"Brig.API.Public.createUser","request":"c0575ff5a2d61bfc2be21e77260fccab","msgs":["I","Sucessfully created user"]} - -6. Invitation sent :: - - {"invitation_code":"hJuh1C1PzMkgtesAYZZ4SZrP5xO-xM_m","email_sha256":"eef48a690436699c653110387455a4afe93ce29febc348acd20f6605787956e6","team":"6ef03a2b-34b5-4b65-8d72-1e4fc7697553","module":"Brig.Team.API","fn":"Brig.Team.API.createInvitationPublic","request":"c43440074629d802a199464dd892cd92","msgs":["I","Succesfully created invitation"]} - -Diagnosing and addressing bad network/disconnect issues -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Diagnosis -========= - -If you are experiencing bad network/disconnection issues, here is how to obtain the cause from the client log files: - -In the Web client, the connection state handler logs the disconnected state as reported by WebRTC as: - -.. code:: - - flow(...): connection_handler: disconnected, starting disconnect timer - -On mobile, the output in the log is slightly different: - -.. code:: - - pf(...): ice connection state: Disconnected - -And when the timer expires and the connection is not re-established: - -.. code:: - - ecall(...): mf_restart_handler: triggering restart due to network drop - -If the attempt to reconnect then fails you will likely see the following: - -.. code:: - - ecall(...): connection timeout after 10000 milliseconds - -If the connection to the SFT (:ref:`understand-sft`) server is considered lost due to missing ping messages from a non-functionning or delayed data channel or a failure to receive/decrypt media you will see: - -.. code:: - - ccall(...): reconnect - -Then followed by these values: - -.. code:: - - cp: received CONFPART message YES/NO - da: decrypt attempted YES/NO - ds: decrypt successful YES/NO - att: number of reconnect attempts - p: the expected ping (how many pings have not returned) - -Configuration -============= - -Question: Are the connection values for bad networks/disconnect configurable on on-prem? - -Answer: The values are not currently configurable, they are built into the clients at compile time, we do have a mechanism for sending calling configs to the clients but these values are not currently there. - - - - - - diff --git a/docs/src/how-to/install/version-requirements.md b/docs/src/how-to/install/version-requirements.md new file mode 100644 index 0000000000..dc9cccc8f9 --- /dev/null +++ b/docs/src/how-to/install/version-requirements.md @@ -0,0 +1,28 @@ +# Required/Supported versions + +*Updated: 26.04.2021* + +```{warning} +If you already installed Wire by using `poetry`, please refer to the +[old version](https://docs.wire.com/versions/install-with-poetry/how-to/index.html) of +the installation guide. +``` + +## Persistence + +- Cassandra: 3.11 (OpenJDK 8) +- Elasticsearch: 6.6.0 +- Minio + : - server: latest (tested v2020-03-25) + - client: latest (tested v2020-03-14) + +### Infrastructure + +- Ubuntu: 18.04 +- Docker: latest +- Kubernetes: 1.19.7 + +### Automation + +- Ansible: 2.9 +- Helm: >= v3 diff --git a/docs/src/how-to/install/version-requirements.rst b/docs/src/how-to/install/version-requirements.rst deleted file mode 100644 index 3c204404bb..0000000000 --- a/docs/src/how-to/install/version-requirements.rst +++ /dev/null @@ -1,35 +0,0 @@ -Required/Supported versions -=========================== - -*Updated: 26.04.2021* - -.. warning:: - - If you already installed Wire by using ``poetry``, please refer to the - `old version `__ of - the installation guide. - - -Persistence -~~~~~~~~~~~ - -- Cassandra: 3.11 (OpenJDK 8) -- Elasticsearch: 6.6.0 -- Minio - - server: latest (tested v2020-03-25) - - client: latest (tested v2020-03-14) - - -Infrastructure --------------- - -- Ubuntu: 18.04 -- Docker: latest -- Kubernetes: 1.19.7 - - -Automation ----------- - -- Ansible: 2.9 -- Helm: >= v3 diff --git a/docs/src/how-to/post-install/index.rst b/docs/src/how-to/post-install/index.md similarity index 53% rename from docs/src/how-to/post-install/index.rst rename to docs/src/how-to/post-install/index.md index 4a7420aa23..2dd2009af9 100644 --- a/docs/src/how-to/post-install/index.rst +++ b/docs/src/how-to/post-install/index.md @@ -1,15 +1,15 @@ -.. _checks: +(checks)= -Verifying your wire-server installation -======================================= +# Verifying your wire-server installation After a successful installation of wire-server and its components, there are some useful checks to be run to ensure the proper functioning of the system. Here's a non-exhaustive list of checks to run on the hosts: NOTE: This page is a work in progress, more sections to be added soon. -.. toctree:: - :maxdepth: 1 - :glob: +```{toctree} +:glob: true +:maxdepth: 1 - Verifying NTP - Verifying data retention for logs don't exceed 72 hours + Verifying NTP + Verifying data retention for logs don't exceed 72 hours +``` diff --git a/docs/src/how-to/post-install/logrotation-check.md b/docs/src/how-to/post-install/logrotation-check.md new file mode 100644 index 0000000000..17bcdde7db --- /dev/null +++ b/docs/src/how-to/post-install/logrotation-check.md @@ -0,0 +1,81 @@ +(logrotation-check)= + +# Logs and Data Protection checks + +On Wire.com, we keep logs for a maximum of 72 hours as described in the [privacy whitepaper](https://wire.com/en/security/) + +We recommend you do the same and limit the amount of logs kept on your servers. + +## How can I see how far in the past access logs are still available on my servers? + +Look at the timestamps of your earliest nginz logs: + +```sh +export NAMESPACE=default # this may be 'default' or 'wire' +kubectl -n "$NAMESPACE" get pods | grep nginz +# choose one of the resulting names, it might be named e.g. nginz-6d75755c5c-h9fwn +kubectl -n "$NAMESPACE" logs -c nginz | head -10 +``` + +If the timestamp is more than 3 days in the past, your logs are kept for unnecessary long amount of time and you should configure log rotation. + +### I used your ansible scripts and prefer to have the default 72 hour maximum log availability configured automatically. + +You can use [the kubernetes_logging.yml ansible playbook](https://github.com/wireapp/wire-server-deploy/blob/develop/ansible/kubernetes_logging.yml) + +### I am not using ansible and like to SSH into hosts and configure things manually + +SSH into one of your kubernetes worker machines. + +If you installed as per the instructions on docs.wire.com, then the default logging strategy is `json-file` with `--log-opt max-size=50m --log-opt max-file=5` storing logs in files under `/var/lib/docker/containers//.log`. You can check this with these commands: + +```sh +docker info --format '{{.LoggingDriver}}' +ps aux | grep log-opt +``` + +(Options configured in `/etc/systemd/system/docker.service.d/docker-options.conf`) + +The default will thus keep your logs around until reaching 250 MB per pod, which is far longer than three days. Since docker logs don't allow a time-based log rotation, we can instead make use of [logrotate](https://linux.die.net/man/8/logrotate) to rotate logs for us. + +Create the file `/etc/logrotate.d/podlogs` with the following contents: + +% NOTE: in case you change these docs, also make sure to update the actual code +% under https://github.com/wireapp/wire-server-deploy/blob/develop/ansible/kubernetes_logging.yml + +``` +"/var/lib/docker/containers/*/*.log" +{ + daily + missingok + rotate 2 + maxage 1 + copytruncate + nocreate + nocompress + } +``` + +Repeat the same for all the other kubernetes worker machines, the file needs to exist on all of them. + +There should already be a cron job for logrotate for other parts of the system, so this should be sufficent, you can stop here. + +You can check for the cron job with: + +``` +ls /etc/cron.daily/logrotate +``` + +And you can manually run a log rotation using: + +``` +/usr/sbin/logrotate -v /etc/logrotate.conf +``` + +If you want to clear out old logs entirely now, you can force log rotation three times (again, on all kubernetes machines): + +``` +/usr/sbin/logrotate -v -f /etc/logrotate.conf +/usr/sbin/logrotate -v -f /etc/logrotate.conf +/usr/sbin/logrotate -v -f /etc/logrotate.conf +``` diff --git a/docs/src/how-to/post-install/logrotation-check.rst b/docs/src/how-to/post-install/logrotation-check.rst deleted file mode 100644 index 6094d6d3a3..0000000000 --- a/docs/src/how-to/post-install/logrotation-check.rst +++ /dev/null @@ -1,79 +0,0 @@ -.. _logrotation-check: - -Logs and Data Protection checks -=============================== - -On Wire.com, we keep logs for a maximum of 72 hours as described in the `privacy whitepaper `_ - -We recommend you do the same and limit the amount of logs kept on your servers. - -How can I see how far in the past access logs are still available on my servers? --------------------------------------------------------------------------------- - -Look at the timestamps of your earliest nginz logs: - -.. code:: sh - - export NAMESPACE=default # this may be 'default' or 'wire' - kubectl -n "$NAMESPACE" get pods | grep nginz - # choose one of the resulting names, it might be named e.g. nginz-6d75755c5c-h9fwn - kubectl -n "$NAMESPACE" logs -c nginz | head -10 - -If the timestamp is more than 3 days in the past, your logs are kept for unnecessary long amount of time and you should configure log rotation. - -I used your ansible scripts and prefer to have the default 72 hour maximum log availability configured automatically. -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can use `the kubernetes_logging.yml ansible playbook `_ - -I am not using ansible and like to SSH into hosts and configure things manually -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -SSH into one of your kubernetes worker machines. - -If you installed as per the instructions on docs.wire.com, then the default logging strategy is ``json-file`` with ``--log-opt max-size=50m --log-opt max-file=5`` storing logs in files under ``/var/lib/docker/containers//.log``. You can check this with these commands: - -.. code:: sh - - docker info --format '{{.LoggingDriver}}' - ps aux | grep log-opt - -(Options configured in ``/etc/systemd/system/docker.service.d/docker-options.conf``) - -The default will thus keep your logs around until reaching 250 MB per pod, which is far longer than three days. Since docker logs don't allow a time-based log rotation, we can instead make use of `logrotate `__ to rotate logs for us. - -Create the file ``/etc/logrotate.d/podlogs`` with the following contents: - -.. - NOTE: in case you change these docs, also make sure to update the actual code - under https://github.com/wireapp/wire-server-deploy/blob/develop/ansible/kubernetes_logging.yml -.. code:: - - "/var/lib/docker/containers/*/*.log" - { - daily - missingok - rotate 2 - maxage 1 - copytruncate - nocreate - nocompress - } - -Repeat the same for all the other kubernetes worker machines, the file needs to exist on all of them. - -There should already be a cron job for logrotate for other parts of the system, so this should be sufficent, you can stop here. - -You can check for the cron job with:: - - ls /etc/cron.daily/logrotate - -And you can manually run a log rotation using:: - - /usr/sbin/logrotate -v /etc/logrotate.conf - -If you want to clear out old logs entirely now, you can force log rotation three times (again, on all kubernetes machines):: - - /usr/sbin/logrotate -v -f /etc/logrotate.conf - /usr/sbin/logrotate -v -f /etc/logrotate.conf - /usr/sbin/logrotate -v -f /etc/logrotate.conf diff --git a/docs/src/how-to/post-install/ntp-check.md b/docs/src/how-to/post-install/ntp-check.md new file mode 100644 index 0000000000..f093998eea --- /dev/null +++ b/docs/src/how-to/post-install/ntp-check.md @@ -0,0 +1,44 @@ +(ntp-check)= + +# NTP Checks + +Ensure that NTP is properly set up on all nodes. Particularly for Cassandra **DO NOT** use anything else other than ntp. Here are some helpful blogs that explain why: + +> - +> - + +## How can I see if NTP is correctly set up? + +This is an important part of your setup, particularly for your Cassandra nodes. You should use `ntpd` and our ansible scripts to ensure it is installed correctly - but you can still check it manually if you prefer. The following 2 sub-sections explain both approaches. + +### I used your ansible scripts and prefer to have automated checks + +Then the easiest way is to use [this ansible playbook](https://github.com/wireapp/wire-server-deploy/blob/develop/ansible/cassandra-verify-ntp.yml) + +### I am not using ansible and like to SSH into hosts and checking things manually + +The following shows how to check for existing servers connected to (assumes `ntpq` is installed) + +```sh +ntpq -pn +``` + +which should yield something like this: + +```sh + remote refid st t when poll reach delay offset jitter +============================================================================== + time.example. .POOL. 16 p - 64 0 0.000 0.000 0.000 ++ 2 u 498 512 377 0.759 0.039 0.081 +* 2 u 412 512 377 1.251 -0.670 0.063 +``` + +if your output shows \_ONLY\_ the entry with a `.POOL.` as `refid` and a lot of 0s, something is probably wrong, i.e.: + +```sh + remote refid st t when poll reach delay offset jitter +============================================================================== + time.example. .POOL. 16 p - 64 0 0.000 0.000 0.000 +``` + +What should you do if this is the case? Ensure that `ntp` is installed and that the servers in the pool (typically at `/etc/ntp.conf`) are reachable. diff --git a/docs/src/how-to/post-install/ntp-check.rst b/docs/src/how-to/post-install/ntp-check.rst deleted file mode 100644 index 09b3852e62..0000000000 --- a/docs/src/how-to/post-install/ntp-check.rst +++ /dev/null @@ -1,48 +0,0 @@ -.. _ntp-check: - -NTP Checks -========== - -Ensure that NTP is properly set up on all nodes. Particularly for Cassandra **DO NOT** use anything else other than ntp. Here are some helpful blogs that explain why: - - * https://blog.rapid7.com/2014/03/14/synchronizing-clocks-in-a-cassandra-cluster-pt-1-the-problem/ - * https://www.digitalocean.com/community/tutorials/how-to-set-up-time-synchronization-on-ubuntu-16-04 - -How can I see if NTP is correctly set up? ------------------------------------------ - -This is an important part of your setup, particularly for your Cassandra nodes. You should use `ntpd` and our ansible scripts to ensure it is installed correctly - but you can still check it manually if you prefer. The following 2 sub-sections explain both approaches. - -I used your ansible scripts and prefer to have automated checks -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Then the easiest way is to use `this ansible playbook `_ - -I am not using ansible and like to SSH into hosts and checking things manually -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The following shows how to check for existing servers connected to (assumes `ntpq` is installed) - -.. code:: sh - - ntpq -pn - -which should yield something like this: - -.. code:: sh - - remote refid st t when poll reach delay offset jitter - ============================================================================== - time.example. .POOL. 16 p - 64 0 0.000 0.000 0.000 - + 2 u 498 512 377 0.759 0.039 0.081 - * 2 u 412 512 377 1.251 -0.670 0.063 - -if your output shows _ONLY_ the entry with a `.POOL.` as `refid` and a lot of 0s, something is probably wrong, i.e.: - -.. code:: sh - - remote refid st t when poll reach delay offset jitter - ============================================================================== - time.example. .POOL. 16 p - 64 0 0.000 0.000 0.000 - -What should you do if this is the case? Ensure that `ntp` is installed and that the servers in the pool (typically at `/etc/ntp.conf`) are reachable. diff --git a/docs/src/how-to/single-sign-on/adfs/main.md b/docs/src/how-to/single-sign-on/adfs/main.md new file mode 100644 index 0000000000..2e48fd531b --- /dev/null +++ b/docs/src/how-to/single-sign-on/adfs/main.md @@ -0,0 +1,41 @@ +# How to set up SSO integration with ADFS + +This is being used in production by some of our customers, but not +documented. We do have a few out-of-context screenshots, which we +provide here in the hope they may help. + +```{image} fig-00.jpg +``` + +```{image} fig-01.jpg +``` + +```{image} fig-02.jpg +``` + +```{image} fig-03.jpg +``` + +```{image} fig-04.jpg +``` + +```{image} fig-05.jpg +``` + +```{image} fig-06.jpg +``` + +```{image} fig-07.jpg +``` + +```{image} fig-08.jpg +``` + +```{image} fig-09.jpg +``` + +```{image} fig-10.jpg +``` + +```{image} fig-11.jpg +``` diff --git a/docs/src/how-to/single-sign-on/adfs/main.rst b/docs/src/how-to/single-sign-on/adfs/main.rst deleted file mode 100644 index 53155b14d8..0000000000 --- a/docs/src/how-to/single-sign-on/adfs/main.rst +++ /dev/null @@ -1,19 +0,0 @@ -How to set up SSO integration with ADFS -======================================= - -This is being used in production by some of our customers, but not -documented. We do have a few out-of-context screenshots, which we -provide here in the hope they may help. - -.. image:: fig-00.jpg -.. image:: fig-01.jpg -.. image:: fig-02.jpg -.. image:: fig-03.jpg -.. image:: fig-04.jpg -.. image:: fig-05.jpg -.. image:: fig-06.jpg -.. image:: fig-07.jpg -.. image:: fig-08.jpg -.. image:: fig-09.jpg -.. image:: fig-10.jpg -.. image:: fig-11.jpg diff --git a/docs/src/how-to/single-sign-on/azure/main.md b/docs/src/how-to/single-sign-on/azure/main.md new file mode 100644 index 0000000000..dbd0338907 --- /dev/null +++ b/docs/src/how-to/single-sign-on/azure/main.md @@ -0,0 +1,92 @@ +# How to set up SSO integration with Microsoft Azure + +## Preprequisites + +- account, admin access to that account +- See also {ref}`sso-generic-setup`. + +## Steps + +### Azure setup + +Go to , and click on 'Azure Active Directory' +in the menu to your left, then on 'Enterprise Applications': + +```{image} 01.png +``` + +Click on 'New Application': + +```{image} 02.png +``` + +Select 'Non-gallery application': + +```{image} 03.png +``` + +Fill in user-facing app name, then click 'add': + +```{image} 04.png +``` + +The app is now created. If you get lost, you can always get back to +it by selecting its name from the enterprise applications list you've +already visited above. + +Click on 'Configure single sign-on'. + +```{image} 05.png +``` + +Select SAML: + +```{image} 06.png +``` + +On the next page, you find a link to a configuration guide which you +can consult if you have any azure-specific questions. Or you can go +straight to adding the two config parameters you need: + +```{image} 07.png +``` + +Enter for both identity and reply url. Save. + +```{image} 08.png +``` + +Click on 'test later': + +```{image} 09.png +``` + +Finally, you need to assign users to the newly created and configured application: + +```{image} 11.png +``` + +```{image} 12.png +``` + +```{image} 13.png +``` + +```{image} 14.png +``` + +```{image} 15.png +``` + +And that's it! You are now ready to set up your wire team for SAML SSO with the XML metadata file you downloaed above. + +## Further reading + +- technical concepts overview: + : - + - +- how to create an app: + : - +- how to configure SAML2.0 SSO: + : - + - diff --git a/docs/src/how-to/single-sign-on/azure/main.rst b/docs/src/how-to/single-sign-on/azure/main.rst deleted file mode 100644 index 02115a753f..0000000000 --- a/docs/src/how-to/single-sign-on/azure/main.rst +++ /dev/null @@ -1,82 +0,0 @@ -How to set up SSO integration with Microsoft Azure -================================================== - -Preprequisites --------------- - -- http://azure.microsoft.com account, admin access to that account -- See also :ref:`SSO generic setup`. - -Steps ------ - -Azure setup -^^^^^^^^^^^ - -Go to https://portal.azure.com/, and click on 'Azure Active Directory' -in the menu to your left, then on 'Enterprise Applications': - -.. image:: 01.png - -Click on 'New Application': - -.. image:: 02.png - -Select 'Non-gallery application': - -.. image:: 03.png - -Fill in user-facing app name, then click 'add': - -.. image:: 04.png - -The app is now created. If you get lost, you can always get back to -it by selecting its name from the enterprise applications list you've -already visited above. - -Click on 'Configure single sign-on'. - -.. image:: 05.png - -Select SAML: - -.. image:: 06.png - -On the next page, you find a link to a configuration guide which you -can consult if you have any azure-specific questions. Or you can go -straight to adding the two config parameters you need: - -.. image:: 07.png - -Enter https://prod-nginz-https.wire.com/sso/finalize-login for both identity and reply url. Save. - -.. image:: 08.png - -Click on 'test later': - -.. image:: 09.png - -Finally, you need to assign users to the newly created and configured application: - -.. image:: 11.png -.. image:: 12.png -.. image:: 13.png -.. image:: 14.png -.. image:: 15.png - -And that's it! You are now ready to set up your wire team for SAML SSO with the XML metadata file you downloaed above. - - -Further reading ---------------- - -- technical concepts overview: - - https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-saml-protocol-reference - - https://docs.microsoft.com/en-us/azure/active-directory/develop/single-sign-on-saml-protocol - -- how to create an app: - - https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app - -- how to configure SAML2.0 SSO: - - https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/what-is-single-sign-on#saml-sso - - https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/configure-single-sign-on-non-gallery-applications diff --git a/docs/src/how-to/single-sign-on/centrify/main.rst b/docs/src/how-to/single-sign-on/centrify/main.md similarity index 55% rename from docs/src/how-to/single-sign-on/centrify/main.rst rename to docs/src/how-to/single-sign-on/centrify/main.md index 12d6d530fd..ed88ba6668 100644 --- a/docs/src/how-to/single-sign-on/centrify/main.rst +++ b/docs/src/how-to/single-sign-on/centrify/main.md @@ -1,46 +1,48 @@ -How to set up SSO integration with Centrify -=========================================== +# How to set up SSO integration with Centrify -Preprequisites --------------- +## Preprequisites -- http://centrify.com account, admin access to that account -- See also :ref:`SSO generic setup`. +- account, admin access to that account +- See also {ref}`sso-generic-setup`. -Steps ------ +## Steps -Centrify setup -^^^^^^^^^^^^^^ +### Centrify setup - Log in into Centrify web interface - Navigate to "Web Apps" - Click "Add Web Apps" -.. image:: 001.png +```{image} 001.png +``` ----- +______________________________________________________________________ - Create a new custom SAML application -.. image:: 002.png +```{image} 002.png +``` ----- +______________________________________________________________________ - Confirm... -.. image:: 003.png +```{image} 003.png +``` ----- +______________________________________________________________________ - Wait a few moments until the UI has rendered the `Settings` tab of your newly created Web App. - Enter at least a name, plus any other information you want to keep about this new Web App. - Then click on `Save`. -.. image:: 004.png -.. image:: 005.png +```{image} 004.png +``` ----- +```{image} 005.png +``` + +______________________________________________________________________ - Move to the `Trust` tab. This is where the SP metadata (everything centrify wants to know about wire, or Service Provider) and the IdP metadata (everything wire needs to know about centrify, or Identity Provider) can be found. - Enter `https://prod-nginz-https.wire.com/sso/finalize-login` as the SP metadata url. @@ -48,25 +50,33 @@ Centrify setup - You can see the metadata appear in the form below the `Load` button. - Click on `Save`. -.. image:: 006.png +```{image} 006.png +``` ----- +______________________________________________________________________ - Scroll down the `Trust` tab until you find the button to download the IdP metadata. - Store it in a file (eg. `my-wire-idp.xml`). You will need this file to set up your wire team for SSO. -.. image:: 007.png +```{image} 007.png +``` ----- +______________________________________________________________________ - Move to the `Permissions` tab and add at least one user. -.. image:: 008.png -.. image:: 009.png -.. image:: 010.png +```{image} 008.png +``` + +```{image} 009.png +``` + +```{image} 010.png +``` ----- +______________________________________________________________________ - If you see the status `Deployed` in the header of the `Web App` setup page, your users are ready to login. -.. image:: 011.png +```{image} 011.png +``` diff --git a/docs/src/how-to/single-sign-on/generic-setup.md b/docs/src/how-to/single-sign-on/generic-setup.md new file mode 100644 index 0000000000..d455899cca --- /dev/null +++ b/docs/src/how-to/single-sign-on/generic-setup.md @@ -0,0 +1,37 @@ +(sso-generic-setup)= + +# How to set up SSO integration with your IdP + +## Preprequisites + +- An account with your SAML IdP, admin access to that account +- Wire team, admin access to that team +- If your team is hosted at wire.com: + : - Ask customer support to enable the SSO feature flag for you. +- If you are running your own on-prem instance: + : - for handling the feature flag, you can run your own [backoffice](https://github.com/wireapp/wire-server-deploy/tree/259cd2664a4e4d890be797217cc715499d72acfc/charts/backoffice) service. + - More simply, you can configure the galley service so that sso is always enabled (just put "enabled-by-default" [here](https://github.com/wireapp/wire-server-deploy/blob/a4a35b65b2312995729b0fc2a04461508cb12de7/values/wire-server/prod-values.example.yaml#L134)). + +## Setting up your IdP + +- The SP Metadata URL: +- The SSO Login URL: +- SP Entity ID (aka Request Issuer ID): + +How you need to use this information during setting up your IdP +depends on the vendor. Let us know if you run into any trouble! + +## Setting up your wire team + +See + +## Authentication + +The team settings will show you a login code from us that looks like +eg. + +\> `wire-959b5840-3e8a-11e9-adff-0fa5314b31c0` + +See +- +on how to use this to login on wire. diff --git a/docs/src/how-to/single-sign-on/generic-setup.rst b/docs/src/how-to/single-sign-on/generic-setup.rst deleted file mode 100644 index 79f4d9585a..0000000000 --- a/docs/src/how-to/single-sign-on/generic-setup.rst +++ /dev/null @@ -1,42 +0,0 @@ -.. _SSO generic setup: - -How to set up SSO integration with your IdP -=========================================== - -Preprequisites --------------- - -- An account with your SAML IdP, admin access to that account -- Wire team, admin access to that team -- If your team is hosted at wire.com: - - Ask customer support to enable the SSO feature flag for you. -- If you are running your own on-prem instance: - - for handling the feature flag, you can run your own `backoffice `_ service. - - More simply, you can configure the galley service so that sso is always enabled (just put "enabled-by-default" `here `_). - -Setting up your IdP -------------------- - -- The SP Metadata URL: https://prod-nginz-https.wire.com/sso/metadata -- The SSO Login URL: https://prod-nginz-https.wire.com/sso/finalize-login -- SP Entity ID (aka Request Issuer ID): https://prod-nginz-https.wire.com/sso/finalize-login - -How you need to use this information during setting up your IdP -depends on the vendor. Let us know if you run into any trouble! - -Setting up your wire team -------------------------- - -See https://support.wire.com/hc/en-us/articles/360001285638-Set-up-SSO-internally - -Authentication --------------- - -The team settings will show you a login code from us that looks like -eg. - -> `wire-959b5840-3e8a-11e9-adff-0fa5314b31c0` - -See -https://support.wire.com/hc/en-us/articles/360000954617-Pro-How-to-log-in-with-SSO- -on how to use this to login on wire. diff --git a/docs/src/how-to/single-sign-on/index.md b/docs/src/how-to/single-sign-on/index.md new file mode 100644 index 0000000000..2cdb939676 --- /dev/null +++ b/docs/src/how-to/single-sign-on/index.md @@ -0,0 +1,15 @@ +# Single Sign-On and User Provisioning + +```{toctree} +:caption: 'Contents:' +:glob: true +:maxdepth: 1 + +Single sign-on and user provisioning +Generic setup +SSO integration with ADFS +SSO integration with Azure +SSO integration with Centrify +SSO integration with Okta +* +``` diff --git a/docs/src/how-to/single-sign-on/okta/main.rst b/docs/src/how-to/single-sign-on/okta/main.md similarity index 73% rename from docs/src/how-to/single-sign-on/okta/main.rst rename to docs/src/how-to/single-sign-on/okta/main.md index faf3799db0..6fe285c55f 100644 --- a/docs/src/how-to/single-sign-on/okta/main.rst +++ b/docs/src/how-to/single-sign-on/okta/main.md @@ -1,53 +1,56 @@ -How to set up SSO integration with Okta -======================================= +(sso-int-with-okta)= -Preprequisites --------------- +# How to set up SSO integration with Okta -- http://okta.com/ account, admin access to that account -- See also :ref:`SSO generic setup`. +## Preprequisites -Steps ------ +- account, admin access to that account +- See also {ref}`sso-generic-setup`. -Okta setup -~~~~~~~~~~ +## Steps + +### Okta setup - Log in into Okta web interface - Open the admin console and switch to the "Classic UI" - Navigate to "Applications" - Click "Add application" -.. image:: 001-applications-screen.png +```{image} 001-applications-screen.png +``` ----- +______________________________________________________________________ - Create a new application -.. image:: 002-add-application.png +```{image} 002-add-application.png +``` ----- +______________________________________________________________________ - Choose `Web`, `SAML 2.0` -.. image:: 003-add-application-1.png +```{image} 003-add-application-1.png +``` ----- +______________________________________________________________________ - Pick a name for the application in "Step 1" and continue -.. image:: 004-add-application-step1.png +```{image} 004-add-application-step1.png +``` ----- +______________________________________________________________________ - Add the following parameters in "Step 2" and continue +```{eval-rst} +-----------------------------+------------------------------------------------------------------------------+ + Paramenter label | Value | +=============================+==============================================================================+ | Single Sign On URL | `https://prod-nginz-https.wire.com/sso/finalize-login` | +-----------------------------+------------------------------------------------------------------------------+ -| Use this for Recipient URL | checked ✅ | +| Use this for Recipient URL | checked | | and Destination URL | | +-----------------------------+------------------------------------------------------------------------------+ | Audience URI (SP Entity ID) | `https://prod-nginz-https.wire.com/sso/finalize-login` | @@ -56,34 +59,41 @@ Okta setup +-----------------------------+------------------------------------------------------------------------------+ | Application Username | `Email` (\*) | +-----------------------------+------------------------------------------------------------------------------+ +``` **(\*) Note**: The application username **must be** unique in your team, and should be immutable once assigned. If more than one user has the same value for the field that you select here, those two users will log in as a single user on Wire. And if the value were to change, users will be re-assigned to a new account at the next login. Usually, `email` is a safe choice but you should evaluate it for your case. -.. image:: 005-add-application-step2.png +```{image} 005-add-application-step2.png +``` ----- +______________________________________________________________________ - Give the following answer in "Step 3" and continue +```{eval-rst} +-----------------------------------+------------------------------------------------------------------------+ + Paramenter label | Value | +===================================+========================================================================+ | Are you a customer or a partner? | I'm an Okta customer | +-----------------------------------+------------------------------------------------------------------------+ +``` -.. image:: 006-add-application-step3.png +```{image} 006-add-application-step3.png +``` ----- +______________________________________________________________________ - The app has been created. Switch to the "Sign-On" tab - Find the "Identity Provider Metadata" link. Copy the link address (normally done by right-clicking on the link and selecting "Copy link location" or a similar item in the menu). - Store the link address somewhere for a future step. -.. image:: 007-application-sign-on.png +```{image} 007-application-sign-on.png +``` ----- +______________________________________________________________________ - Switch to the "Assignments" tab - Make sure that some users (or everyone) is assigned to the application. These are the users that will be allowed to log in to Wire using Single Sign On. Add the relevant users to the list with the "Assign" button. -.. image:: 008-assignment.png +```{image} 008-assignment.png +``` diff --git a/docs/src/how-to/single-sign-on/trouble-shooting.rst b/docs/src/how-to/single-sign-on/trouble-shooting.md similarity index 60% rename from docs/src/how-to/single-sign-on/trouble-shooting.rst rename to docs/src/how-to/single-sign-on/trouble-shooting.md index da0ca43210..cdc7e1204a 100644 --- a/docs/src/how-to/single-sign-on/trouble-shooting.rst +++ b/docs/src/how-to/single-sign-on/trouble-shooting.md @@ -1,32 +1,28 @@ -.. _trouble-shooting-faq: +(trouble-shooting-faq)= -Trouble shooting & FAQ -====================== +# Trouble shooting & FAQ -Reporting a problem with user provisioning or SSO authentication ----------------------------------------------------------------- +## Reporting a problem with user provisioning or SSO authentication In order for us to analyse and understand your problem, we need at least the following information up-front: - Have you followed the following instructions? - - :ref:`FAQ ` (This document) - - `Howtos `_ for supported vendors - - `General documentation on the setup flow `_ + : - {ref}`FAQ ` (This document) + - [Howtos](https://docs.wire.com/how-to/single-sign-on/index.html) for supported vendors + - [General documentation on the setup flow](https://support.wire.com/hc/en-us/articles/360001285718-Set-up-SSO-externally) - Vendor information (octa, azure, centrica, other (which one)?) - Team ID (looks like eg. `2e9a9c9c-6f83-11eb-a118-3342c6f16f4e`, can be found in team settings) - What do you expect to happen? - - eg.: "I enter login code, authenticate successfully against IdP, get redirected, and see the wire landing page." + : - eg.: "I enter login code, authenticate successfully against IdP, get redirected, and see the wire landing page." - What does happen instead? - - Screenshots + : - Screenshots - Copy the text into your report where applicable in addition to screenshots (for automatic processing). - eg.: "instead of being logged into wire, I see the following error page: ..." - Screenshots of the Configuration (both SAML and SCIM, as applicable), including, but not limited to: - - If you are using SAML: SAML IdP metadata file + : - If you are using SAML: SAML IdP metadata file - If you are using SCIM for provisioning: Which attributes in the User schema are mapped? How? - -Can I use the same SSO login code for multiple teams? ------------------------------------------------------ +## Can I use the same SSO login code for multiple teams? No, but there is a good reason for it and a work-around. @@ -45,28 +41,21 @@ still use the same user base for all teams. This has the extra advantage that a user can be part of two teams with the same credentials, which would be impossible even with the hypothetical fix. - -Can an existing user without IdP (or with a different IdP) be bound to a new IdP? ---------------------------------------------------------------------------------- +## Can an existing user without IdP (or with a different IdP) be bound to a new IdP? No. This is a feature we never fully implemented. Details / latest -updates: https://github.com/wireapp/wire-server/issues/1151 - - -Can the SSO feature be disabled for a team? -------------------------------------------- +updates: -No, this is `not implemented `_. +## Can the SSO feature be disabled for a team? +No, this is [not implemented](https://github.com/wireapp/wire-server/blob/7a97cb5a944ae593c729341b6f28dfa1dabc28e5/services/galley/src/Galley/API/Error.hs#L215). -Can you remove a SAML connection? ---------------------------------- +## Can you remove a SAML connection? It is not possible to delete a SAML connection in the Team Settings app, however it can be overwritten with a new connection. -It is possible do delete a SAML connection directly via the API endpoint ``DELETE /identity-providers/{id}``. However deleting a SAML connection also requires deleting all users that can log in with this SAML connection. To prevent accidental deletion of users this functionality is not available directly from Team Settings. +It is possible do delete a SAML connection directly via the API endpoint `DELETE /identity-providers/{id}`. However deleting a SAML connection also requires deleting all users that can log in with this SAML connection. To prevent accidental deletion of users this functionality is not available directly from Team Settings. -If you get an error when returning from your IdP ------------------------------------------------- +## If you get an error when returning from your IdP `Symptoms:` @@ -86,9 +75,7 @@ that contains a lot of machine-readable info. With all this information, please get in touch with our customer support. - -Do I need any firewall settings? --------------------------------- +## Do I need any firewall settings? No. @@ -96,9 +83,7 @@ There is nothing to be done here. There is no internet traffic between your SAML IdP and the wire service. All communication happens via the browser or app. - -Why does the team owner have to keep using password? ----------------------------------------------------- +## Why does the team owner have to keep using password? The user who creates the team cannot be authenticated via SSO. There is fundamentally no easy way around that: we need somebody to give us @@ -119,71 +104,66 @@ for IdP registration and upgrade of IdP-authenticated owners / admins. In practice, user A and some owner authenticated via IdP would then be controlled by the same person, probably. - -What should the SAML response look like? ----------------------------------------- +## What should the SAML response look like? Here is an example that works. Much of this beyond the subject's NameID is required by the SAML standard. If you can find a more minimal example that still works, we'd be love to take a look. -.. code:: xml - - - ... - - - - - - - - - - - - - ... - - - ... - - - ... - - - - - ... - - - - - - - https://prod-nginz-https.wire.com/sso/finalize-login - - - - - - urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport - - - - - -Why does the auth response not contain a reference to an auth request? (Also: can i use IdP-initiated login?) ------------------------------------------------------------------------------------------------------------------ +```xml + + ... + + + + + + + + + + + + + ... + + + ... + + + ... + + + + + ... + + + + + + + https://prod-nginz-https.wire.com/sso/finalize-login + + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + + + +``` + +## Why does the auth response not contain a reference to an auth request? (Also: can i use IdP-initiated login?) tl;dr: Wire only supports SP-initiated login, where the user selects the auth method from inside the app's login screen. It does not support IdP-initiated login, where the user enters the app from a list of applications in the IdP UI. -The full story -^^^^^^^^^^^^^^ +### The full story SAML authentication can be initiated by the IdP (eg., Okta or Azure), or by the SP (Wire). @@ -206,9 +186,7 @@ impersonate rogue accounts) hard that were otherwise quite feasible. Wire therefore only supports SP-initiated login. - -How are SAML2 assertion details used in wire? ---------------------------------------------- +## How are SAML2 assertion details used in wire? Wire only uses the SAML `NameID` from the assertion, plus the information whether authentication and authorization was successful. @@ -221,9 +199,7 @@ wire user display name a default value. (The user will be allowed to change that value later; changing it does NOT affect the authentication handshake between wire and the IdP.) - -How should I map user data to SCIM attributes when provisioning users via SCIM? -------------------------------------------------------------------------------- +## How should I map user data to SCIM attributes when provisioning users via SCIM? If you are provisioning users via SCIM, the following mapping is used in your wire team: @@ -238,17 +214,16 @@ in your wire team: 3. SCIM's `preferredLanguage` is mapped to wire's user locale settings when a locale is not defined for that user. It must consist of an - ISO 639-1 language code. + ISO 639-1 language code. 4. SCIM's `externalId`: - a. If SAML SSO is used, it is mapped on the SAML `NameID`. If it + 1. If SAML SSO is used, it is mapped on the SAML `NameID`. If it parses as an email, it will have format `email`, and you can choose to validate it during provisioning (by enabeling the feature flag for your team). Otherwise, the format will be `unspecified`. - - b. If email/password authentication is used, SCIM's `externalId` is + 2. If email/password authentication is used, SCIM's `externalId` is mapped on wire's email address, and provisioning works like in team settings with invitation emails. @@ -262,29 +237,24 @@ Also note that the account will be set to `"active": false` until the user has accepted the invitation and activated the account. Please contact customer support if this causes any issues. - -Can I distribute a URL to my users that contains the login code? ----------------------------------------------------------------- +## Can I distribute a URL to my users that contains the login code? Users may find it awkward to copy and paste the login code into the form. If they are using the webapp, an alternative is to give them the following URL (fill in the login code that you can find in your team settings): -.. code:: bash +```bash +https://wire-webapp-dev.zinfra.io/auth#sso/3c4f050a-f073-11eb-b4c9-931bceeed13e +``` - https://wire-webapp-dev.zinfra.io/auth#sso/3c4f050a-f073-11eb-b4c9-931bceeed13e - - -(Theoretical) name clashes in SAML NameIDs ------------------------------------------- +## (Theoretical) name clashes in SAML NameIDs You can technically configure your SAML IdP to create name clashes in wire, ie., to map two (technically) different NameIDs to the same wire user. -How to know you're safe -^^^^^^^^^^^^^^^^^^^^^^^ +### How to know you're safe This is highly unlikely, since the distinguishing parts of `NameID` that we ignore are generally either @@ -292,16 +262,14 @@ unused or redundant. If you are confident that any two users you have assigned to the wire app can be distinguished solely by the lower-cased `NameID` content, you're safe. -Impact -^^^^^^ +### Impact If you are using SCIM for user provisioning, this may lead to errors during provisioning of new users ("user already exists"). If you use SAML auto-provisioning, this may lead to unintential account sharing instead of an error. -How to reproduce -^^^^^^^^^^^^^^^^ +### How to reproduce If you have users whose combination of `IssuerId` and `NameID` can only be distinguished by casing (upper @@ -309,30 +277,27 @@ vs. lower) or by the `NameID` qualifiers (`NameID` xml attributes `NameQualifier`, `IdPNameQualifier`, ...), those users will name clash. -Solution -^^^^^^^^ +### Solution Do not rely on case sensitivity of `IssuerID` or `NameID`, or on `NameID` qualifiers for distinguishing user identifiers. - -How to report problems ----------------------- +## How to report problems If you have a problem you cannot resolve by yourself, please get in touch. Add as much of the following details to your report as possible: -* Are you on cloud or on-prem? (If on-prem: which instance?) -* XML IdP metadata -* SSL Login code or IdP Issuer EntityID -* NameID of the account that has the problem -* SP metadata +- Are you on cloud or on-prem? (If on-prem: which instance?) +- XML IdP metadata +- SSL Login code or IdP Issuer EntityID +- NameID of the account that has the problem +- SP metadata Problem description, including, but not limited to: -* what happened? -* what did you want to happen? -* what does your idp config in the wire team management app look like? -* what does your wire config in your IdP management app look like? -* Please include screenshots *and* copied text (for cut&paste when we investigate) *and* further description and comments where feasible. +- what happened? +- what did you want to happen? +- what does your idp config in the wire team management app look like? +- what does your wire config in your IdP management app look like? +- Please include screenshots *and* copied text (for cut&paste when we investigate) *and* further description and comments where feasible. (If you can't produce some of this information of course please get in touch anyway! It'll merely be harder for us to resolve your issue quickly, and we may need to make a few extra rounds of data gathering together with you.) diff --git a/docs/src/understand/single-sign-on/Wire_SAML_Flow (lucidchart).svg b/docs/src/how-to/single-sign-on/understand/Wire_SAML_Flow (lucidchart).svg similarity index 100% rename from docs/src/understand/single-sign-on/Wire_SAML_Flow (lucidchart).svg rename to docs/src/how-to/single-sign-on/understand/Wire_SAML_Flow (lucidchart).svg diff --git a/docs/src/understand/single-sign-on/Wire_SAML_Flow.png b/docs/src/how-to/single-sign-on/understand/Wire_SAML_Flow.png similarity index 100% rename from docs/src/understand/single-sign-on/Wire_SAML_Flow.png rename to docs/src/how-to/single-sign-on/understand/Wire_SAML_Flow.png diff --git a/docs/src/how-to/single-sign-on/understand/main.md b/docs/src/how-to/single-sign-on/understand/main.md new file mode 100644 index 0000000000..465ef9dc48 --- /dev/null +++ b/docs/src/how-to/single-sign-on/understand/main.md @@ -0,0 +1,561 @@ +# Single sign-on and user provisioning + +```{contents} +``` + +## Introduction + +This page is intended as a manual for administrator users in need of setting up {term}`SSO` and provisionning users using {term}`SCIM` on their installation of Wire. + +Historically and by default, Wire's user authentication method is via phone or password. This has security implications and does not scale. + +Solution: {term}`SSO` with {term}`SAML`! [(Security Assertion Markup Language)](https://en.wikipedia.org/wiki/Security_Assertion_Markup_Language) + +{term}`SSO` systems allow users to identify on multiple systems (including Wire once configured as such) using a single ID and password. + +You can find some of the advantages of {term}`SSO` over more traditional schemes [here](https://en.wikipedia.org/wiki/Single_sign-on). + +Also historically, wire has allowed team admins and owners to manage their users in the team management app. + +This does not scale as it requires a lot of manual labor for each user. + +The solution we offer to solve this issue is implementing {term}`SCIM` [(System for Cross-domain Identity Management)](https://en.wikipedia.org/wiki/System_for_Cross-domain_Identity_Management) + +{term}`SCIM` is an interface that allows both software (for example Active Directory) and custom scripts to manage Identities (users) in bulk. + +This page explains how to set up {term}`SCIM` and then use it. + +```{note} +Note that it is recommended to use both {term}`SSO` and {term}`SCIM` (as opposed to just {term}`SSO` alone). +The reason is if you only use {term}`SSO`, but do not configure/implement {term}`SCIM`, you will experience reduced functionality. +In particular, without {term}`SCIM` all Wire users will be named according their e-mail address and won't have any rich profiles. +See below in the {term}`SCIM` section for a more detailled explanation. +``` + +## Further reading + +If you can't find the answers to your questions here, we have a few +more documents. Some of them are very technical, some may not be up +to date any more, and we are planning to move many of them into this +page. But for now they may be worth checking out. + +- {ref}`Trouble shooting & FAQ ` +- +- +- + +## Definitions + +The following concepts need to be understood to use the present manual: + +```{eval-rst} +.. glossary:: + + SCIM + System for Cross-domain Identity Management (:term:`SCIM`) is a standard for automating the exchange of user identity information between identity domains, or IT systems. + + One example might be that as a company onboards new employees and separates from existing employees, they are added and removed from the company's electronic employee directory. :term:`SCIM` could be used to automatically add/delete (or, provision/de-provision) accounts for those users in external systems such as G Suite, Office 365, or Salesforce.com. Then, a new user account would exist in the external systems for each new employee, and the user accounts for former employees might no longer exist in those systems. + + See: `System for Cross-domain Identity Management at Wikipedia `_ + + In the context of Wire, SCIM is the interface offered by the Wire service (in particular the spar service) that allows for single or mass automated addition/removal of user accounts. + + SSO + + Single sign-on (:term:`SSO`) is an authentication scheme that allows a user to log in with a single ID and password to any of several organizationally related, yet independent, software systems. + + True single sign-on allows the user to log in once and access different, independent services without re-entering authentication factors. + + See: `Single-Sign-On at Wikipedia `_ + + SAML + + Security Assertion Markup Language (:term:`SAML`, pronounced SAM-el, /'sæməl/) is an open standard for exchanging authentication and authorization data between parties, in particular, between an identity provider and a service provider. :term:`SAML` is an XML-based markup language for security assertions (statements that service providers use to make access-control decisions). :term:`SAML` is also: + + * A set of XML-based protocol messages + * A set of protocol message bindings + * A set of profiles (utilizing all of the above) + + An important use case that :term:`SAML` addresses is web-browser `single sign-on (SSO) `_ . Single sign-on is relatively easy to accomplish within a security domain (using cookies, for example) but extending :term:`SSO` across security domains is more difficult and resulted in the proliferation of non-interoperable proprietary technologies. The `SAML Web Browser SSO `_ profile was specified and standardized to promote interoperability. + + See: `SAML at Wikipedia `_ + + In the context of Wire, SAML is the standard/protocol used by the Wire services (in particular the spar service) to provide the Single Sign On feature. + + IdP + + In the context of Wire, an identity provider (abbreviated :term:`IdP`) is a service that provides SAML single sign-on (:term:`SSO`) credentials that give users access to Wire. + + Curl + + :term:`Curl` (pronounced ":term:`Curl`") is a command line tool used to download files over the HTTP (web) protocol. For example, `curl http://wire.com` will download the ``wire.com`` web page. + + In this manual, it is used to contact API (Application Programming Interface) endpoints manually, where those endpoints would normally be accessed by code or other software. + + This can be used either for illustrative purposes (to "show" how the endpoints can be used) or to allow the manual execution of some simple tasks. + + For example (not a real endpoint) `curl http://api.wire.com/delete_user/thomas` would (schematically) execute the :term:`Curl` command, which would contact the wire.com API and delete the user named "thomas". + + Running this command in a terminal would cause the :term:`Curl` command to access this URL, and the API at that URL would execute the requested action. + + See: `curl at Wikipedia `__ + + + Spar + + The Wire backend software stack is composed of different services, `running as pods <../overview.html#focus-on-pods>`__ in a kubernetes cluster. + + One of those pods is the "spar" service. That service/pod is dedicated to the providing :term:`SSO` (using :term:`SAML`) and :term:`SCIM` services. This page is the manual for this service. + + In the context of :term:`SCIM`, Wire's spar service is the `Service Provider `__ that Identity Management Software + (for example Azure, Okta, Ping Identity, SailPoint, Technology Nexus, etc.) uses for user account provisioning and deprovisioning. +``` + +## User login for the first time with SSO + +{term}`SSO` allows users to register and log into Wire with their company credentials that they use on other software in their workplace. +No need to remember another password. + +When a team is set up on Wire, the administrators can provide users a login code or link that they can use to go straight to their company's login page. + +Here is what this looks from a user's perspective: + +1. Download Wire. +2. Select and copy the code that your company gave you / the administrator generated +3. Open Wire. Wire may detect the code on your clipboard and open a pop-up window with a text field. + Wire will automatically put the code into the text field. + If so, click Log in and go to step 8. +4. If no pop-up: click Login on the first screen. +5. Click Enterprise Login. +6. A pop-up will appear. In the text field, paste or type the code your company gave you. +7. Click Log in. +8. Wire will load your company's login page: log in with your company credentials. + +(saml-sso)= + +## SAML/SSO + +### Introduction + +SSO (Single Sign-On) is technology allowing users to sign into multiple services with a single identity provider/credential. + +SSO is about `authentication`, not `provisioning` (create, update, remove user accounts). To learn more about the latter, continue {ref}`below `. + +For example, if a company already has SSO setup for some of their services, and they start using Wire, they can use Wire's SSO support to add Wire to the set of services their users will be able to sign into with their existing SSO credentials. + +Here is a blog post we like about how SAML works: + +And here is a diagram that explains it in slightly more technical terms: + +```{image} Wire_SAML_Flow.png +``` + +Here is a critique of XML/DSig security (which SAML relies on): + +### Terminology and concepts + +- End + The browser carrries out all the redirections from the SP to the IdP and vice versa. +- Service Provider (SP): The entity (here Wire software) that provides its protected resource when an end user tries to access this resource. To accomplish the SAML based SSO authentication, the Service Provider + must have the Identity Provider's metadata. +- Identity Provider (IdP): Defines the entity that provides the user identities, including the ability to authenticate a user to get access to a protected resource / application from a Service Provider. To accomplish + the SAML based SSO authentication, the IdP must have the Service Provider's metadata. +- SAML Request: This is the authentication request generated by the Service Provider to request an authentication from the Identity Provider for verifying the user's identity. +- SAML Response: The SAML Response contains the cryptographically signed assertion of the authenticated user and is generated by the Identity Provider. + +(Definitons adapted from [collab.net](http://help.collab.net/index.jsp?topic=/teamforge178/action/saml.html)) + +(setting-up-sso-externally)= + +### Setting up SSO externally + +To set up {term}`SSO` for a given Wire installation, the Team owner/administrator must enable it. + +The first step is to configure the Identity Provider: you'll need to register Wire as a service provider in your Identity Provider. + +We've put together guides for registering with different providers: + +- Instructions for {ref}`Okta ` +- Instructions for {doc}`Centrify <../centrify/main>` +- Instructions for {doc}`Azure <../azure/main>` +- Some screenshots for {doc}`ADFS <../adfs/main>` +- {doc}`Generic instructions (try this if none of the above are applicable) <../generic-setup>` + +As you do this, make sure you take note of your {term}`IdP` metadata, which you will need for the next step. + +Once you are finished with registering Wire to your {term}`IdP`, move on to the next step, setting up {term}`SSO` internally. + +### Setting up SSO internally + +Now that you've registered Wire with your identity provider ({term}`IdP`), you can enable {term}`SSO` for your team on Wire. + +On Desktop: + +- Click Settings and click "Manage Team"; or go directly to teams.wire.com, or if you have an on-premise install, go to teams.\.com +- Login with your account credentials. +- Click "Customization". Here you will see the section for {term}`SSO`. +- Click the blue down arrow. +- Click "Add {term}`SAML` Connection". +- Provide the {term}`IdP` metadata. To find out more about retrieving this for your provider, see the guides in the "Setting up {term}`SSO` externally" step just above. +- Click "Save". +- Wire will now validate the document to set up the {term}`SAML` connection. +- If the data is valid, you will return to the Settings page. +- The page shows the information you need to log in with {term}`SSO`. Copy the login code or URL and send it to your team members or partners. For more information see: Logging in with {term}`SSO`. + +What to expect after {term}`SSO` is enabled: + +Anyone with a login through your {term}`SAML` identity provider ({term}`IdP`) and with access to the Wire app will be able to register and log in to your team using the {term}`SSO` Login URL and/or Code. + +Take care to share the code only with members of your team. + +If you haven't set up {term}`SCIM` ([we recommend you do](#introduction)), your team members can create accounts on Wire using {term}`SSO` simply by logging in, and will appear on the People tab of the team management page. + +If team members already have Wire accounts, use {term}`SCIM` to associate them with the {term}`SAML` credentials. If you make a mistake here, you may end up with several accounts for the same person. + +(user-provisioning-scim-ldap)= + +## User provisioning (SCIM/LDAP) + +SCIM/LDAP is about `provisioning` (create, update, remove user accounts), not `authentication`. To learn more about the latter, continue {ref}`above `. + +Wire supports the [SCIM](http://www.simplecloud.info/) ([RFC 7643](https://tools.ietf.org/html/rfc7643)) protocol to create, update and delete users. + +If your user data is stored in an LDAP data source like Active Directory or OpenLDAP, you can use our docker-base [ldap-scim-bridge](https://github.com/wireapp/ldap-scim-bridge/#use-via-docker) to connect it to wire. + +Note that connecting a SCIM client to Wire also disables the functionality to create new users in the SSO login process. This functionality is disabled when a token is created (see below) and re-enabled when all tokens have been deleted. + +To set up the connection of your SCIM client (e.g. Azure Active Directory) you need to provide + +1. The URL under which Wire's SCIM API is hosted: `https://prod-nginz-https.wire.com/scim/v2`. + If you are hosting your own instance of Wire then the URL is `https:///scim/v2`, where `` is where you are serving Wire's public endpoints. Some SCIM clients append `/v2` to the URL your provide. If this happens (check the URL mentioned in error messages of your SCIM client) then please provide the URL without the `/v2` suffix, i.e. `https://prod-nginz-https.wire.com/scim` or `https:///scim`. +2. A secret token which authorizes the use of the SCIM API. Use the [wire_scim_token.py](https://raw.githubusercontent.com/wireapp/wire-server/654b62e3be74d9dddae479178990ebbd4bc77b1e/docs/reference/provisioning/wire_scim_token.py) + script to generate a token. To run the script you need access to an user account with "admin" privileges that can login via email and password. Note that the token is independent from the admin account that created it, i.e. the token remains valid if the admin account gets deleted or changed. + +You need to configure your SCIM client to use the following mandatory SCIM attributes: + +1. Set the `userName` attribute to the desired user handle (the handle is shown + with an @ prefix in apps). It must be unique accross the entire Wire Cloud + (or unique on your own instance), and consist of the characters `a-z0-9_.-` + (no capital letters). + +2. Set the `displayName` attribute to the user's desired display name, e.g. "Jane Doe". + It must consist of 1-128 unicode characters. It does not need to be unique. + +3. The `externalId` attribute: + + 1. If you are using Wire's SAML SSO feature then set `externalId` attribute to the same identifier used for `NameID` in your SAML configuration. + 2. If you are using email/password authentication then set the `externalId` + attribute to the user's email address. The user will receive an invitation email during provisioning. Also note that the account will be set to `"active": false` until the user has accepted the invitation and activated the account. + +You can optionally make use of Wire's `urn:wire:scim:schemas:profile:1.0` extension field to store arbitrary user profile data that is shown in the users profile, e.g. department, role. See [docs](https://github.com/wireapp/wire-server/blob/develop/docs/reference/user/rich-info.md#scim-support-refrichinfoscim) for details. + +### SCIM management in Wire (in Team Management) + +#### SCIM security and authentication + +Wire uses a very basic variant of oauth, where a *bearer token* is presented to the server in header with all {term}`SCIM` requests. + +You can create such bearer tokens in team management and copy them from there into your the dashboard of your SCIM data source. + +#### Generating a SCIM token + +In order to be able to send SCIM requests to Wire, we first need to generate a SCIM token. This section explains how to do this. + +Once the token is generated, it should be noted/remembered, and it will be used in all subsequent SCIM uses/requests to authenticate the request as valid/authenticated. + +These are the steps to generate a new {term}`SCIM` token, which you will need to provide to your identity provider ({term}`IdP`), along with the target API URL, to enable {term}`SCIM` provisionning. + +- Step 1: Go to (Here replace "wire.com" with your own domain if you have an on-premise installation of Wire). + +```{image} token-step-01.png +:align: center +``` + +- Step 2: In the left menu, go to "Customization". + +```{image} token-step-02.png +:align: center +``` + +- Step 3: Go to "Automated User Management ({term}`SCIM`)" and click the "down" to expand + +```{image} token-step-03.png +:align: center +``` + +- Step 4: Click "Generate token", if your password is requested, enter it. + +```{image} token-step-04.png +:align: center +``` + +- Step 5: Once the token is generated, copy it into your clipboard and store it somewhere safe (eg., in the dashboard of your SCIM data source). + +```{image} token-step-05.png +:align: center +``` + +- Step 6: You're done! You can now view token information, delete the token, or create more tokens should you need them. + +```{image} token-step-06.png +:align: center +``` + +Tokens are now listed in this {term}`SCIM`-related area of the screen, you can generate up to 8 such tokens. + +### Using SCIM via Curl + +You can use the term:`Curl` command line HTTP tool to access tho wire backend (in particular the `spar` service) through the {term}`SCIM` API. + +This can be helpful to write your own tooling to interface with wire. + +#### Creating a SCIM token + +Before we can send commands to the {term}`SCIM` API/Spar service, we need to be authenticated. This is done through the creation of a {term}`SCIM` token. + +First, we need a little shell environment. Run the following in your terminal/shell: + +```{code-block} bash +:linenos: true + + export WIRE_BACKEND=https://prod-nginz-https.wire.com + export WIRE_ADMIN=... + export WIRE_PASSWD=... +``` + +Wire's SCIM API currently supports a variant of HTTP basic auth. + +In order to create a token in your team, you need to authenticate using your team admin credentials. + +The way this works behind the scenes in your browser or cell phone, and in plain sight if you want to use curl, is you need to get a Wire token. + +First install the `jq` command (): + +```bash +sudo apt install jq +``` + +```{note} +If you don't want to install `jq`, you can just call the `curl` command and copy the access token into the shell variable manually. +``` + +Then run: + +```{code-block} bash +:linenos: true + +export BEARER=$(curl -X POST \ +--header 'Content-Type: application/json' \ +--header 'Accept: application/json' \ +-d '{"email":"'"$WIRE_ADMIN"'","password":"'"$WIRE_PASSWD"'"}' \ +$WIRE_BACKEND/login'?persist=false' | jq -r .access_token) +``` + +This token will be good for 15 minutes; after that, just repeat the command above to get a new token. + +```{note} +SCIM requests are authenticated with a SCIM token, see below. SCIM tokens and Wire tokens are different things. + +A Wire token is necessary to get a SCIM token. SCIM tokens do not expire, but need to be deleted explicitly. +``` + +You can test that you are logged in with the following command: + +```bash +curl -X GET --header "Authorization: Bearer $BEARER" $WIRE_BACKEND/self +``` + +Now you are ready to create a SCIM token: + +```{code-block} bash +:linenos: true + +export SCIM_TOKEN_FULL=$(curl -X POST \ +--header "Authorization: Bearer $BEARER" \ +--header 'Content-Type: application/json;charset=utf-8' \ +-d '{ "description": "test '"`date`"'", "password": "'"$WIRE_PASSWD"'" }' \ +$WIRE_BACKEND/scim/auth-tokens) +export SCIM_TOKEN=$(echo $SCIM_TOKEN_FULL | jq -r .token) +export SCIM_TOKEN_ID=$(echo $SCIM_TOKEN_FULL | jq -r .info.id) +``` + +The SCIM token is now contained in the `SCIM_TOKEN` environment variable. + +You can look it up again with: + +```{code-block} bash +:linenos: true + +curl -X GET --header "Authorization: Bearer $BEARER" \ +$WIRE_BACKEND/scim/auth-tokens +``` + +And you can delete it with: + +```{code-block} bash +:linenos: true + +curl -X DELETE --header "Authorization: Bearer $BEARER" \ +$WIRE_BACKEND/scim/auth-tokens?id=$SCIM_TOKEN_ID +``` + +#### Using a SCIM token to Create Read Update and Delete (CRUD) users + +Now that you have your SCIM token, you can use it to talk to the SCIM API to manipulate (create, read, update, delete) users, either individually or in bulk. + +**JSON encoding of SCIM Users** + +In order to manipulate users using commands, you need to specify user data. + +A minimal definition of a user is written in JSON format and looks like this: + +```{code-block} json +:linenos: true + +{ + "schemas" : ["urn:ietf:params:scim:schemas:core:2.0:User"], + "externalId" : "nick@example.com", + "userName" : "nick", + "displayName" : "The Nick" +} +``` + +You can store it in a variable using this sort of command: + +```{code-block} bash +:linenos: true + +export SCIM_USER='{ + "schemas" : ["urn:ietf:params:scim:schemas:core:2.0:User"], + "externalId" : "nick@example.com", + "userName" : "nick", + "displayName" : "The Nick" +}' +``` + +The `externalId` is used to construct a SAML identity. Two cases are +currently supported: + +1. `externalId` contains a valid email address. + The SAML `NameID` has the form `me@example.com`. +2. `externalId` contains anything that is *not* an email address. + The SAML `NameID` has the form `...`. + +```{note} +It is important to configure your SAML provider to use `nameid-format:emailAddress` or `nameid-format:unspecified`. Other nameid formats are not supported at this moment. + +See [FAQ](https://docs.wire.com/how-to/single-sign-on/trouble-shooting.html#how-should-i-map-user-data-to-scim-attributes-when-provisioning-users-via-scim) +``` + +We also support custom fields that are used in rich profiles in this form (see: ): + +```{code-block} bash +:linenos: true + + export SCIM_USER='{ + "schemas" : ["urn:ietf:params:scim:schemas:core:2.0:User", "urn:wire:scim:schemas:profile:1.0"], + "externalId" : "rnick@example.com", + "userName" : "rnick", + "displayName" : "The Rich Nick", + "urn:wire:scim:schemas:profile:1.0": { + "richInfo": [ + { + "type": "Department", + "value": "Sales & Marketing" + }, + { + "type": "Favorite color", + "value": "Blue" + } + ] + } + }' +``` + +**How to create a user** + +You can create a user using the following command: + +```{code-block} bash +:linenos: true + + export STORED_USER=$(curl -X POST \ + --header "Authorization: Bearer $SCIM_TOKEN" \ + --header 'Content-Type: application/json;charset=utf-8' \ + -d "$SCIM_USER" \ + $WIRE_BACKEND/scim/v2/Users) + export STORED_USER_ID=$(echo $STORED_USER | jq -r .id) +``` + +Note that `$SCIM_USER` is in the JSON format and is declared before running this commend as described in the section above. + +**Get a specific user** + +```{code-block} bash +:linenos: true + + curl -X GET \ + --header "Authorization: Bearer $SCIM_TOKEN" \ + --header 'Content-Type: application/json;charset=utf-8' \ + $WIRE_BACKEND/scim/v2/Users/$STORED_USER_ID +``` + +**Search a specific user** + +SCIM user search is quite flexible. Wire currently only supports lookup by wire handle or email address. + +Email address (and/or SAML NameID, if /a): + +```{code-block} bash +:linenos: true + + curl -X GET \ + --header "Authorization: Bearer $SCIM_TOKEN" \ + --header 'Content-Type: application/json;charset=utf-8' \ + $WIRE_BACKEND/scim/v2/Users/'?filter=externalId%20eq%20%22me%40example.com%22' +``` + +Wire handle: same request, just replace the query part with + +```bash +'?filter=userName%20eq%20%22me%22' +``` + +**Update a specific user** + +For each put request, you need to provide the full json object. All omitted fields will be set to `null`. (If you do not have an up-to-date user present, just `GET` one right before the `PUT`.) + +```{code-block} bash +:linenos: true + + export SCIM_USER='{ + "schemas" : ["urn:ietf:params:scim:schemas:core:2.0:User"], + "externalId" : "rnick@example.com", + "userName" : "newnick", + "displayName" : "The New Nick" + }' +``` + +```{code-block} bash +:linenos: true + + curl -X PUT \ + --header "Authorization: Bearer $SCIM_TOKEN" \ + --header 'Content-Type: application/json;charset=utf-8' \ + -d "$SCIM_USER" \ + $WIRE_BACKEND/scim/v2/Users/$STORED_USER_ID +``` + +**Deactivate user** + +It is possible to temporarily deactivate an user (and reactivate him later) by setting his `active` property to `true/false` without affecting his device history. (`active=false` changes the wire user status to `suspended`.) + +**Delete user** + +```{code-block} bash +:linenos: true + + curl -X DELETE \ + --header "Authorization: Bearer $SCIM_TOKEN" \ + $WIRE_BACKEND/scim/v2/Users/$STORED_USER_ID +``` diff --git a/docs/src/understand/single-sign-on/token-step-01.png b/docs/src/how-to/single-sign-on/understand/token-step-01.png similarity index 100% rename from docs/src/understand/single-sign-on/token-step-01.png rename to docs/src/how-to/single-sign-on/understand/token-step-01.png diff --git a/docs/src/understand/single-sign-on/token-step-02.png b/docs/src/how-to/single-sign-on/understand/token-step-02.png similarity index 100% rename from docs/src/understand/single-sign-on/token-step-02.png rename to docs/src/how-to/single-sign-on/understand/token-step-02.png diff --git a/docs/src/understand/single-sign-on/token-step-03.png b/docs/src/how-to/single-sign-on/understand/token-step-03.png similarity index 100% rename from docs/src/understand/single-sign-on/token-step-03.png rename to docs/src/how-to/single-sign-on/understand/token-step-03.png diff --git a/docs/src/understand/single-sign-on/token-step-04.png b/docs/src/how-to/single-sign-on/understand/token-step-04.png similarity index 100% rename from docs/src/understand/single-sign-on/token-step-04.png rename to docs/src/how-to/single-sign-on/understand/token-step-04.png diff --git a/docs/src/understand/single-sign-on/token-step-05.png b/docs/src/how-to/single-sign-on/understand/token-step-05.png similarity index 100% rename from docs/src/understand/single-sign-on/token-step-05.png rename to docs/src/how-to/single-sign-on/understand/token-step-05.png diff --git a/docs/src/understand/single-sign-on/token-step-06.png b/docs/src/how-to/single-sign-on/understand/token-step-06.png similarity index 100% rename from docs/src/understand/single-sign-on/token-step-06.png rename to docs/src/how-to/single-sign-on/understand/token-step-06.png diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000000..c5b3d5b4db --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,46 @@ +% Wire documentation master file, created by +% sphinx-quickstart on Thu Jul 18 13:44:11 2019. +% You can adapt this file completely to your liking, but it should at least +% contain the root `toctree` directive. + +# Welcome to Wire's documentation! + +If you are a Wire end-user, please check out our [support pages](https://support.wire.com/). + +The targeted audience of this documentation is: + +- the curious power-user (people who want to understand how the server components of Wire work) +- on-premise operators/administrators (people who want to self-host Wire-Server on their own datacentres or cloud) +- developers (people who are working with the wire-server source code) + +If you are a developer, you may want to check out the "Notes for developers" first. + +This documentation may be expanded in the future to cover other aspects of Wire. + +```{toctree} +:caption: 'Contents:' +:glob: true +:maxdepth: 1 + +Release notes +Administrator's Guide +Understanding wire-server components +Single-Sign-On and user provisioning +Client API documentation +Security responses +Notes for developers +``` + +% Overview + +% commented out for now... + +% Indices and tables + +% ================== + +% * :ref:`genindex` + +% * :ref:`modindex` + +% * :ref:`search` diff --git a/docs/src/index.rst b/docs/src/index.rst deleted file mode 100644 index 28721d822a..0000000000 --- a/docs/src/index.rst +++ /dev/null @@ -1,43 +0,0 @@ -.. Wire documentation master file, created by - sphinx-quickstart on Thu Jul 18 13:44:11 2019. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to Wire's documentation! -=============================================== - -If you are a Wire end-user, please check out our `support pages `_. - -The targeted audience of this documentation is: - -* the curious power-user (people who want to understand how the server components of Wire work) -* on-premise operators/administrators (people who want to self-host Wire-Server on their own datacentres or cloud) -* developers (people who are working with the wire-server source code) - -If you are a developer, you may want to check out the "Notes for developers" first. - -This documentation may be expanded in the future to cover other aspects of Wire. - -.. toctree:: - :maxdepth: 1 - :caption: Contents: - :glob: - - Release notes - Administrator's Guide - Understanding wire-server components - Administrator's manual: single-sign-on and user provisioning - Client API documentation - Security responses - Notes for developers - -.. Overview - -.. commented out for now... - -.. Indices and tables -.. ================== - -.. * :ref:`genindex` -.. * :ref:`modindex` -.. * :ref:`search` diff --git a/docs/src/release-notes.rst b/docs/src/release-notes.md similarity index 51% rename from docs/src/release-notes.rst rename to docs/src/release-notes.md index 497af3cca3..478db87668 100644 --- a/docs/src/release-notes.rst +++ b/docs/src/release-notes.md @@ -1,14 +1,13 @@ -.. _release-notes: +(release-notes)= -Release notes -------------- +# Release notes This page previously contained the release notes for the project, and they were manually updated each time a new release was done, due to limitations in Github's «releases» feature. -However, Github since updated the feature, making this page un-necessary. +However, Github since updated the feature, making this page un-necessary. -Go to → `GitHub - wireapp/wire-server: Wire back-end services `_ +Go to → [GitHub - wireapp/wire-server: Wire back-end services](https://github.com/wireapp/wire-server/) -→ Look at releases on right hand side. They are shown by date of release. `Release Notes `_ +→ Look at releases on right hand side. They are shown by date of release. [Release Notes](https://github.com/wireapp/wire-server/releases) -→ Open the CHANGELOG.md. This will give you chart version. \ No newline at end of file +→ Open the CHANGELOG.md. This will give you chart version. diff --git a/docs/src/security-responses/2021-12-15_log4shell.md b/docs/src/security-responses/2021-12-15_log4shell.md new file mode 100644 index 0000000000..b567c21e74 --- /dev/null +++ b/docs/src/security-responses/2021-12-15_log4shell.md @@ -0,0 +1,90 @@ +# 2021-12 - log4shell + +Last updated: 2021-12-15 + +This page concerns ON-PREMISE (i.e. self-hosted) installations of wire-server as documented in and its possible vulnerability to “log4shell” / CVE-2021-44228 and CVE-2021-45046. + +## Introduction + +The “log4shell” vulnerability ([CVE-2021-44228](https://www.cve.org/CVERecord?id=CVE-2021-44228) and [CVE-2021-45046](https://www.cve.org/CVERecord?id=CVE-2021-45046)) concerns a logging library “log4j” used in Java or JVM software components. + +- Wire-server’s source code is not written in a JVM language (it's written mostly in Haskell), and as such, is not vulnerable. + +- Wire-server makes use of Cassandra, which is running on the JVM, however as of version 2.1 no longer makes use of log4j (it uses logback). Since the start of Wire’s on-premise product, we have used Cassandra versions > 3 (currently 3.11), which is not vulnerable. + +- Wire-server makes use of **Elasticsearch**, which **does use log4j. See the section below for details**. + +- All other components Wire-server’s on-premise current and near-time-future product relies on are not based on the JVM and as such are not vulnerable: + + > - Calling restund/SFT servers: written in C + > - Minio: written in Go + > - Redis: written in C + > - Nginx: written in C + > - Wire-Server: written in Haskell + > - Wire-Frontend (webapp, team settings): written in Javascript / NodeJS + > - Fake-aws components: based on localstack written in python or for SQS written in ruby + > - fake-aws-dynamodb: this component is JVM based and was used in the past on on-premise installations, but should not be in use anymore these days. If it is still in use in your environment, please stop using it: all recent versions of wire-server since June 2021 will not make use of that component anymore. Even if still in use, it does not store or log any user-provided data nor is it internet-facing and as such should pose little to no risk. + > - Upcoming releases may have wire-server-metrics: prometheus (Ruby), node-exporter (Golang) and Grafana (Golang) + > - Upcoming releases may have: Logging/Kibana: fluent-bit (C), Kibana (JavaScript), ElasticSearch (covered in section below) + +## Elasticsearch + +Wire uses Elasticsearch for for storing indexes used when searching for users in Wire. + +Elasticsearch clusters are not directly user-facing or internet-facing and it is therefore not immediately possible to inject problematic exploit strings into elasticsearch’s own logging (i.e. elasticsearch stores user-provided data, but doesn’t itself log this data). + +*Example: A Wire user display name will be stored inside elasticsearch, but not logged by elasticsearch (elasticsearch logs mostly contain information about connectivity to other elasticsearch processes)* + +Hypothetically, the log4shell exploit could be combined with another exploit which would allow an attacker to get Elasticsearch to log some of the data stored inside its cluster. As elasticsearch is not internet-facing, this doesn’t look easy to exploit. + +In addition as per Elastics’s [own information on the matter](https://discuss.elastic.co/t/apache-log4j2-remote-code-execution-rce-vulnerability-cve-2021-44228-esa-2021-31/291476) + +> "Elasticsearch 6 and 7 are not susceptible to remote code execution with this vulnerability due to our use of the Java Security Manager. Investigation into Elasticsearch 5 is ongoing. Elasticsearch running on JDK8 or below is susceptible to an information leak via DNS which is fixable by the JVM property identified below. The JVM option identified below is effective for Elasticsearch versions 5.5+, 6.5+, and 7+" + +The JVM property referred to is `-Dlog4j2.formatMsgNoLookups=true` + +[Update 15th December about CVE-2021-45046 from Elasitic](https://discuss.elastic.co/t/apache-log4j2-remote-code-execution-rce-vulnerability-cve-2021-44228-esa-2021-31/291476): + +> "Update 15 December: A further vulnerability (CVE-2021-45046) was disclosed on December 14th after it was found that the fix to address CVE-2021-44228 in Apache Log4j 2.15.0 was incomplete in certain non-default configurations. Our guidance for Elasticsearch \[...\] are unchanged by this new vulnerability" + +Wire on-premise installations contain a version of Elasticsearch between \[`6.6.0` and `6.8.18`\] at the time of writing. + +**As such, while ElasticSearch is affected, it is A. only susceptible to an information leak, not to remote code execution and B. not easily exploitable due to the way Wire uses ElasticSearch.** + +Still, if you’d like to avoid even the potential information leak problem: + +## Disable log4jLookups: + +If you have followed our official documentation on [https://docs.wire.com](https://docs.wire.com), then Elasticsearch on premise was set up using [wire-server-deploy](https://github.com/wireapp/wire-server-deploy) using the `./ansible/elasticsearch.yml` playbook, which installs a vulnerable Log4J `2.11.1`: + +``` +find / | grep -i log4j +./etc/elasticsearch/HOSTNAME/log4j2.properties +./usr/share/elasticsearch/lib/log4j-core-2.11.1.jar +./usr/share/elasticsearch/lib/log4j-1.2-api-2.11.1.jar +./usr/share/elasticsearch/lib/log4j-api-2.11.1.jar +``` + +The BSI [recommends](https://www.bsi.bund.de/SharedDocs/Cybersicherheitswarnungen/DE/2021/2021-549032-10F2.pdf?__blob=publicationFile&v=3) to mitigate setting the `log4j2.formatMsgNoLookups` to True in the JVM options. Elastic [recommends](https://discuss.elastic.co/t/apache-log4j2-remote-code-execution-rce-vulnerability-cve-2021-44228-esa-2021-31/291476) the same mitigation. + +You can do this in the concrete Wire on-premise case using: + +First, ssh to all your elasticsearch machines and do the following: + +```shell +find /etc/elasticsearch | grep jvm.options + +# set this variable with the filepath found from above, usually something like +# /etc/elasticsearch//jvm.options +JVM_OPTIONS_FILE= + +# run the following to add the mitigation log4j flag (command is idempotent) +grep "\-Dlog4j2.formatMsgNoLookups=True" "$JVM_OPTIONS_FILE" || echo "-Dlog4j2.formatMsgNoLookups=True" >> "$JVM_OPTIONS_FILE" +``` + +Next, restart your cluster using instructions provided in {ref}`restart-elasticsearch`. + +## Further information + +- A mitigation for this with fresh on-premise installations is introduced in [https://github.com/wireapp/wire-server-deploy/pull/526](https://github.com/wireapp/wire-server-deploy/pull/526) +- We have of course fully applied the above counter measures to our cloud offering. We have no evidence that this vulnerability was used to launch an attack before this. Any hypothetical undetected attack would have required additional security vulnerabilities to be successful. diff --git a/docs/src/security-responses/2021-12-15_log4shell.rst b/docs/src/security-responses/2021-12-15_log4shell.rst deleted file mode 100644 index 741d2622cc..0000000000 --- a/docs/src/security-responses/2021-12-15_log4shell.rst +++ /dev/null @@ -1,103 +0,0 @@ -2021-12 - log4shell --------------------- - -Last updated: 2021-12-15 - -This page concerns ON-PREMISE (i.e. self-hosted) installations of wire-server as documented in https://docs.wire.com and its possible vulnerability to “log4shell” / CVE-2021-44228 and CVE-2021-45046. - -Introduction -~~~~~~~~~~~~~ - -The “log4shell” vulnerability (`CVE-2021-44228 `__ and `CVE-2021-45046 `__) concerns a logging library “log4j” used in Java or JVM software components. - -* Wire-server’s source code is not written in a JVM language (it's written mostly in Haskell), and as such, is not vulnerable. - -* Wire-server makes use of Cassandra, which is running on the JVM, however as of version 2.1 no longer makes use of log4j (it uses logback). Since the start of Wire’s on-premise product, we have used Cassandra versions > 3 (currently 3.11), which is not vulnerable. - -* Wire-server makes use of **Elasticsearch**, which **does use log4j. See the section below for details**. - -* All other components Wire-server’s on-premise current and near-time-future product relies on are not based on the JVM and as such are not vulnerable: - - * Calling restund/SFT servers: written in C - - * Minio: written in Go - - * Redis: written in C - - * Nginx: written in C - - * Wire-Server: written in Haskell - - * Wire-Frontend (webapp, team settings): written in Javascript / NodeJS - - * Fake-aws components: based on localstack written in python or for SQS written in ruby - - * fake-aws-dynamodb: this component is JVM based and was used in the past on on-premise installations, but should not be in use anymore these days. If it is still in use in your environment, please stop using it: all recent versions of wire-server since June 2021 will not make use of that component anymore. Even if still in use, it does not store or log any user-provided data nor is it internet-facing and as such should pose little to no risk. - - * Upcoming releases may have wire-server-metrics: prometheus (Ruby), node-exporter (Golang) and Grafana (Golang) - - * Upcoming releases may have: Logging/Kibana: fluent-bit (C), Kibana (JavaScript), ElasticSearch (covered in section below) - -Elasticsearch -~~~~~~~~~~~~~ - -Wire uses Elasticsearch for for storing indexes used when searching for users in Wire. - -Elasticsearch clusters are not directly user-facing or internet-facing and it is therefore not immediately possible to inject problematic exploit strings into elasticsearch’s own logging (i.e. elasticsearch stores user-provided data, but doesn’t itself log this data). - -*Example: A Wire user display name will be stored inside elasticsearch, but not logged by elasticsearch (elasticsearch logs mostly contain information about connectivity to other elasticsearch processes)* - -Hypothetically, the log4shell exploit could be combined with another exploit which would allow an attacker to get Elasticsearch to log some of the data stored inside its cluster. As elasticsearch is not internet-facing, this doesn’t look easy to exploit. - -In addition as per Elastics’s `own information on the matter `__ - - "Elasticsearch 6 and 7 are not susceptible to remote code execution with this vulnerability due to our use of the Java Security Manager. Investigation into Elasticsearch 5 is ongoing. Elasticsearch running on JDK8 or below is susceptible to an information leak via DNS which is fixable by the JVM property identified below. The JVM option identified below is effective for Elasticsearch versions 5.5+, 6.5+, and 7+" - -The JVM property referred to is ``-Dlog4j2.formatMsgNoLookups=true`` - -`Update 15th December about CVE-2021-45046 from Elasitic `__: - - "Update 15 December: A further vulnerability (CVE-2021-45046) was disclosed on December 14th after it was found that the fix to address CVE-2021-44228 in Apache Log4j 2.15.0 was incomplete in certain non-default configurations. Our guidance for Elasticsearch [...] are unchanged by this new vulnerability" - -Wire on-premise installations contain a version of Elasticsearch between [``6.6.0`` and ``6.8.18``] at the time of writing. - -**As such, while ElasticSearch is affected, it is A. only susceptible to an information leak, not to remote code execution and B. not easily exploitable due to the way Wire uses ElasticSearch.** - -Still, if you’d like to avoid even the potential information leak problem: - -Disable log4jLookups: -~~~~~~~~~~~~~~~~~~~~~ - -If you have followed our official documentation on ``__, then Elasticsearch on premise was set up using `wire-server-deploy `__ using the ``./ansible/elasticsearch.yml`` playbook, which installs a vulnerable Log4J ``2.11.1``:: - - find / | grep -i log4j - ./etc/elasticsearch/HOSTNAME/log4j2.properties - ./usr/share/elasticsearch/lib/log4j-core-2.11.1.jar - ./usr/share/elasticsearch/lib/log4j-1.2-api-2.11.1.jar - ./usr/share/elasticsearch/lib/log4j-api-2.11.1.jar - -The BSI `recommends `__ to mitigate setting the ``log4j2.formatMsgNoLookups`` to True in the JVM options. Elastic `recommends `__ the same mitigation. - -You can do this in the concrete Wire on-premise case using: - -First, ssh to all your elasticsearch machines and do the following: - -.. code:: shell - - find /etc/elasticsearch | grep jvm.options - - # set this variable with the filepath found from above, usually something like - # /etc/elasticsearch//jvm.options - JVM_OPTIONS_FILE= - - # run the following to add the mitigation log4j flag (command is idempotent) - grep "\-Dlog4j2.formatMsgNoLookups=True" "$JVM_OPTIONS_FILE" || echo "-Dlog4j2.formatMsgNoLookups=True" >> "$JVM_OPTIONS_FILE" - -Next, restart your cluster using instructions provided in :ref:`restart-elasticsearch`. - -Further information -~~~~~~~~~~~~~~~~~~~ - -* A mitigation for this with fresh on-premise installations is introduced in ``__ - -* We have of course fully applied the above counter measures to our cloud offering. We have no evidence that this vulnerability was used to launch an attack before this. Any hypothetical undetected attack would have required additional security vulnerabilities to be successful. diff --git a/docs/src/security-responses/index.md b/docs/src/security-responses/index.md new file mode 100644 index 0000000000..a0c58f66ff --- /dev/null +++ b/docs/src/security-responses/index.md @@ -0,0 +1,14 @@ +(security-responses)= + +# Security responses + +% comment: The toctree directive below takes a list of the pages you want to appear in order, +% and '*' is used to include any other pages in the federation directory in alphabetical order + +```{toctree} +:glob: true +:maxdepth: 1 +:reversed: true + +* +``` diff --git a/docs/src/security-responses/index.rst b/docs/src/security-responses/index.rst deleted file mode 100644 index 1c1e3077c0..0000000000 --- a/docs/src/security-responses/index.rst +++ /dev/null @@ -1,16 +0,0 @@ -.. _security_responses: - -++++++++++++++++++ -Security responses -++++++++++++++++++ - -.. - comment: The toctree directive below takes a list of the pages you want to appear in order, - and '*' is used to include any other pages in the federation directory in alphabetical order - -.. toctree:: - :maxdepth: 1 - :glob: - :reversed: - - * diff --git a/docs/src/understand/api-client-perspective/authentication.md b/docs/src/understand/api-client-perspective/authentication.md new file mode 100644 index 0000000000..51cb738b85 --- /dev/null +++ b/docs/src/understand/api-client-perspective/authentication.md @@ -0,0 +1,435 @@ +# Authentication + +% useful vim replace commands when porting markdown -> restructured text: + +% :%s/.. raw:: html//g + +% :%s/ /.. _\1:/gc + +## Access Tokens + +The authentication protocol used by the API is loosely inspired by the +[OAuth2 protocol](http://oauth.net/2/). As such, API requests are +authorised through so-called [bearer +tokens](https://tools.ietf.org/html/rfc6750). For as long as a bearer +token is valid, it grants access to the API under the identity of the +user whose credentials have been used for the [login]. The +current validity of access tokens is `15 minutes`, however, that may +change at any time without prior notice. + +In order to obtain new access tokens without having to ask the user for +his credentials again, so-called "user tokens" are issued which are +issued in the form of a `zuid` HTTP +[cookie](https://en.wikipedia.org/wiki/HTTP_cookie). These cookies +have a long lifetime (if {ref}`persistent ` typically +at least a few months) and their use is strictly limited to the +{ref}`/access ` endpoint used for token refresh. +{ref}`Persistent ` access cookies are regularly +refreshed as part of an {ref}`access token refresh `. + +An access cookie is obtained either directly after registration or through a +subsequent {ref}`login `. A successful login provides both an access +cookie and and access token. Both access token and cookie must be stored safely +and kept confidential. User passwords should not be stored. + +As of yet, there is no concept of authorising third-party applications to +perform operations on the API on behalf of a user (Notable exceptions: +{ref}`sso`). Such functionality may be provided in the future through +standardised OAuth2 flows. + +To authorise an API request, the access token must be provided via the +HTTP `Authorization` header with the `Bearer` scheme as follows: + +``` +Authorization: Bearer fmmLpDSjArpksFv57r5rDrzZZlj... +``` + +While the API currently also supports passing the access token in the +query string of a request, this approach is highly discouraged as it +unnecessarily exposes access tokens (e.g. in server logs) and thus might +be removed in the future. + +(login)= + +## Login - `POST /login` + +A login is the process of authenticating a user either through a known secret in +a {ref}`password login ` or by proving ownership of a verified +phone number associated with an account in an {ref}`SMS login `. The +response to a successful login contains an access cookie in a `Set-Cookie` +header and an access token in the JSON response body. + +(login-cookies)= + +### Cookies + +There is a hard limit for the number of session-scoped access cookies and the same +amount of persistent access cookies per user account. When this number is +reached, old cookies are removed when new ones are issued. Thereby, the cookies +with the oldest expiration timestamp are removed first. The removal takes the +type of the cookie to issue into account. I.e. session cookies are replaced by +session cookies, persistent cookies are replaced by persistent cookies. + +To prevent performance issues and malicious usages of the API, there is a +throttling mechanism in place. When the maximum number of cookies of one type +are issued, it's checked that login calls don't happen too frequently (too +quickly after one another.) + +In case of throttling no cookie gets issued. The error response ([HTTP status +code 429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429)) has +a `Retry-After` header which specifies the time to wait before accepting the +next request in Seconds. + +Being throttled is a clear indicator of incorrect API usage. There is no need to +login many times in a row on the same device. Instead, the cookie should be +re-used. + +The corresponding backend configuration settings are described in: +{ref}`auth-cookie-config` . + +(login-password)= + +### Password Login + +To perform a password login, send a `POST` request to the `/login` +endpoint, providing either a verified email address or phone number and +the corresponding password. For example: + +``` +POST /login HTTP/1.1 +[headers omitted] + +{ + "email": "me@wire.com", + "password": "Quo2Booz" +} +``` + +If a phone number is used, the `phone` field is used instead of +`email`. If a @handle is used, the `handle` field is used instead of +`email` (note that the handle value should be sent *without* the `@` +symbol). Assuming the credentials are correct, the API will respond with +a `200 OK` and an access token and cookie: + +``` +HTTP/1.1 200 OK +zuid=...; Expires=Fri, 02-Aug-2024 09:15:54 GMT; Domain=zinfra.io; Path=/access; HttpOnly; Secure +[other headers omitted] + +{ + "expires_in": 900, + "access_token": "fmmLpDSjArpksFv57r5rDrzZZlj...", + "token_type": "Bearer" +} +``` + +% + +> The `Domain` of the cookie will be different depending on the +> environment. + +The value of `expires_in` is the number of seconds that the +`access_token` is valid from the moment it was issued. + +As of yet, the `token_type` is always `Bearer`. + +(login-sms)= + +### SMS Login + +To perform an SMS login, first request an SMS code to be sent to a +verified phone number: + +``` +POST /login/send HTTP/1.1 +[headers omitted] + +{ + "phone": "+1234567890" +} +``` + +An SMS with a short-lived login code will be sent. Upon receiving the +SMS and extracting the code from it, the login can be performed using +the `phone` and `code` as follows: + +``` +POST /login HTTP/1.1 +[headers omitted] + +{ + "phone": "+1234567890", + "code": "123456" +} +``` + +A successful response is identical to that of a {ref}`password +login `. + +(login-persistent)= + +### Persistent Logins + +By default, access cookies are issued as [session +cookies](https://en.wikipedia.org/wiki/HTTP_cookie#Session_cookie) +with a validity of 1 week. Furthermore, these session cookies are not +refreshed as part of an {ref}`access token refresh `. To +request a `persistent` access cookie which does get refreshed, specify +the `persist=true` parameter during a login: + +``` +POST /login?persist=true HTTP/1.1 +[headers omitted] + +{ + "phone": "+1234567890", + "code": "123456" +} +``` + +All access cookies returned on registration are persistent. + +(token-refresh)= + +### FAQ: is my cookie a persistent cookie or a session cookie? + +When you log in **without** the `persist=true` query parameter, or +with persist=false, you get a `session cookie`, which means it has no +expiration date set, and will expire when you close the browser (and on +the backend has a validity of max 1 day or 1 week (configurable, see +current config in [hegemony](https://github.com/zinfra/hegemony)). +Example **session cookie**: + +``` +POST /login?persist=false + +Set-Cookie: zuid=(redacted); Path=/access; Domain=zinfra.io; HttpOnly; Secure +``` + +When you log in **with** `persist=true`, you get a persistent cookie, +which means it has *some* expiration date. In production this is +currently 56 days (again, configurable, check current config in +[hegemony](https://github.com/zinfra/hegemony)) and can be renewed +during token refresh. Example **persistent cookie**: + +``` +POST /login?persist=true + +Set-Cookie: zuid=(redacted); Path=/access; Expires=Thu, 10-Jan-2019 10:43:28 GMT; Domain=zinfra.io; HttpOnly; Secure +``` + +## Token Refresh - `POST /access` + +Since access tokens have a relatively short lifetime to limit the time +window of abuse for a captured token, they need to be regularly +refreshed. In order to refresh an access token, send a `POST` reques +to `/access`, including the access cookie in the `Cookie` header and +the old (possibly expired) access token in the `Authorization` header: + +``` +POST /access HTTP/1.1 +Authorization: Bearer fmmLpDSjArpksFv57r5rDrzZZlj... +Cookie: zuid=... +[other headers omitted] + + +``` + +Providing the old access token is not required but strongly recommended +as it will link the new access token to the old, enabling the API to see +the new access token as a continued session of the same client. + +As part of an access token refresh, the response may also contain a new +`zuid` access cookie in form of a `Set-Cookie` header. A client must +expect a new `zuid` cookie as part of any access token refresh and +replace the existing cookie appropriately. + +(cookies-1)= + +## Cookie Management + +(cookies-logout)= + +### Logout - `POST /access/logout` + +An explicit logout effectively deletes the cookie used to perform the +operation: + +``` +POST /access/logout HTTP/1.1 +Authorization: Bearer fmmLpDSjArpksFv57r5rDrzZZlj... +Cookie: zuid=... +[other headers omitted] + + +``` + +Afterwards, the cookie that was sent as part of the `Cookie` header is +no longer valid. + +If a client offers an explicit logout, this operation must be performed. +An explicit logout is especially important for Web clients. + +(cookies-labels)= + +### Labels + +Cookies can be labeled by specifying a `label` during login or +registration, e.g.: + +``` +POST /login?persist=true HTTP/1.1 +[headers omitted] + +{ + "phone": "+1234567890", + "code": "123456", + "label": "Google Nexus 5" +} +``` + +Specifying a label is recommended as it helps to identify the cookies in a +user-friendly way and allows {ref}`selective revocation ` based +on the labels. + +(cookies-list)= + +### Listing Cookies - `GET /cookies` + +To list the cookies currently associated with an account, send a `GET` +request to `/cookies`. The response will contain a list of cookies, +e.g.: + +``` +HTTP/1.1 200 OK +[other headers omitted] + +{ + "cookies": [ + { + "time": "2015-06-04T14:29:23.000Z", + "id": 967153183, + "type": "session", + "label": null + }, + { + "time": "2015-06-04T14:44:23.000Z", + "id": 942451749, + "type": "session", + "label": null + }, + ... + ] +} +``` + +Note that expired cookies are not automatically removed when they +expire, only as new cookies are issued. + +(cookies-revoke)= + +### Revoking Cookies - `POST /cookies/remove` + +Cookies can be removed individually or in bulk either by specifying the full +cookie structure as it is returned by {ref}`GET /cookies ` or only +by their labels in a `POST` request to `/cookies/remove`, alongside with the +user's credentials: + +``` +POST /cookies/remove HTTP/1.1 +[headers omitted] + +{ + "ids": [{}, {}, ...], + "labels": ["", "", ...] + "email": "me@wire.com", + "password": "secret" +} +``` + +Cookie removal currently requires an account with an email address and +password. + +(password-reset)= + +## Password Reset - `POST /password-reset` + +A password reset can be used to set a new password if the existing password +associated with an account has been forgotten. This is not to be confused with +the act of merely changing your password for the purpose of password rotation or +if you suspect your current password to be compromised. + +### Initiate a Password Reset + +To initiate a password reset, send a `POST` request to +`/password-reset`, specifying either a verified email address or phone +number for the account in question: + +``` +POST /password-reset HTTP/1.1 +[headers omitted] + +{ + "phone": "+1234567890" +} +``` + +For a phone number, the `phone` field would be used instead. As a +result of a successful request, either a password reset key and code is +sent via email or a password reset code is sent via SMS, depending on +whether an email address or a phone number was provided. Password reset +emails will contain a link to the [wire.com](https://www.wire.com/) +website which will guide the user through the completion of the password +reset, which means that the website will perform the necessary requests +to complete the password reset. To complete a password reset initiated +with a phone number, the completion of the password reset has to happen +from the mobile client application itself. + +Once a password reset has been initiated for an email address or phone +number, no further password reset can be initiated for the same email +address or phone number before the prior reset is completed or times +out. The current timeout for an initiated password reset is +`10 minutes`. + +### Complete a Password Reset + +To complete a password reset, the password reset code, together with the +new password and the `email` or `phone` used when initiating the +reset (or the opaque `key` sent by mail) are sent to +`/password-reset/complete` in a `POST` request: + +``` +POST /password-reset/complete HTTP/1.1 +[headers omitted] + +{ + "phone": "+1234567890", + "code": "123456", + "password": "new-secret-password" +} +``` + +There is a maximum of `3` attempts at completing a password reset, +after which the password reset code becomes invalid and a new password +reset must be initiated. + +A completed password reset results in all access cookies to be revoked, +requiring the user to {ref}`login `. + +## Related topics: SSO, Legalhold + +(sso)= + +### Single Sign-On + +Users that are part of a team, for which a team admin has configured SSO (Single Sign On), authentication can happen through SAML. + +More information: + +- {ref}`FAQ ` +- [setup howtos for various IdP vendors](https://docs.wire.com/how-to/single-sign-on/index.html) +- [a few fragments that may help admins](https://github.com/wireapp/wire-server/blob/develop/docs/reference/spar-braindump.md) + +### LegalHold + +Users that are part of a team, for which a team admin has configured "LegalHold", can add a so-called "LegalHold" device. The endpoints in use to authenticate for a "LegalHold" Device are the same as for regular users, but the access tokens they get can only use a restricted set of API endpoints. See also [legalhold documentation on wire-server](https://github.com/wireapp/wire-server/blob/develop/docs/reference/team/legalhold.md) diff --git a/docs/src/understand/api-client-perspective/authentication.rst b/docs/src/understand/api-client-perspective/authentication.rst deleted file mode 100644 index 52630c58a6..0000000000 --- a/docs/src/understand/api-client-perspective/authentication.rst +++ /dev/null @@ -1,476 +0,0 @@ -Authentication -============== - -.. useful vim replace commands when porting markdown -> restructured text: -.. :%s/.. raw:: html//g -.. :%s/ /.. _\1:/gc - -Access Tokens -------------- - -The authentication protocol used by the API is loosely inspired by the -`OAuth2 protocol `__. As such, API requests are -authorised through so-called `bearer -tokens `__. For as long as a bearer -token is valid, it grants access to the API under the identity of the -user whose credentials have been used for the login_. The -current validity of access tokens is ``15 minutes``, however, that may -change at any time without prior notice. - -In order to obtain new access tokens without having to ask the user for -his credentials again, so-called "user tokens" are issued which are -issued in the form of a ``zuid`` HTTP -`cookie `__. These cookies -have a long lifetime (if `persistent <#login-persistent>`__, typically -at least a few months) and their use is strictly limited to the -`/access <#token-refresh>`__ endpoint used for token refresh. -`Persistent <#login-persistent>`__ access cookies are regularly -refreshed as part of an `access token refresh <#token-refresh>`__. - -An access cookie is obtained either directly after -`registration `__ or through a -subsequent `login <#login>`__. A successful login provides both an -access cookie and and access token. Both access token and cookie must be -stored safely and kept confidential. User passwords should not be -stored. - -As of yet, there is no concept of authorising third-party applications to -perform operations on the API on behalf of a user (Notable exceptions: -:ref:`sso`). Such functionality may be provided in the future through -standardised OAuth2 flows. - -To authorise an API request, the access token must be provided via the -HTTP ``Authorization`` header with the ``Bearer`` scheme as follows: - -:: - - Authorization: Bearer fmmLpDSjArpksFv57r5rDrzZZlj... - -While the API currently also supports passing the access token in the -query string of a request, this approach is highly discouraged as it -unnecessarily exposes access tokens (e.g. in server logs) and thus might -be removed in the future. - -.. _login: - -Login - ``POST /login`` ------------------------ - -A login is the process of authenticating a user either through a known -secret in a `password login <#login-password>`__ or by proving ownership -of a verified phone number associated with an account in an `SMS -login <#login-sms>`__. The response to a successful login contains an -access cookie in a ``Set-Cookie`` header and an access token in the JSON -response body. - -.. _login-cookies: - -Cookies -~~~~~~~ - -There is a hard limit for the number of session-scoped access cookies and the same -amount of persistent access cookies per user account. When this number is -reached, old cookies are removed when new ones are issued. Thereby, the cookies -with the oldest expiration timestamp are removed first. The removal takes the -type of the cookie to issue into account. I.e. session cookies are replaced by -session cookies, persistent cookies are replaced by persistent cookies. - -To prevent performance issues and malicious usages of the API, there is a -throttling mechanism in place. When the maximum number of cookies of one type -are issued, it's checked that login calls don't happen too frequently (too -quickly after one another.) - -In case of throttling no cookie gets issued. The error response (`HTTP status -code 429 `_) has -a ``Retry-After`` header which specifies the time to wait before accepting the -next request in Seconds. - -Being throttled is a clear indicator of incorrect API usage. There is no need to -login many times in a row on the same device. Instead, the cookie should be -re-used. - -The corresponding backend configuration settings are described in: -:ref:`auth-cookie-config` . - -.. _login-password: - -Password Login -~~~~~~~~~~~~~~ - -To perform a password login, send a ``POST`` request to the ``/login`` -endpoint, providing either a verified email address or phone number and -the corresponding password. For example: - -:: - - POST /login HTTP/1.1 - [headers omitted] - - { - "email": "me@wire.com", - "password": "Quo2Booz" - } - -If a phone number is used, the ``phone`` field is used instead of -``email``. If a @handle is used, the ``handle`` field is used instead of -``email`` (note that the handle value should be sent *without* the ``@`` -symbol). Assuming the credentials are correct, the API will respond with -a ``200 OK`` and an access token and cookie: - -:: - - HTTP/1.1 200 OK - zuid=...; Expires=Fri, 02-Aug-2024 09:15:54 GMT; Domain=zinfra.io; Path=/access; HttpOnly; Secure - [other headers omitted] - - { - "expires_in": 900, - "access_token": "fmmLpDSjArpksFv57r5rDrzZZlj...", - "token_type": "Bearer" - } - -.. - - The ``Domain`` of the cookie will be different depending on the - environment. - -The value of ``expires_in`` is the number of seconds that the -``access_token`` is valid from the moment it was issued. - -As of yet, the ``token_type`` is always ``Bearer``. - - - -.. _login-sms: - -SMS Login -~~~~~~~~~ - -To perform an SMS login, first request an SMS code to be sent to a -verified phone number: - -:: - - POST /login/send HTTP/1.1 - [headers omitted] - - { - "phone": "+1234567890" - } - -An SMS with a short-lived login code will be sent. Upon receiving the -SMS and extracting the code from it, the login can be performed using -the ``phone`` and ``code`` as follows: - -:: - - POST /login HTTP/1.1 - [headers omitted] - - { - "phone": "+1234567890", - "code": "123456" - } - -A successful response is identical to that of a `password -login <#login-password>`__. - - - -.. _login-persistent: - -Persistent Logins -~~~~~~~~~~~~~~~~~ - -By default, access cookies are issued as `session -cookies `__ -with a validity of 1 week. Furthermore, these session cookies are not -refreshed as part of an `access token refresh <#token-refresh>`__. To -request a ``persistent`` access cookie which does get refreshed, specify -the ``persist=true`` parameter during a login: - -:: - - POST /login?persist=true HTTP/1.1 - [headers omitted] - - { - "phone": "+1234567890", - "code": "123456" - } - -All access cookies returned on registration are persistent. - - - -.. _token-refresh: - -FAQ: is my cookie a persistent cookie or a session cookie? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -When you log in **without** the ``persist=true`` query parameter, or -with persist=false, you get a ``session cookie``, which means it has no -expiration date set, and will expire when you close the browser (and on -the backend has a validity of max 1 day or 1 week (configurable, see -current config in `hegemony `__). -Example **session cookie**: - -:: - - POST /login?persist=false - - Set-Cookie: zuid=(redacted); Path=/access; Domain=zinfra.io; HttpOnly; Secure - -When you log in **with** ``persist=true``, you get a persistent cookie, -which means it has *some* expiration date. In production this is -currently 56 days (again, configurable, check current config in -`hegemony `__) and can be renewed -during token refresh. Example **persistent cookie**: - -:: - - POST /login?persist=true - - Set-Cookie: zuid=(redacted); Path=/access; Expires=Thu, 10-Jan-2019 10:43:28 GMT; Domain=zinfra.io; HttpOnly; Secure - -Token Refresh - ``POST /access`` --------------------------------- - -Since access tokens have a relatively short lifetime to limit the time -window of abuse for a captured token, they need to be regularly -refreshed. In order to refresh an access token, send a ``POST`` reques -to ``/access``, including the access cookie in the ``Cookie`` header and -the old (possibly expired) access token in the ``Authorization`` header: - -:: - - POST /access HTTP/1.1 - Authorization: Bearer fmmLpDSjArpksFv57r5rDrzZZlj... - Cookie: zuid=... - [other headers omitted] - - - -Providing the old access token is not required but strongly recommended -as it will link the new access token to the old, enabling the API to see -the new access token as a continued session of the same client. - -As part of an access token refresh, the response may also contain a new -``zuid`` access cookie in form of a ``Set-Cookie`` header. A client must -expect a new ``zuid`` cookie as part of any access token refresh and -replace the existing cookie appropriately. - - - -.. _cookies: - -Cookie Management ------------------ - - - -.. _cookies-logout: - -Logout - ``POST /access/logout`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -An explicit logout effectively deletes the cookie used to perform the -operation: - -:: - - POST /access/logout HTTP/1.1 - Authorization: Bearer fmmLpDSjArpksFv57r5rDrzZZlj... - Cookie: zuid=... - [other headers omitted] - - - -Afterwards, the cookie that was sent as part of the ``Cookie`` header is -no longer valid. - -If a client offers an explicit logout, this operation must be performed. -An explicit logout is especially important for Web clients. - - - -.. _cookies-labels: - -Labels -~~~~~~ - -Cookies can be labeled by specifying a ``label`` during login or -registration, e.g.: - -:: - - POST /login?persist=true HTTP/1.1 - [headers omitted] - - { - "phone": "+1234567890", - "code": "123456", - "label": "Google Nexus 5" - } - -Specifying a label is recommended as it helps to identify the cookies in -a user-friendly way and allows `selective -revocation <#cookies-revoke>`__ based on the labels. - - - -.. _cookies-list: - -Listing Cookies - ``GET /cookies`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To list the cookies currently associated with an account, send a ``GET`` -request to ``/cookies``. The response will contain a list of cookies, -e.g.: - -:: - - HTTP/1.1 200 OK - [other headers omitted] - - { - "cookies": [ - { - "time": "2015-06-04T14:29:23.000Z", - "id": 967153183, - "type": "session", - "label": null - }, - { - "time": "2015-06-04T14:44:23.000Z", - "id": 942451749, - "type": "session", - "label": null - }, - ... - ] - } - -Note that expired cookies are not automatically removed when they -expire, only as new cookies are issued. - - - -.. _cookies-revoke: - -Revoking Cookies - ``POST /cookies/remove`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Cookies can be removed individually or in bulk either by specifying the -full cookie structure as it is returned by `GET -/cookies <#cookies-list>`__ or only by their labels in a ``POST`` -request to ``/cookies/remove``, alongside with the user's credentials: - -:: - - POST /cookies/remove HTTP/1.1 - [headers omitted] - - { - "ids": [{}, {}, ...], - "labels": ["", "", ...] - "email": "me@wire.com", - "password": "secret" - } - -Cookie removal currently requires an account with an email address and -password. - - - -.. _password-reset: - -Password Reset - ``POST /password-reset`` ------------------------------------------ - -A password reset can be used to set a new password if the existing -password associated with an account has been forgotten. This is not to -be confused with the act of merely `changing your -password `__ for the purpose of password -rotation or if you suspect your current password to be compromised. - -Initiate a Password Reset -~~~~~~~~~~~~~~~~~~~~~~~~~ - -To initiate a password reset, send a ``POST`` request to -``/password-reset``, specifying either a verified email address or phone -number for the account in question: - -:: - - POST /password-reset HTTP/1.1 - [headers omitted] - - { - "phone": "+1234567890" - } - -For a phone number, the ``phone`` field would be used instead. As a -result of a successful request, either a password reset key and code is -sent via email or a password reset code is sent via SMS, depending on -whether an email address or a phone number was provided. Password reset -emails will contain a link to the `wire.com `__ -website which will guide the user through the completion of the password -reset, which means that the website will perform the necessary requests -to complete the password reset. To complete a password reset initiated -with a phone number, the completion of the password reset has to happen -from the mobile client application itself. - -Once a password reset has been initiated for an email address or phone -number, no further password reset can be initiated for the same email -address or phone number before the prior reset is completed or times -out. The current timeout for an initiated password reset is -``10 minutes``. - -Complete a Password Reset -~~~~~~~~~~~~~~~~~~~~~~~~~ - -To complete a password reset, the password reset code, together with the -new password and the ``email`` or ``phone`` used when initiating the -reset (or the opaque ``key`` sent by mail) are sent to -``/password-reset/complete`` in a ``POST`` request: - -:: - - POST /password-reset/complete HTTP/1.1 - [headers omitted] - - { - "phone": "+1234567890", - "code": "123456", - "password": "new-secret-password" - } - -There is a maximum of ``3`` attempts at completing a password reset, -after which the password reset code becomes invalid and a new password -reset must be initiated. - -A completed password reset results in all access cookies to be revoked, -requiring the user to `login <#login>`__. - -Related topics: SSO, Legalhold -------------------------------- - -.. _sso: - -Single Sign-On -~~~~~~~~~~~~~~~~~~ - -Users that are part of a team, for which a team admin has configured SSO (Single Sign On), authentication can happen through SAML. - -More information: - -* :ref:`FAQ ` -* `setup howtos for various IdP vendors `__ -* `a few fragments that may help admins `__ - - -LegalHold -~~~~~~~~~~ - -Users that are part of a team, for which a team admin has configured "LegalHold", can add a so-called "LegalHold" device. The endpoints in use to authenticate for a "LegalHold" Device are the same as for regular users, but the access tokens they get can only use a restricted set of API endpoints. See also `legalhold documentation on wire-server `__ diff --git a/docs/src/understand/api-client-perspective/index.md b/docs/src/understand/api-client-perspective/index.md new file mode 100644 index 0000000000..8bf19e4290 --- /dev/null +++ b/docs/src/understand/api-client-perspective/index.md @@ -0,0 +1,15 @@ +# Wire-server API documentation + +The following documentation provides information for, and takes the perspective of a Wire client developer. (wire-desktop, wire-android and wire-ios are examples of Wire Clients). This means only publicly accessible endpoints are mentioned. + +```{warning} +This section of the documentation is very incomplete at the time of writing (summer 2020) - more pages on the client API will follow in the future. +``` + +```{toctree} +:glob: true +:maxdepth: 2 +:titlesonly: true + +* +``` diff --git a/docs/src/understand/api-client-perspective/index.rst b/docs/src/understand/api-client-perspective/index.rst deleted file mode 100644 index d419508892..0000000000 --- a/docs/src/understand/api-client-perspective/index.rst +++ /dev/null @@ -1,14 +0,0 @@ -Wire-server API documentation -============================= - -The following documentation provides information for, and takes the perspective of a Wire client developer. (wire-desktop, wire-android and wire-ios are examples of Wire Clients). This means only publicly accessible endpoints are mentioned. - -.. warning:: - This section of the documentation is very incomplete at the time of writing (summer 2020) - more pages on the client API will follow in the future. - -.. toctree:: - :maxdepth: 2 - :glob: - :titlesonly: - - * diff --git a/docs/src/understand/api-client-perspective/swagger.rst b/docs/src/understand/api-client-perspective/swagger.md similarity index 67% rename from docs/src/understand/api-client-perspective/swagger.rst rename to docs/src/understand/api-client-perspective/swagger.md index 5dd3d29e36..057d5beb2a 100644 --- a/docs/src/understand/api-client-perspective/swagger.rst +++ b/docs/src/understand/api-client-perspective/swagger.md @@ -1,5 +1,4 @@ -Swagger API documentation (all public endpoints) -================================================ +# Swagger API documentation (all public endpoints) Our staging system provides swagger documentation of our public rest API. @@ -11,21 +10,19 @@ documentation still has some endpoints, but the new one is getting more and more Please check the new docs first, and if you can't find what you're looking for, double-check the old. -New docs --------- +## New docs These docs show swagger 2.0: -`new staging swagger page `_ +[new staging swagger page](https://staging-nginz-https.zinfra.io/api/swagger-ui/) - -Old docs --------- +## Old docs Some endpoints are only shown using swagger 1.2. At the time of writing, both swagger version 1.2 and version 2.0 are in use. If you are an employee of Wire, you can log in here and try out requests in the browser; if not, you can make use of the "List Operations" button on both 1.2 and 2.0 pages to see the possible API requests. -Browse to our `old staging swagger page `_ to see rendered swagger documentation for the remaining endpoints. +Browse to our [old staging swagger page](https://staging-nginz-https.zinfra.io/swagger-ui/) to see rendered swagger documentation for the remaining endpoints. -.. image:: img/swagger.png +```{image} img/swagger.png +``` diff --git a/docs/src/understand/federation/index.md b/docs/src/understand/federation/index.md new file mode 100644 index 0000000000..48e78ea649 --- /dev/null +++ b/docs/src/understand/federation/index.md @@ -0,0 +1,24 @@ +(federation-understand)= + +# Wire Federation + +Wire Federation, once implemented, aims to allow multiple Wire-server {ref}`backends ` to federate with each other. That means that a user 1 registered on backend A and a user 2 registered on backend B should be able to interact with each other as if they belonged to the same backend. + +```{note} +Federation is as of January 2022 still work in progress, since the implementation of federation is ongoing, and certain design decision are still subject to change. Where possible documentation will indicate the state of implementation. + +Some sections of the documentation are still incomplete (indicated with a 'TODO' comment). Check back later for updates. +``` + +% comment: The toctree directive below takes a list of the pages you want to appear in order, +% and '*' is used to include any other pages in the federation directory in alphabetical order + +```{toctree} +:glob: true +:maxdepth: 2 +:numbered: true + +introduction +architecture +* +``` diff --git a/docs/src/understand/federation/index.rst b/docs/src/understand/federation/index.rst deleted file mode 100644 index 70ff484f1f..0000000000 --- a/docs/src/understand/federation/index.rst +++ /dev/null @@ -1,25 +0,0 @@ -.. _federation-understand: - -+++++++++++++++++ -Wire Federation -+++++++++++++++++ - -Wire Federation, once implemented, aims to allow multiple Wire-server :ref:`backends ` to federate with each other. That means that a user 1 registered on backend A and a user 2 registered on backend B should be able to interact with each other as if they belonged to the same backend. - -.. note:: - Federation is as of January 2022 still work in progress, since the implementation of federation is ongoing, and certain design decision are still subject to change. Where possible documentation will indicate the state of implementation. - - Some sections of the documentation are still incomplete (indicated with a 'TODO' comment). Check back later for updates. - -.. - comment: The toctree directive below takes a list of the pages you want to appear in order, - and '*' is used to include any other pages in the federation directory in alphabetical order - -.. toctree:: - :maxdepth: 2 - :numbered: - :glob: - - introduction - architecture - * diff --git a/docs/src/understand/helm.md b/docs/src/understand/helm.md new file mode 100644 index 0000000000..9b27659a75 --- /dev/null +++ b/docs/src/understand/helm.md @@ -0,0 +1,61 @@ +(understand-helm)= + +# Understanding helm + +See also the official [helm documentation](https://docs.helm.sh/). This page is meant to explain a few concepts directly relevant when installing wire-server helm charts. + +(understand-helm-overrides)= + +## Overriding helm configuration settings + +### Default values + +Default values are under a specific chart's `values.yaml` file, e.g. for the chart named `cassandra-ephemeral`, this file: [charts/cassandra-ephemeral/values.yaml](https://github.com/wireapp/wire-server/blob/develop/charts/cassandra-ephemeral/values.yaml). When you install or upgrade a chart, with e.g.: + +``` +helm upgrade --install my-cassandra wire/cassandra-ephemeral +``` + +Then the default values from above are used. + +### Overriding + +Overriding parts of the yaml configuration can be achieved by passing `-f path/to/override-file.yaml` when installing or upgrading a helm chart, like this: + +Create file my-file.yaml: + +```yaml +cassandra-ephemeral: + resources: + requests: + cpu: "2" +``` + +Now you can install that chart with a custom value (using 2 cpu cores): + +``` +helm upgrade --install my-cassandra wire/cassandra-ephemeral -f my-values.yaml +``` + +### Sub charts + +If a chart uses sub charts, there can be overrides in the parent +chart's `values.yaml` file, if namespaced to the sub chart. +Example: if chart `parent` includes chart `child`, and +`child`'s `values.yaml` has a default value `foo: bar`, and the +`parent` chart's `values.yaml` has a value + +```yaml +child: + foo: baz +``` + +then the value that will be used for `foo` by default is `baz` when you install the parent chart. + +Note that if you `helm install parent` but wish to override values for `child`, you need to pass them as above, indented underneath `child:` as above. + +### Multiple overrides + +If `-f ` is used multiple times, the last file wins in case keys exist +multiple times (there is no merge performed between multiple files passed to `-f`). +This can lead to unexpected results. If you use multiple files with `-f`, ensure they don't overlap. diff --git a/docs/src/understand/helm.rst b/docs/src/understand/helm.rst deleted file mode 100644 index 3899186182..0000000000 --- a/docs/src/understand/helm.rst +++ /dev/null @@ -1,64 +0,0 @@ -.. _understand-helm: - -Understanding helm -=================== - -See also the official `helm documentation `__. This page is meant to explain a few concepts directly relevant when installing wire-server helm charts. - - -.. _understand-helm-overrides: - -Overriding helm configuration settings ------------------------------------------- - -Default values -^^^^^^^^^^^^^^ - -Default values are under a specific chart's ``values.yaml`` file, e.g. for the chart named ``cassandra-ephemeral``, this file: `charts/cassandra-ephemeral/values.yaml `__. When you install or upgrade a chart, with e.g.:: - - helm upgrade --install my-cassandra wire/cassandra-ephemeral - -Then the default values from above are used. - -Overriding -^^^^^^^^^^^ - -Overriding parts of the yaml configuration can be achieved by passing ``-f path/to/override-file.yaml`` when installing or upgrading a helm chart, like this: - -Create file my-file.yaml: - -.. code:: yaml - - cassandra-ephemeral: - resources: - requests: - cpu: "2" - -Now you can install that chart with a custom value (using 2 cpu cores):: - - helm upgrade --install my-cassandra wire/cassandra-ephemeral -f my-values.yaml - -Sub charts -^^^^^^^^^^^ - -If a chart uses sub charts, there can be overrides in the parent -chart's ``values.yaml`` file, if namespaced to the sub chart. -Example: if chart ``parent`` includes chart ``child``, and -``child``'s ``values.yaml`` has a default value ``foo: bar``, and the -``parent`` chart's ``values.yaml`` has a value - -.. code:: yaml - - child: - foo: baz - -then the value that will be used for ``foo`` by default is ``baz`` when you install the parent chart. - -Note that if you ``helm install parent`` but wish to override values for ``child``, you need to pass them as above, indented underneath ``child:`` as above. - -Multiple overrides -^^^^^^^^^^^^^^^^^^^^ - -If ``-f `` is used multiple times, the last file wins in case keys exist -multiple times (there is no merge performed between multiple files passed to `-f`). -This can lead to unexpected results. If you use multiple files with `-f`, ensure they don't overlap. diff --git a/docs/src/understand/index.md b/docs/src/understand/index.md new file mode 100644 index 0000000000..f7ca56369a --- /dev/null +++ b/docs/src/understand/index.md @@ -0,0 +1,17 @@ +(understand)= + +# Understanding wire-server components + +This section is almost empty, more documentation will come soon... + +```{toctree} +:glob: true +:maxdepth: 1 + +Overview +Audio/video calling, restund servers (TURN/STUN) +Conference Calling 2.0 (SFT) +Minio +Helm +Federation +``` diff --git a/docs/src/understand/index.rst b/docs/src/understand/index.rst deleted file mode 100644 index 3cca9519a8..0000000000 --- a/docs/src/understand/index.rst +++ /dev/null @@ -1,17 +0,0 @@ -.. _understand: - -Understanding wire-server components -==================================== - -This section is almost empty, more documentation will come soon... - -.. toctree:: - :maxdepth: 1 - :glob: - - Overview - Audio/video calling, restund servers (TURN/STUN) - Conference Calling 2.0 (SFT) - Minio - Helm - Federation diff --git a/docs/src/understand/minio.rst b/docs/src/understand/minio.md similarity index 86% rename from docs/src/understand/minio.rst rename to docs/src/understand/minio.md index 0c8fb60c38..afd4e1cd27 100644 --- a/docs/src/understand/minio.rst +++ b/docs/src/understand/minio.md @@ -1,10 +1,8 @@ -Minio -====== +# Minio -Official minio documentation available: ``_ +Official minio documentation available: [https://docs.min.io/](https://docs.min.io/) -Minio philosophy ------------------ +## Minio philosophy Minio clusters are configured with a fixed size once, and cannot be resized afterwards. It is thus important to make a good conservative estimate about @@ -23,8 +21,7 @@ cluster is starting to get full, you will need to set up a parallel bigger cluster, mirror everything to the new cluster, swap the DNS entries to the new one, and then decommission the old one. -Hurdles from the trenches: disk usage statistics; directories vs. disks ------------------------------------------------------------------------ +## Hurdles from the trenches: disk usage statistics; directories vs. disks I have done some more go code reading and have solved more minio mysteries. tl;dr: if you want to be safe, run minio on disks, not @@ -35,7 +32,7 @@ to figure out the amount of available blocks. If it's not a mount directory, it will just call `du .` in a for loop and update some counter (which sounds like a bad strategy to me). -https://github.com/minio/minio/blob/e6d8e272ced8b54872c6df1ef2ad556092280224/cmd/posix.go#L320-L352 + so the answer is: if you use minio, e.g. with mountpoints, it will silently do the right thing and if you configure it to use two directories on the same diff --git a/docs/src/understand/notes/port-ranges.md b/docs/src/understand/notes/port-ranges.md new file mode 100644 index 0000000000..94191336da --- /dev/null +++ b/docs/src/understand/notes/port-ranges.md @@ -0,0 +1,36 @@ +--- +orphan: true +--- + +(port-ranges)= + +# Note on port ranges + +Some parts of Wire (SFT, Restund) related to conference calling and Audio/Video, establish outgoing connections in a range of UDP ports. Which ports are used is determined by the kernel using `/proc/sys/net/ipv4/ip_local_port_range`. + +The /proc/sys/net/ipv4/ip_local_port_range defines the local port range that is used by TCP and UDP traffic to choose the local port. + +You will see in the parameters of this file two numbers: The first number is the first local port allowed for TCP and UDP traffic on the server, the second is the last local port number. + +When setting up firewall rules, this entire range must be allowed for both UDP and TCP. + +This range is defined by the system, and is set by the `/proc/sys/net/ipv4/ip_local_port_range` parameter. + +You read this range for your system by running the following command: + +```bash +cat /proc/sys/net/ipv4/ip_local_port_range +``` + +Or by finding the following line in your `/etc/sysctl.conf` file, if it exists: + +``` +# Allowed local port range +net.ipv4.ip_local_port_range = 32768 61000 +``` + +To change the range, edit the `/etc/sysctl.conf` file or run the following command: + +```bash +echo "32768 61001" > /proc/sys/net/ipv4/ip_local_port_range +``` diff --git a/docs/src/understand/notes/port-ranges.rst b/docs/src/understand/notes/port-ranges.rst deleted file mode 100644 index 0d2cc4e13b..0000000000 --- a/docs/src/understand/notes/port-ranges.rst +++ /dev/null @@ -1,36 +0,0 @@ -:orphan: - -.. _port-ranges: - -Note on port ranges -=================== - -Some parts of Wire (SFT, Restund) related to conference calling and Audio/Video, establish outgoing connections in a range of UDP ports. Which ports are used is determined by the kernel using ``/proc/sys/net/ipv4/ip_local_port_range``. - -The /proc/sys/net/ipv4/ip_local_port_range defines the local port range that is used by TCP and UDP traffic to choose the local port. - -You will see in the parameters of this file two numbers: The first number is the first local port allowed for TCP and UDP traffic on the server, the second is the last local port number. - -When setting up firewall rules, this entire range must be allowed for both UDP and TCP. - -This range is defined by the system, and is set by the ``/proc/sys/net/ipv4/ip_local_port_range`` parameter. - -You read this range for your system by running the following command: - -.. code-block:: bash - - cat /proc/sys/net/ipv4/ip_local_port_range - -Or by finding the following line in your ``/etc/sysctl.conf`` file, if it exists: - -.. code-block:: - - # Allowed local port range - net.ipv4.ip_local_port_range = 32768 61000 - -To change the range, edit the ``/etc/sysctl.conf`` file or run the following command: - -.. code-block:: bash - - echo "32768 61001" > /proc/sys/net/ipv4/ip_local_port_range - diff --git a/docs/src/understand/overview.md b/docs/src/understand/overview.md new file mode 100644 index 0000000000..56f203f707 --- /dev/null +++ b/docs/src/understand/overview.md @@ -0,0 +1,143 @@ +(overview)= + +# Overview + +## Introduction + +In a simplified way, the server components for Wire involve the following: + +```{image} img/architecture-server-simplified.png +``` + +The Wire clients (such as the Wire app on your phone) connect either directly (or via a load balancer) to the "Wire Server". By "Wire Server" we mean multiple API server components that connect to each other, and which also connect to a few databases. Both the API components and the databases are each in a "cluster", which means copies of the same program code runs multiple times. This allows any one component to fail without users noticing that there is a problem (also called +"high-availability"). + +## Architecture and networking + +Note that the webapp, account pages, and team-settings, while in a way not part of the backend, +are installed with the rest and therefore included. + +### Focus on internet protocols + +```{image} ./img/architecture-tls-on-prem-2020-09.png +``` + +### Focus on high-availability + +The following diagram shows a usual setup with multiple VMs (Virtual Machines): + +```{image} ../how-to/install/img/architecture-server-ha.png +``` + +Wire clients (such as the Wire app on your phone) connect to a load balancer. + +The load balancer forwards traffic to the ingress inside the kubernetes VMs. (Restund is special, see {ref}`understand-restund` for details on how Restund works.) + +The nginx ingress pods inside kubernetes look at incoming traffic, and forward that traffic on to the right place, depending on what's inside the URL passed. For example, if a request comes in for `https://example-https.example.com`, it is forwarded to a component called `nginz`, which is the main entry point for the [wire-server API](https://github.com/wireapp/wire-server). If, however, a request comes in for `https://webapp.example.com`, it is forwarded to a component called [webapp](https://github.com/wireapp/wire-webapp), which hosts the graphical browser Wire client (as found when you open [https://app.wire.com](https://app.wire.com)). + +Wire-server needs a range of databases. Their names are: cassandra, elasticsearch, minio, redis, etcd. + +All the server components on one physical machine can connect to all the databases (also those on a different physical machine). The databases each connect to each-other, e.g. cassandra on machine 1 will connect to the cassandra VMs on machines 2 and 3. + +### Backend components startup + +The Wire server backend is designed to run on a kubernetes cluster. From a high level perspective the startup sequence from machine power-on to the Wire server being ready to receive requests is as follow: + +1. *Kubernetes node power on*. Systemd starts the kubelet service which makes the worker node available to kubernetes. For more details about kubernetes startup refer to [the official kubernetes documentation](https://kubernetes.io/docs/reference/setup-tools/kubeadm/implementation-details/). For details about the installation and configuration of kubernetes and worker nodes for Wire server see {ref}`Installing kubernetes and databases on VMs with ansible ` +2. *Kubernetes workload startup*. Kubernetes will ensure that Wire server workloads installed via helm are scheduled on available worker nodes. For more details about workload scheduling refer to [the official kubernetes documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/kube-scheduler/). For details about how to install Wire server with helm refer to {ref}`Installing wire-server (production) components using Helm `. +3. *Stateful workload startup*. Systemd starts the stateful services (cassandra, elasticsearch and minio). See for instance [ansible-cassandra role](https://github.com/wireapp/ansible-cassandra/blob/master/tasks/systemd.yml#L10) and other database installation instructions in {ref}`Installing kubernetes and databases on VMs with ansible ` +4. *Other services*. Systemd starts the restund docker container. See [ansible-restund role](https://github.com/wireapp/ansible-restund/blob/9807313a7c72ffa40e74f69d239404fd87db65ab/templates/restund.service.j2#L12-L19). For details about docker container startup [consult the official documentation](https://docs.docker.com/get-started/overview/#docker-architecture) + +```{note} +For more information about Virual Machine startup or operating system level service startup, please consult your virtualisation and operating system documentation. +``` + +### Focus on pods + +The Wire backend runs in [a kubernetes cluster](https://kubernetes.io/), with different components running in different [pods](https://kubernetes.io/docs/concepts/workloads/pods/). + +This is a list of those pods as found in a typical installation. + +HTTPS Entry points: + +- `nginx-ingress-controller-controller`: [Ingress](https://kubernetes.github.io/ingress-nginx/) exposes HTTP and HTTPS routes from outside the cluster to services within the cluster. +- `nginx-ingress-controller-default-backend`: [The default backend](https://kubernetes.github.io/ingress-nginx/user-guide/default-backend/) is a service which handles all URL paths and hosts the nginx controller doesn't understand (i.e., all the requests that are not mapped with an Ingress), that is 404 pages. Part of `nginx-ingress`. + +Frontend pods: + +- `webapp`: The fully functioning Web client (like ). [This pod](https://github.com/wireapp/wire-docs/blob/master/src/how-to/install/helm.rst#what-will-be-installed) serves the web interface itself, which then interfaces with other services/pods, such as the APIs. +- `account-pages`: [This pod](https://github.com/wireapp/wire-docs/blob/master/src/how-to/install/helm.rst#what-will-be-installed) serves Web pages for user account management (a few pages relating to e.g. password reset). +- `team-settings`: Team management Web interface (like ). + +Pods with an HTTP API: + +- `brig`: [The user management API service](https://github.com/wireapp/wire-server/tree/develop/services/brig). Connects to `cassandra` and `elastisearch` for user data storage, sends emails and SMS for account validation. +- `cannon`: [WebSockets API Service](https://github.com/wireapp/wire-server/blob/develop/services/cannon/). Holds WebSocket connections. +- `cargohold`: [Asset Storage API Service](https://docs.wire.com/how-to/install/aws-prod.html). Amazon-AWS-S3-style services are used by `cargohold` to store encrypted files that users are sharing amongst each other, such as images, files, and other static content, which we call assets. All assets except profile pictures are symmetrically encrypted before storage (and the keys are only known to the participants of the conversation in which an assets was shared - servers have no knowledge of the keys). +- `galley`: [Conversations and Teams API Service](https://docs.wire.com/understand/api-client-perspective/index.html). Data is stored in cassandra. Uses `gundeck` to send notifications to users. +- `nginz`: Public API Reverse Proxy (Nginx with custom libzauth module). A modified copy of nginx, compiled with a specific set of upstream extra modules, and one important additional module zauth_nginx_module. Responsible for user authentication validation. Forwards traffic to all other API services (except federator) +- `spar`: [Single Sign On (SSO)](https://en.wikipedia.org/wiki/Single_sign-on) and [SCIM](https://en.wikipedia.org/wiki/System_for_Cross-domain_Identity_Management). Stores data in cassandra. +- `gundeck`: Push Notification Hub (WebSocket/mobile push notifications). Uses redis as a temporary data store for websocket presences. Uses Amazon SNS and SQS. +- `federator`: [Connects different wire installations together](https://docs.wire.com/understand/federation/index.html). Wire Federation, once implemented, aims to allow multiple Wire-server backends to federate with each other. That means that a user 1 registered on backend A and a user 2 registered on backend B should be able to interact with each other as if they belonged to the same backend. + +Supporting pods and data storage: + +- `cassandra-ephemeral` (or `cassandra-external`): [NoSQL Database management system](https://github.com/wireapp/wire-server/tree/develop/charts/cassandra-ephemeral) (). Everything stateful in wire-server (cassandra is used by `brig`, `galley`, `gundeck` and `spar`) is stored in cassandra. + \* `cassandra-ephemeral` is for test clusters where persisting the data (i.e. loose users, conversations,...) does not matter, but this shouldn't be used in production environments. + \* `cassandra-external` is used to point to an external cassandra cluster which is installed outside of Kubernetes. +- `demo-smtp`: In "demo" installations, used to replace a proper external SMTP server for the sending of emails (for example verification codes). In production environments, an actual SMTP server is used directly instead of this pod. () +- `fluent-bit`: A log processor and forwarder, allowing collection of data such as metrics and logs from different sources. Not typically deployed. () +- `elastisearch-ephemeral` (or `elastisearch-external`): [Distributed search and analytics engines, stores some user information (name, handle, userid, teamid)](https://github.com/wireapp/wire-server/tree/develop/charts/elastisearch-external). Information is duplicated here from cassandra to allow searching for users. Information here can be re-populated from data in cassandra (albeit with some downtime for search functionality) (). + \* `elastisearch-ephemeral` is for test clusters where persisting the data doesn't matter. + \* `elastisearch-external` refers to elasticsearch IPs located outside kubernetes by specifying IPs manually. +- `fake-aws-s3`: Amazon-AWS-S3-compatible object storage using MinIO (), used by cargohold to store (encrypted) assets such as files, posted images, profile pics, etc. +- `fake-aws-s3-reaper`: Creates the default S3 bucket inside fake-aws-s3. +- `fake-aws-sns`. [Amazon Simple Notification Service (Amazon SNS)](https://docs.aws.amazon.com/AmazonS3/latest/userguide/NotificationHowTo.html), used to push messages to mobile devices or distributed services. SNS can publish a message once, and deliver it one or more times. +- `fake-aws-sqs`: [Amazon Simple Queue Service (Amazon SQS) queue](https://docs.aws.amazon.com/AmazonS3/latest/userguide/NotificationHowTo.html), used to transmit any volume of data without requiring other services to be always available. +- `redis-ephemeral`: Stores websocket connection assignments (part of the `gundeck` / `cannon` architecture). + +Short running jobs that run during installation/upgrade (these should usually be in the status 'Completed' except immediately after installation/upgrade): + +- `cassandra-migrations`: Used to initialize or upgrade the database schema in cassandra (for example when the software is upgraded to a new version). +- `galley-migrate-data`: Used to upgrade data in `cassandra` when the data model changes (for example when the software is upgraded to a new version). +- `brig-index-migrate-data`: Used to upgrade data in `cassandra` when the data model changes in brig (for example when the software is upgraded to a new version) +- `elastisearch-index-create`: [Creates](https://github.com/wireapp/wire-server/blob/develop/charts/elasticsearch-index/templates/create-index.yaml#L29) an Elastisearch index for brig. +- `spar-migrate-data`: [Used to update spar data](https://github.com/wireapp/wire-server/blob/develop/charts/cassandra-migrations/templates/spar-migrate-data.yaml) in cassandra when schema changes occur. + +As an example, this is the result of running the `kubectl get pods --namespace wire` command to obtain a list of all pods in a typical cluster: + +```shell +NAMESPACE NAME READY STATUS RESTARTS AGE +wire account-pages-54bfcb997f-hwxlf 1/1 Running 0 85d +wire brig-58bc7f844d-rp2mx 1/1 Running 0 3h54m +wire brig-index-migrate-data-s7lmf 0/1 Completed 0 3h33m +wire cannon-0 1/1 Running 0 3h53m +wire cargohold-779bff9fc6-7d9hm 1/1 Running 0 3h54m +wire cassandra-ephemeral-0 1/1 Running 0 176d +wire cassandra-migrations-66n8d 0/1 Completed 0 3h34m +wire demo-smtp-784ddf6989-7zvsk 1/1 Running 0 176d +wire elasticsearch-ephemeral-86f4b8ff6f-fkjlk 1/1 Running 0 176d +wire elasticsearch-index-create-l5zbr 0/1 Completed 0 3h34m +wire fake-aws-s3-77d9447b8f-9n4fj 1/1 Running 0 176d +wire fake-aws-s3-reaper-78d9f58dd4-kf582 1/1 Running 0 176d +wire fake-aws-sns-6c7c4b7479-nzfj2 2/2 Running 0 176d +wire fake-aws-sqs-59fbfbcbd4-ptcz6 2/2 Running 0 176d +wire federator-6d7b66f4d5-xgkst 1/1 Running 0 3h54m +wire galley-5b47f7ff96-m9zrs 1/1 Running 0 3h54m +wire galley-migrate-data-97gn8 0/1 Completed 0 3h33m +wire gundeck-76c4599845-4f4pd 1/1 Running 0 3h54m +wire nginx-ingress-controller-controller-2nbkq 1/1 Running 0 9d +wire nginx-ingress-controller-controller-8ggw2 1/1 Running 0 9d +wire nginx-ingress-controller-default-backend-dd5c45cf-jlmbl 1/1 Running 0 176d +wire nginz-77d7586bd9-vwlrh 2/2 Running 0 3h54m +wire redis-ephemeral-master-0 1/1 Running 0 176d +wire spar-8576b6845c-npb92 1/1 Running 0 3h54m +wire spar-migrate-data-lz5ls 0/1 Completed 0 3h33m +wire team-settings-86747b988b-5rt45 1/1 Running 0 50d +wire webapp-54458f756c-r7l6x 1/1 Running 0 3h54m + 1/1 Running 0 3h54m +``` + +```{note} +This list is not exhaustive, and your installation may have additional pods running depending on your configuration. +``` diff --git a/docs/src/understand/overview.rst b/docs/src/understand/overview.rst deleted file mode 100644 index 71d2f2a45d..0000000000 --- a/docs/src/understand/overview.rst +++ /dev/null @@ -1,148 +0,0 @@ -Overview -======== - -Introduction ------------- - -In a simplified way, the server components for Wire involve the following: - -|arch-simplified| - -The Wire clients (such as the Wire app on your phone) connect either directly (or via a load balancer) to the "Wire Server". By "Wire Server" we mean multiple API server components that connect to each other, and which also connect to a few databases. Both the API components and the databases are each in a "cluster", which means copies of the same program code runs multiple times. This allows any one component to fail without users noticing that there is a problem (also called -"high-availability"). - -Architecture and networking ----------------------------- - -Note that the webapp, account pages, and team-settings, while in a way not part of the backend, -are installed with the rest and therefore included. - -Focus on internet protocols -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -|arch-proto| - - -Focus on high-availability -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The following diagram shows a usual setup with multiple VMs (Virtual Machines): - -|arch-ha| - -Wire clients (such as the Wire app on your phone) connect to a load balancer. - -The load balancer forwards traffic to the ingress inside the kubernetes VMs. (Restund is special, see :ref:`understand-restund` for details on how Restund works.) - -The nginx ingress pods inside kubernetes look at incoming traffic, and forward that traffic on to the right place, depending on what's inside the URL passed. For example, if a request comes in for ``https://example-https.example.com``, it is forwarded to a component called ``nginz``, which is the main entry point for the `wire-server API `__. If, however, a request comes in for ``https://webapp.example.com``, it is forwarded to a component called `webapp `__, which hosts the graphical browser Wire client (as found when you open ``__). - -Wire-server needs a range of databases. Their names are: cassandra, elasticsearch, minio, redis, etcd. - -All the server components on one physical machine can connect to all the databases (also those on a different physical machine). The databases each connect to each-other, e.g. cassandra on machine 1 will connect to the cassandra VMs on machines 2 and 3. - -Backend components startup -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The Wire server backend is designed to run on a kubernetes cluster. From a high level perspective the startup sequence from machine power-on to the Wire server being ready to receive requests is as follow: - -1. *Kubernetes node power on*. Systemd starts the kubelet service which makes the worker node available to kubernetes. For more details about kubernetes startup refer to `the official kubernetes documentation `__. For details about the installation and configuration of kubernetes and worker nodes for Wire server see :ref:`Installing kubernetes and databases on VMs with ansible ` -2. *Kubernetes workload startup*. Kubernetes will ensure that Wire server workloads installed via helm are scheduled on available worker nodes. For more details about workload scheduling refer to `the official kubernetes documentation `__. For details about how to install Wire server with helm refer to :ref:`Installing wire-server (production) components using Helm `. -3. *Stateful workload startup*. Systemd starts the stateful services (cassandra, elasticsearch and minio). See for instance `ansible-cassandra role `__ and other database installation instructions in :ref:`Installing kubernetes and databases on VMs with ansible ` -4. *Other services*. Systemd starts the restund docker container. See `ansible-restund role `__. For details about docker container startup `consult the official documentation `__ - -.. note:: - For more information about Virual Machine startup or operating system level service startup, please consult your virtualisation and operating system documentation. - -.. |arch-simplified| image:: img/architecture-server-simplified.png -.. |arch-proto| image:: ./img/architecture-tls-on-prem-2020-09.png -.. |arch-ha| image:: ../how-to/install/img/architecture-server-ha.png - -Focus on pods -~~~~~~~~~~~~~ - -The Wire backend runs in `a kubernetes cluster `__, with different components running in different `pods `__. - -This is a list of those pods as found in a typical installation. - -HTTPS Entry points: - -* ``nginx-ingress-controller-controller``: `Ingress `__ exposes HTTP and HTTPS routes from outside the cluster to services within the cluster. -* ``nginx-ingress-controller-default-backend``: `The default backend `__ is a service which handles all URL paths and hosts the nginx controller doesn't understand (i.e., all the requests that are not mapped with an Ingress), that is 404 pages. Part of ``nginx-ingress``. - -Frontend pods: - -* ``webapp``: The fully functioning Web client (like https://app.wire.com). `This pod `__ serves the web interface itself, which then interfaces with other services/pods, such as the APIs. -* ``account-pages``: `This pod `__ serves Web pages for user account management (a few pages relating to e.g. password reset). -* ``team-settings``: Team management Web interface (like https://teams.wire.com). - -Pods with an HTTP API: - -* ``brig``: `The user management API service `__. Connects to ``cassandra`` and ``elastisearch`` for user data storage, sends emails and SMS for account validation. -* ``cannon``: `WebSockets API Service `__. Holds WebSocket connections. -* ``cargohold``: `Asset Storage API Service `__. Amazon-AWS-S3-style services are used by ``cargohold`` to store encrypted files that users are sharing amongst each other, such as images, files, and other static content, which we call assets. All assets except profile pictures are symmetrically encrypted before storage (and the keys are only known to the participants of the conversation in which an assets was shared - servers have no knowledge of the keys). -* ``galley``: `Conversations and Teams API Service `__. Data is stored in cassandra. Uses ``gundeck`` to send notifications to users. -* ``nginz``: Public API Reverse Proxy (Nginx with custom libzauth module). A modified copy of nginx, compiled with a specific set of upstream extra modules, and one important additional module zauth_nginx_module. Responsible for user authentication validation. Forwards traffic to all other API services (except federator) -* ``spar``: `Single Sign On (SSO) `__ and `SCIM `__. Stores data in cassandra. -* ``gundeck``: Push Notification Hub (WebSocket/mobile push notifications). Uses redis as a temporary data store for websocket presences. Uses Amazon SNS and SQS. -* ``federator``: `Connects different wire installations together `__. Wire Federation, once implemented, aims to allow multiple Wire-server backends to federate with each other. That means that a user 1 registered on backend A and a user 2 registered on backend B should be able to interact with each other as if they belonged to the same backend. - -Supporting pods and data storage: - -* ``cassandra-ephemeral`` (or ``cassandra-external``): `NoSQL Database management system `__ (https://en.wikipedia.org/wiki/Apache_Cassandra). Everything stateful in wire-server (cassandra is used by ``brig``, ``galley``, ``gundeck`` and ``spar``) is stored in cassandra. - * ``cassandra-ephemeral`` is for test clusters where persisting the data (i.e. loose users, conversations,...) does not matter, but this shouldn't be used in production environments. - * ``cassandra-external`` is used to point to an external cassandra cluster which is installed outside of Kubernetes. -* ``demo-smtp``: In "demo" installations, used to replace a proper external SMTP server for the sending of emails (for example verification codes). In production environments, an actual SMTP server is used directly instead of this pod. (https://github.com/namshi/docker-smtp) -* ``fluent-bit``: A log processor and forwarder, allowing collection of data such as metrics and logs from different sources. Not typically deployed. (https://fluentbit.io/) -* ``elastisearch-ephemeral`` (or ``elastisearch-external``): `Distributed search and analytics engines, stores some user information (name, handle, userid, teamid) `__. Information is duplicated here from cassandra to allow searching for users. Information here can be re-populated from data in cassandra (albeit with some downtime for search functionality) (https://www.elastic.co/what-is/elasticsearch). - * ``elastisearch-ephemeral`` is for test clusters where persisting the data doesn't matter. - * ``elastisearch-external`` refers to elasticsearch IPs located outside kubernetes by specifying IPs manually. -* ``fake-aws-s3``: Amazon-AWS-S3-compatible object storage using MinIO (https://min.io/), used by cargohold to store (encrypted) assets such as files, posted images, profile pics, etc. -* ``fake-aws-s3-reaper``: Creates the default S3 bucket inside fake-aws-s3. -* ``fake-aws-sns``. `Amazon Simple Notification Service (Amazon SNS) `__, used to push messages to mobile devices or distributed services. SNS can publish a message once, and deliver it one or more times. -* ``fake-aws-sqs``: `Amazon Simple Queue Service (Amazon SQS) queue `__, used to transmit any volume of data without requiring other services to be always available. -* ``redis-ephemeral``: Stores websocket connection assignments (part of the ``gundeck`` / ``cannon`` architecture). - -Short running jobs that run during installation/upgrade (these should usually be in the status 'Completed' except immediately after installation/upgrade): - -* ``cassandra-migrations``: Used to initialize or upgrade the database schema in cassandra (for example when the software is upgraded to a new version). -* ``galley-migrate-data``: Used to upgrade data in ``cassandra`` when the data model changes (for example when the software is upgraded to a new version). -* ``brig-index-migrate-data``: Used to upgrade data in ``cassandra`` when the data model changes in brig (for example when the software is upgraded to a new version) -* ``elastisearch-index-create``: `Creates `__ an Elastisearch index for brig. -* ``spar-migrate-data``: `Used to update spar data `__ in cassandra when schema changes occur. - -As an example, this is the result of running the ``kubectl get pods --namespace wire`` command to obtain a list of all pods in a typical cluster: - -.. code:: shell - - NAMESPACE NAME READY STATUS RESTARTS AGE - wire account-pages-54bfcb997f-hwxlf 1/1 Running 0 85d - wire brig-58bc7f844d-rp2mx 1/1 Running 0 3h54m - wire brig-index-migrate-data-s7lmf 0/1 Completed 0 3h33m - wire cannon-0 1/1 Running 0 3h53m - wire cargohold-779bff9fc6-7d9hm 1/1 Running 0 3h54m - wire cassandra-ephemeral-0 1/1 Running 0 176d - wire cassandra-migrations-66n8d 0/1 Completed 0 3h34m - wire demo-smtp-784ddf6989-7zvsk 1/1 Running 0 176d - wire elasticsearch-ephemeral-86f4b8ff6f-fkjlk 1/1 Running 0 176d - wire elasticsearch-index-create-l5zbr 0/1 Completed 0 3h34m - wire fake-aws-s3-77d9447b8f-9n4fj 1/1 Running 0 176d - wire fake-aws-s3-reaper-78d9f58dd4-kf582 1/1 Running 0 176d - wire fake-aws-sns-6c7c4b7479-nzfj2 2/2 Running 0 176d - wire fake-aws-sqs-59fbfbcbd4-ptcz6 2/2 Running 0 176d - wire federator-6d7b66f4d5-xgkst 1/1 Running 0 3h54m - wire galley-5b47f7ff96-m9zrs 1/1 Running 0 3h54m - wire galley-migrate-data-97gn8 0/1 Completed 0 3h33m - wire gundeck-76c4599845-4f4pd 1/1 Running 0 3h54m - wire nginx-ingress-controller-controller-2nbkq 1/1 Running 0 9d - wire nginx-ingress-controller-controller-8ggw2 1/1 Running 0 9d - wire nginx-ingress-controller-default-backend-dd5c45cf-jlmbl 1/1 Running 0 176d - wire nginz-77d7586bd9-vwlrh 2/2 Running 0 3h54m - wire redis-ephemeral-master-0 1/1 Running 0 176d - wire spar-8576b6845c-npb92 1/1 Running 0 3h54m - wire spar-migrate-data-lz5ls 0/1 Completed 0 3h33m - wire team-settings-86747b988b-5rt45 1/1 Running 0 50d - wire webapp-54458f756c-r7l6x 1/1 Running 0 3h54m - 1/1 Running 0 3h54m -.. note:: - - This list is not exhaustive, and your installation may have additional pods running depending on your configuration. diff --git a/docs/src/understand/restund.rst b/docs/src/understand/restund.md similarity index 61% rename from docs/src/understand/restund.rst rename to docs/src/understand/restund.md index 35014c28bf..0cb8dd6f6d 100644 --- a/docs/src/understand/restund.rst +++ b/docs/src/understand/restund.md @@ -1,25 +1,22 @@ -.. _understand-restund: +(understand-restund)= -Restund (TURN) servers -======================== +# Restund (TURN) servers -Introduction -~~~~~~~~~~~~ +## Introduction Restund servers allow two users on different networks (for example Alice who is in an office connected to an office router and Bob who is at home connected to a home router) to have a Wire audio or video call. More precisely: - Restund is a modular and flexible - `STUN `__ and - `TURN `__ - Server, with IPv4 and IPv6 support. +> Restund is a modular and flexible +> [STUN](https://en.wikipedia.org/wiki/STUN) and +> [TURN](https://en.wikipedia.org/wiki/Traversal_Using_Relays_around_NAT) +> Server, with IPv4 and IPv6 support. -.. _architecture-restund: +(architecture-restund)= -Architecture -~~~~~~~~~~~~ +## Architecture Since the restund servers help establishing a connection between two users, they need to be reachable by both of these users, which usually @@ -32,29 +29,28 @@ Restund instance may communicate with other Restund instances. You can either have restund servers directly exposed to the public internet: -|architecture-restund| +```{image} img/architecture-restund.png +``` Or you can have them reachable by fronting them with a firewall or load balancer machine that may have a different IP than the server where restund is installed: -|architecture-restund-lb| +```{image} img/architecture-restund-lb.png +``` -What is it used for -~~~~~~~~~~~~~~~~~~~ +## What is it used for Restund is used to assist in NAT-traversal. Its goal is to connect two clients who are (possibly both) behind NAT directly in a peer to peer fashion, for optimal call quality and lowest latency. - client A sends a UDP packet to Restund; which will get address-translated by the router. Restund then sends back to the client what the source IP and the source port was that Restund observed. If the client then communicates this to Client B, Client B will be able to send data to that IP,port pair over UDP if it does so quickly enough. Client A and B will then have a peer-to-peer leg. - This is not always possible (e.g. symmetric NAT makes this technique impossible, as the router will NAT a different source-port for each connection). In that case clients fall back to TURN, which asks Restund to @@ -63,17 +59,16 @@ allocate a relay address which relays packets between nodes A and B. Restund servers need to have a wide range of ports open to allocate such relay addresses. -Network -~~~~~~~ +## Network As briefly mentioned above, a TURN server functions as a bridge between networks. Networks which don't have a direct route defined between them, usually have distinct address blocks. Depending on the address block they are configured with - such block is either considered to be *public* or *private* -(aka special-purpose addresses `[RFC 6890] `__) +(aka special-purpose addresses [\[RFC 6890\]](https://tools.ietf.org/html/rfc6890)) -- `IPv4 private blocks `__ -- `IPv6 private blocks `__ +- [IPv4 private blocks](https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml) +- [IPv6 private blocks](https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml) In cases where a machine, that is hosting the TURN server, also connects to a *private* network in which other services are running, chances are @@ -81,56 +76,51 @@ that these services are being indirectly exposed through that TURN server. To prevent this kind of exposure, a TURN server has to be configured with an inclusive or exclusive list of address blocks to prevents undesired connections from being -established [1]_. At the moment (Feb. 2021), this functionality is not yet available +established [^footnote-1]. At the moment (Feb. 2021), this functionality is not yet available with *Restund* on the application-level. Instead, the system-level firewall capabilities -must be utilized. The `IP ranges `__ -mentioned in the article [1]_ should be blocked for egress and, depending on the scenario, -also for ingress traffic. Tools like ``iptables`` or ``ufw`` can be used to set this up. - -.. [1] `Details about CVE-2020-26262, bypass of Coturn's default access control protection `__ +must be utilized. The [IP ranges](https://www.rtcsec.com/post/2021/01/details-about-cve-2020-26262-bypass-of-coturns-default-access-control-protection/#further-concerns-what-else) +mentioned in the article [^footnote-1] should be blocked for egress and, depending on the scenario, +also for ingress traffic. Tools like `iptables` or `ufw` can be used to set this up. +[^footnote-1]: [Details about CVE-2020-26262, bypass of Coturn's default access control protection](https://www.rtcsec.com/post/2021/01/details-about-cve-2020-26262-bypass-of-coturns-default-access-control-protection/) -.. _understand-restund-protocal-and-ports: +(understand-restund-protocal-and-ports)= -Protocols and open ports -~~~~~~~~~~~~~~~~~~~~~~~~ +## Protocols and open ports Restund servers provide the best audio/video connections if end-user devices -can connect to them via UDP. +can connect to them via UDP. -In this case, a firewall (if any) needs to allow and/or forward the complete :ref:`default port range ` for incoming UDP traffic. +In this case, a firewall (if any) needs to allow and/or forward the complete {ref}`default port range ` for incoming UDP traffic. -Ports for allocations are allocated from the :ref:`default port range `, for more information on this port range, how to read and change it, and how to configure your firewall, see :ref:`this note `. +Ports for allocations are allocated from the {ref}`default port range `, for more information on this port range, how to read and change it, and how to configure your firewall, see {ref}`this note `. -In case e.g. office firewall rules disallow UDP traffic in this range, there is a possibility to use TCP instead, at the expense of call quality. +In case e.g. office firewall rules disallow UDP traffic in this range, there is a possibility to use TCP instead, at the expense of call quality. -Port ``3478`` is the default control port, +Port `3478` is the default control port, however one UDP port per active connection is required, so a whole port range must be available and reachable from the outside. -If *Conference Calling 2.0* (:ref:`SFT `) is enabled, a Restund instance, -additionally, must be allowed to communicate with ::ref:`SFT instances ` +If *Conference Calling 2.0* ({ref}`SFT `) is enabled, a Restund instance, +additionally, must be allowed to communicate with :{ref}`SFT instances ` on the same UDP ports mentioned above. In this scenario a Restund server becomes sort of a proxy for the client, if the client is not able to establish a media channel between itself and the SFT server. -*For more information, please refer to the source code of the Ansible role:* `restund `__. +*For more information, please refer to the source code of the Ansible role:* [restund](https://github.com/wireapp/ansible-restund/blob/master/tasks/firewall.yml). -Control ports -^^^^^^^^^^^^^ +### Control ports -Restund listens for control messages on port ``3478`` on both UDP and TCP. It -also can listen on port ``5349`` which uses TLS. One can reconfigure both ports. -For example, port ``5349`` can be reconfigured to be port ``443``; so that TURN +Restund listens for control messages on port `3478` on both UDP and TCP. It +also can listen on port `5349` which uses TLS. One can reconfigure both ports. +For example, port `5349` can be reconfigured to be port `443`; so that TURN traffic can not be distinguished from any other TLS traffic. This might help with overcoming certain firewall restrictions. You can instead use (if that's -easier with firewall rules) for example ports ``80`` and ``443`` (requires to +easier with firewall rules) for example ports `80` and `443` (requires to run restund as root) or do a redirect from a load balancer (if using one) to -redirect ``443 -> 5349`` and ``80 -> 3478``. +redirect `443 -> 5349` and `80 -> 3478`. - -Amount of users and file descriptors -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +## Amount of users and file descriptors Each allocation (active connection by one participant) requires 1 or 2 file descriptors, so ensure you increase your file descriptor limits in @@ -140,33 +130,27 @@ Currently one restund server can have a maximum of 64000 allocations. If you have more users than that in an active call, you need to deploy more restund servers. -Load balancing and high-availability -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +## Load balancing and high-availability Load balancing is not possible, since STUN/TURN is a stateful protocol, -so UDP packets addressed to ``restund server 1``, if by means of a load -balancer were to end up at ``restund server 2``, would get dropped, as +so UDP packets addressed to `restund server 1`, if by means of a load +balancer were to end up at `restund server 2`, would get dropped, as the second server doesn't know the source address. High-availability is nevertheless ensured by having and advertising more than one restund server. Instead of the load balancer, the clients will switch their server if it fails. -Discovery and establishing a call -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +## Discovery and establishing a call A simplified flow of how restund servers, along with the wire-server are used to establish a call: -|flow-restund| +```{image} img/flow-restund.png +``` -DNS -~~~ +## DNS Usually DNS records are used which point to the public IPs of the restund servers (or of the respective firewall or load balancer machines). These DNS names are then used when configuring wire-server. - -.. |architecture-restund| image:: img/architecture-restund.png -.. |architecture-restund-lb| image:: img/architecture-restund-lb.png -.. |flow-restund| image:: img/flow-restund.png diff --git a/docs/src/understand/sft.rst b/docs/src/understand/sft.md similarity index 77% rename from docs/src/understand/sft.rst rename to docs/src/understand/sft.md index aec41fe742..28f2c432d6 100644 --- a/docs/src/understand/sft.rst +++ b/docs/src/understand/sft.md @@ -1,82 +1,76 @@ -.. _understand-sft: +(understand-sft)= -Conference Calling 2.0 (aka SFT) -================================ +# Conference Calling 2.0 (aka SFT) -Background ----------- +## Background Previously, Wire group calls were implemented as a mesh, where each participant was connected to each other in a peer-to-peer fashion. This meant that a client would have to upload their video and audio feeds separately for each participant. This in practice meant that the amount of participants was limited by the upload bandwidth of the clients. -Wire now has a signalling-forwarding unit called `SFT `__ which allows clients to upload once and +Wire now has a signalling-forwarding unit called [SFT](https://github.com/wireapp/wire-avs-service) which allows clients to upload once and then the SFT fans it out to the other clients. Because connections are not end-to-end anymore now, dTLS encryption offered by WebRTC is not enough anymore as the encryption is terminated at the server-side. To avoid Wire from seeing the contents of calls SFT utilises WebRTC InsertibleStreams to encrypt the packets a second time with a group key that is not known to the server. With SFT it is thus possible to have conference calls with many participants without compromising end-to-end security. -.. note:: - We will describe conferencing first in a single domain in this section. - Conferencing in an environment with Federation is described in the - :ref:`federated conferencing` section. +```{note} +We will describe conferencing first in a single domain in this section. +Conferencing in an environment with Federation is described in the +{ref}`federated conferencing` section. +``` - -Architecture ------------- +## Architecture The following diagram is centered around SFT and its role within a calling setup. Restund is seen as a mere client proxy and its relation to and interaction with a client is explained -:ref:`here `. The diagram shows that a call resides on a single SFT instance +{ref}`here `. The diagram shows that a call resides on a single SFT instance and that the instance allocates at least one port for media transport per participant in the call. -.. figure:: img/architecture-sft.png - - SFT signaling, and media sending from the perspective of one caller +```{figure} img/architecture-sft.png +SFT signaling, and media sending from the perspective of one caller +``` - -Establishing a call -------------------- +## Establishing a call 1. *Client A* wants to initiate a call. It contacts all the known SFT servers via HTTPS. The SFT server that is quickest to respond is the one that will be used by the client. - (Request 1: ``CONFCONN``) + (Request 1: `CONFCONN`) 2. *Client A* gathers connection candidates (own public IP, public IP of the network the - client is in with the help of STUN, through TURN servers) [1]_ for the SFT server to + client is in with the help of STUN, through TURN servers) [^footnote-1] for the SFT server to establish a media connection to *Client A*. These information are then being send again - from *Client A* to the chosen SFT server via HTTPS request. (Request 2: ``SETUP``) + from *Client A* to the chosen SFT server via HTTPS request. (Request 2: `SETUP`) 3. The SFT server tests which of the connection candidates actually work. Meaning, it goes through all the candidates until one leads to a successful media connection between itself and *client A* -4. *Client A* sends an end-to-end encrypted message [2]_ ``CONFSTART`` to all members of chat, which contains +4. *Client A* sends an end-to-end encrypted message [^footnote-2] `CONFSTART` to all members of chat, which contains the URL of the SFT server that is being used for the call. 5. Any other client that wants to join the call, does 1. + 2. with the exception of **only** contacting one SFT server i.e. the one that *client A* chose and told all other - potential participants about via ``CONFSTART`` message + potential participants about via `CONFSTART` message At that point a media connection between *client A* and the SFT server has been established, and they continue talking to each other by using the data-channel, which uses the media connection (i.e. no more HTTPS at that point). There are just 2 HTTPS request/response sequences per participant. -.. [1] STUN & TURN are both part of a :ref:`Restund server ` -.. [2] This encrypted message is sent in the same conversation, hidden from user's view but - interpreted by user's clients. It is sent via backend servers and forwarded to other - conversation participants, not to or via SFT. +[^footnote-1]: STUN & TURN are both part of a {ref}`Restund server ` +[^footnote-2]: This encrypted message is sent in the same conversation, hidden from user's view but + interpreted by user's clients. It is sent via backend servers and forwarded to other + conversation participants, not to or via SFT. -Prerequisites -------------- +## Prerequisites For Conference Calling to function properly, clients need to be able to reach the HTTPS interface of the SFT server(s) - either directly or through a load balancer sitting in front of the servers. This is only needed for the call initiation/joining part. Additionally, for the media connection, clients and SFT servers should be able to reach each other -via UDP (see :ref:`Firewall rules `). +via UDP (see {ref}`Firewall rules `). If that is not possible, then at least SFT servers and Restund servers should be able to reach each other via UDP - and clients may connect via UDP and/or TCP to Restund servers -(see :ref:`Protocols and open ports `), which in +(see {ref}`Protocols and open ports `), which in turn will connect to the SFT server. In the unlikely scenario where no UDP is allowed whatsoever or SFT servers may not be able to reach the Restund servers that clients are using to make themselves reachable, an SFT server itself can @@ -90,19 +84,17 @@ Due to this `hostNetwork` limitation only one SFT instance can run per node so i As a rule of thumb you will need 1vCPU of compute per 50 participants. SFT will utilise multiple cores. You can use this rule of thumb to decide how many kubernetes nodes you need to provision. -For more information about capacity planning and networking please refer to the `technical documentation `__ +For more information about capacity planning and networking please refer to the [technical documentation](https://github.com/wireapp/wire-server/blob/eab0ce1ff335889bc5a187c51872dfd0e78cc22b/charts/sftd/README.md) -.. _federated-sft: +(federated-sft)= -Federated Conference Calling -============================ +# Federated Conference Calling -Conferencing in a federated environment assumes that each domain participating in a +Conferencing in a federated environment assumes that each domain participating in a conference will use an SFT in its own domain. The SFT in the caller's domain is called -the `anchor SFT`. +the `anchor SFT`. -Multi-SFT Architecture ----------------------- +## Multi-SFT Architecture With support for federation, each domain participating in a conference is responsible to make available an SFT for users in that domain. The SFT in the domain of the caller is @@ -116,7 +108,7 @@ initiates a call in a federated conversation which contains herself, Adam also i A, and Bob and Beth in domain B. Alice's client first creates a conference and is assigned a conference URL on SFT A2. Because the SFT is configured for federation, it assumes the role of anchor and also returns an IP address and port (the `anchor SFT tuple`) -which can be used by any federated SFTs which need to connect. (Alice sets up her media +which can be used by any federated SFTs which need to connect. (Alice sets up her media connection with SFT A2 as normal). Alice's client forwards the conference URL and the anchor SFT tuple to the other @@ -128,9 +120,9 @@ to the anchor SFT using the anchor SFT tuple and provides the SFT URL. (Bob's cl also sets up media with SFT B1 normally.) At this point all paths are established and the conference call can happen normally. -.. figure:: img/multi-sft-noturn.png - - Basic Multi-SFT conference initiated by Alice in domain A, with Bob in domain B +```{figure} img/multi-sft-noturn.png +Basic Multi-SFT conference initiated by Alice in domain A, with Bob in domain B +``` Because some customers do not wish to expose their SFTs directly to hosts on the public Internet, the SFTs can allocate a port on a TURN server. In this way, only the IP @@ -140,16 +132,16 @@ this scenario. In this configuration, SFT A2 requests an allocation from the fe TURN server in domain A before responding to Alice. The anchor SFT tuple is the address allocated on the federation TURN server in domain A. -.. figure:: img/multi-sft-turn.png - - Multi-SFT conference with TURN servers between federated SFTs +```{figure} img/multi-sft-turn.png +Multi-SFT conference with TURN servers between federated SFTs +``` Finally, for extremely restrictive firewall environments, the TURN servers used for federated SFT traffic can be further secured with a TURN to TURN mutually authenticated DTLS connection. The SFTs allocate a channel inside this DTLS connection per conference. The channel number is included along with the anchor SFT tuple returned to Alice, which Alice shares with the conversation, which Bob sends to SFT B1, -and which SFT B1 uses when forming its DTLS connection to SFT A2. This DTLS connection +and which SFT B1 uses when forming its DTLS connection to SFT A2. This DTLS connection runs on a dedicated port number which is not used for regular TURN traffic. Under this configuration, only that single IP address and port is exposed for each federated TURN server with all SFT traffic multiplexed over the connection. The diagram below shows @@ -157,7 +149,6 @@ this scenario. Note that this TURN DTLS multiplexing is only used for SFT to SF communication into federated group calls, and does not affect the connectivity requirements for normal one-on-one calls. -.. figure:: img/multi-sft-turn-dtls.png - - Multi-SFT conference with federated TURN servers with DTLS multiplexing - +```{figure} img/multi-sft-turn-dtls.png +Multi-SFT conference with federated TURN servers with DTLS multiplexing +``` diff --git a/docs/src/understand/single-sign-on/design.rst b/docs/src/understand/single-sign-on/design.rst deleted file mode 100644 index af2102e363..0000000000 --- a/docs/src/understand/single-sign-on/design.rst +++ /dev/null @@ -1,3 +0,0 @@ -:orphan: - -This page is gone. Please visit `this one <./main.html>`_ diff --git a/docs/src/understand/single-sign-on/main.rst b/docs/src/understand/single-sign-on/main.rst deleted file mode 100644 index 8603a8fd71..0000000000 --- a/docs/src/understand/single-sign-on/main.rst +++ /dev/null @@ -1,560 +0,0 @@ - -Single sign-on and user provisioning ------------------------------------- - -.. contents:: - -Introduction -~~~~~~~~~~~~ - -This page is intended as a manual for administrator users in need of setting up :term:`SSO` and provisionning users using :term:`SCIM` on their installation of Wire. - -Historically and by default, Wire's user authentication method is via phone or password. This has security implications and does not scale. - -Solution: :term:`SSO` with :term:`SAML`! `(Security Assertion Markup Language) `_ - -:term:`SSO` systems allow users to identify on multiple systems (including Wire once configured as such) using a single ID and password. - -You can find some of the advantages of :term:`SSO` over more traditional schemes `here `_. - -Also historically, wire has allowed team admins and owners to manage their users in the team management app. - -This does not scale as it requires a lot of manual labor for each user. - -The solution we offer to solve this issue is implementing :term:`SCIM` `(System for Cross-domain Identity Management) `_ - -:term:`SCIM` is an interface that allows both software (for example Active Directory) and custom scripts to manage Identities (users) in bulk. - -This page explains how to set up :term:`SCIM` and then use it. - -.. note:: - Note that it is recommended to use both :term:`SSO` and :term:`SCIM` (as opposed to just :term:`SSO` alone). - The reason is if you only use :term:`SSO`, but do not configure/implement :term:`SCIM`, you will experience reduced functionality. - In particular, without :term:`SCIM` all Wire users will be named according their e-mail address and won't have any rich profiles. - See below in the :term:`SCIM` section for a more detailled explanation. - - -Further reading -~~~~~~~~~~~~~~~ - -If you can't find the answers to your questions here, we have a few -more documents. Some of them are very technical, some may not be up -to date any more, and we are planning to move many of them into this -page. But for now they may be worth checking out. - -- :ref:`Trouble shooting & FAQ ` -- https://support.wire.com/hc/en-us/sections/360000580658-Authentication -- https://github.com/wireapp/wire-server/blob/1753b790e5cfb2d35e857648c88bcad3ac329f01/docs/reference/spar-braindump.md -- https://github.com/wireapp/wire-server/tree/1753b790e5cfb2d35e857648c88bcad3ac329f01/docs/reference/provisioning/ - - -Definitions -~~~~~~~~~~~ - -The following concepts need to be understood to use the present manual: - -.. glossary:: - - SCIM - System for Cross-domain Identity Management (:term:`SCIM`) is a standard for automating the exchange of user identity information between identity domains, or IT systems. - - One example might be that as a company onboards new employees and separates from existing employees, they are added and removed from the company's electronic employee directory. :term:`SCIM` could be used to automatically add/delete (or, provision/de-provision) accounts for those users in external systems such as G Suite, Office 365, or Salesforce.com. Then, a new user account would exist in the external systems for each new employee, and the user accounts for former employees might no longer exist in those systems. - - See: `System for Cross-domain Identity Management at Wikipedia `_ - - In the context of Wire, SCIM is the interface offered by the Wire service (in particular the spar service) that allows for single or mass automated addition/removal of user accounts. - - SSO - - Single sign-on (:term:`SSO`) is an authentication scheme that allows a user to log in with a single ID and password to any of several organizationally related, yet independent, software systems. - - True single sign-on allows the user to log in once and access different, independent services without re-entering authentication factors. - - See: `Single-Sign-On at Wikipedia `_ - - SAML - - Security Assertion Markup Language (:term:`SAML`, pronounced SAM-el, /'sæməl/) is an open standard for exchanging authentication and authorization data between parties, in particular, between an identity provider and a service provider. :term:`SAML` is an XML-based markup language for security assertions (statements that service providers use to make access-control decisions). :term:`SAML` is also: - - * A set of XML-based protocol messages - * A set of protocol message bindings - * A set of profiles (utilizing all of the above) - - An important use case that :term:`SAML` addresses is web-browser `single sign-on (SSO) `_ . Single sign-on is relatively easy to accomplish within a security domain (using cookies, for example) but extending :term:`SSO` across security domains is more difficult and resulted in the proliferation of non-interoperable proprietary technologies. The `SAML Web Browser SSO `_ profile was specified and standardized to promote interoperability. - - See: `SAML at Wikipedia `_ - - In the context of Wire, SAML is the standard/protocol used by the Wire services (in particular the spar service) to provide the Single Sign On feature. - - IdP - - In the context of Wire, an identity provider (abbreviated :term:`IdP`) is a service that provides SAML single sign-on (:term:`SSO`) credentials that give users access to Wire. - - Curl - - :term:`Curl` (pronounced ":term:`Curl`") is a command line tool used to download files over the HTTP (web) protocol. For example, `curl http://wire.com` will download the ``wire.com`` web page. - - In this manual, it is used to contact API (Application Programming Interface) endpoints manually, where those endpoints would normally be accessed by code or other software. - - This can be used either for illustrative purposes (to "show" how the endpoints can be used) or to allow the manual execution of some simple tasks. - - For example (not a real endpoint) `curl http://api.wire.com/delete_user/thomas` would (schematically) execute the :term:`Curl` command, which would contact the wire.com API and delete the user named "thomas". - - Running this command in a terminal would cause the :term:`Curl` command to access this URL, and the API at that URL would execute the requested action. - - See: `curl at Wikipedia `__ - - - Spar - - The Wire backend software stack is composed of different services, `running as pods <../overview.html#focus-on-pods>`__ in a kubernetes cluster. - - One of those pods is the "spar" service. That service/pod is dedicated to the providing :term:`SSO` (using :term:`SAML`) and :term:`SCIM` services. This page is the manual for this service. - - In the context of :term:`SCIM`, Wire's spar service is the `Service Provider `__ that Identity Management Software - (for example Azure, Okta, Ping Identity, SailPoint, Technology Nexus, etc.) uses for user account provisioning and deprovisioning. - -User login for the first time with SSO -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -:term:`SSO` allows users to register and log into Wire with their company credentials that they use on other software in their workplace. -No need to remember another password. - -When a team is set up on Wire, the administrators can provide users a login code or link that they can use to go straight to their company's login page. - -Here is what this looks from a user's perspective: - -1. Download Wire. -2. Select and copy the code that your company gave you / the administrator generated -3. Open Wire. Wire may detect the code on your clipboard and open a pop-up window with a text field. - Wire will automatically put the code into the text field. - If so, click Log in and go to step 8. -4. If no pop-up: click Login on the first screen. -5. Click Enterprise Login. -6. A pop-up will appear. In the text field, paste or type the code your company gave you. -7. Click Log in. -8. Wire will load your company's login page: log in with your company credentials. - - -SAML/SSO -~~~~~~~~ - -Introduction -^^^^^^^^^^^^ - -SSO (Single Sign-On) is technology allowing users to sign into multiple services with a single identity provider/credential. - -SSO is about `authentication`, not `provisioning` (create, update, remove user accounts). To learn more about the latter, continue `below `_. - -For example, if a company already has SSO setup for some of their services, and they start using Wire, they can use Wire's SSO support to add Wire to the set of services their users will be able to sign into with their existing SSO credentials. - -Here is a blog post we like about how SAML works: https://duo.com/blog/the-beer-drinkers-guide-to-saml - -And here is a diagram that explains it in slightly more technical terms: - -.. image:: Wire_SAML_Flow.png - -Here is a critique of XML/DSig security (which SAML relies on): https://www.cs.auckland.ac.nz/~pgut001/pubs/xmlsec.txt - -Terminology and concepts -^^^^^^^^^^^^^^^^^^^^^^^^ - -* End User / Browser: The end user is generally a human, an Application (Wire Client) or a browser (agent) who accesses the Service Provider to get access to a service or a protected resource. - The browser carrries out all the redirections from the SP to the IdP and vice versa. -* Service Provider (SP): The entity (here Wire software) that provides its protected resource when an end user tries to access this resource. To accomplish the SAML based SSO authentication, the Service Provider - must have the Identity Provider's metadata. -* Identity Provider (IdP): Defines the entity that provides the user identities, including the ability to authenticate a user to get access to a protected resource / application from a Service Provider. To accomplish - the SAML based SSO authentication, the IdP must have the Service Provider's metadata. -* SAML Request: This is the authentication request generated by the Service Provider to request an authentication from the Identity Provider for verifying the user's identity. -* SAML Response: The SAML Response contains the cryptographically signed assertion of the authenticated user and is generated by the Identity Provider. - -(Definitons adapted from `collab.net `_) - -.. _Setting up SSO externally: - -Setting up SSO externally -^^^^^^^^^^^^^^^^^^^^^^^^^ - -To set up :term:`SSO` for a given Wire installation, the Team owner/administrator must enable it. - -The first step is to configure the Identity Provider: you'll need to register Wire as a service provider in your Identity Provider. - -We've put together guides for registering with different providers: - -.. toctree:: - :maxdepth: 1 - - Instructions for Okta <../../how-to/single-sign-on/okta/main.rst> - Instructions for Centrify <../../how-to/single-sign-on/centrify/main.rst> - Instructions for Azure <../../how-to/single-sign-on/azure/main.rst> - Some screenshots for ADFS <../../how-to/single-sign-on/adfs/main.rst> - Generic instructions (try this if none of the above are applicable) <../../how-to/single-sign-on/generic-setup.rst> - Trouble shooting & FAQ <../../how-to/single-sign-on/trouble-shooting.rst> - -As you do this, make sure you take note of your :term:`IdP` metadata, which you will need for the next step. - -Once you are finished with registering Wire to your :term:`IdP`, move on to the next step, setting up :term:`SSO` internally. - -Setting up SSO internally -^^^^^^^^^^^^^^^^^^^^^^^^^ - -Now that you've registered Wire with your identity provider (:term:`IdP`), you can enable :term:`SSO` for your team on Wire. - -On Desktop: - -* Click Settings and click "Manage Team"; or go directly to teams.wire.com, or if you have an on-premise install, go to teams..com -* Login with your account credentials. -* Click "Customization". Here you will see the section for :term:`SSO`. -* Click the blue down arrow. -* Click "Add :term:`SAML` Connection". -* Provide the :term:`IdP` metadata. To find out more about retrieving this for your provider, see the guides in the "Setting up :term:`SSO` externally" step just above. -* Click "Save". -* Wire will now validate the document to set up the :term:`SAML` connection. -* If the data is valid, you will return to the Settings page. -* The page shows the information you need to log in with :term:`SSO`. Copy the login code or URL and send it to your team members or partners. For more information see: Logging in with :term:`SSO`. - -What to expect after :term:`SSO` is enabled: - -Anyone with a login through your :term:`SAML` identity provider (:term:`IdP`) and with access to the Wire app will be able to register and log in to your team using the :term:`SSO` Login URL and/or Code. - -Take care to share the code only with members of your team. - -If you haven't set up :term:`SCIM` (`we recommend you do <#introduction>`_), your team members can create accounts on Wire using :term:`SSO` simply by logging in, and will appear on the People tab of the team management page. - -If team members already have Wire accounts, use :term:`SCIM` to associate them with the :term:`SAML` credentials. If you make a mistake here, you may end up with several accounts for the same person. - -.. _User provisioning: - -User provisioning (SCIM/LDAP) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -SCIM/LDAP is about `provisioning` (create, update, remove user accounts), not `authentication`. To learn more about the latter, continue `above `_. - -Wire supports the `SCIM `__ (`RFC 7643 `__) protocol to create, update and delete users. - -If your user data is stored in an LDAP data source like Active Directory or OpenLDAP, you can use our docker-base `ldap-scim-bridge `__ to connect it to wire. - -Note that connecting a SCIM client to Wire also disables the functionality to create new users in the SSO login process. This functionality is disabled when a token is created (see below) and re-enabled when all tokens have been deleted. - -To set up the connection of your SCIM client (e.g. Azure Active Directory) you need to provide - -1. The URL under which Wire's SCIM API is hosted: ``https://prod-nginz-https.wire.com/scim/v2``. - If you are hosting your own instance of Wire then the URL is ``https:///scim/v2``, where ```` is where you are serving Wire's public endpoints. Some SCIM clients append ``/v2`` to the URL your provide. If this happens (check the URL mentioned in error messages of your SCIM client) then please provide the URL without the ``/v2`` suffix, i.e. ``https://prod-nginz-https.wire.com/scim`` or ``https:///scim``. - -2. A secret token which authorizes the use of the SCIM API. Use the `wire_scim_token.py `__ - script to generate a token. To run the script you need access to an user account with "admin" privileges that can login via email and password. Note that the token is independent from the admin account that created it, i.e. the token remains valid if the admin account gets deleted or changed. - -You need to configure your SCIM client to use the following mandatory SCIM attributes: - -1. Set the ``userName`` attribute to the desired user handle (the handle is shown - with an @ prefix in apps). It must be unique accross the entire Wire Cloud - (or unique on your own instance), and consist of the characters ``a-z0-9_.-`` - (no capital letters). - -2. Set the ``displayName`` attribute to the user's desired display name, e.g. "Jane Doe". - It must consist of 1-128 unicode characters. It does not need to be unique. - -3. The ``externalId`` attribute: - - a. If you are using Wire's SAML SSO feature then set ``externalId`` attribute to the same identifier used for ``NameID`` in your SAML configuration. - - b. If you are using email/password authentication then set the ``externalId`` - attribute to the user's email address. The user will receive an invitation email during provisioning. Also note that the account will be set to ``"active": false`` until the user has accepted the invitation and activated the account. - -You can optionally make use of Wire's ``urn:wire:scim:schemas:profile:1.0`` extension field to store arbitrary user profile data that is shown in the users profile, e.g. department, role. See `docs `__ for details. - -SCIM management in Wire (in Team Management) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -SCIM security and authentication -'''''''''''''''''''''''''''''''' - -Wire uses a very basic variant of oauth, where a *bearer token* is presented to the server in header with all :term:`SCIM` requests. - -You can create such bearer tokens in team management and copy them from there into your the dashboard of your SCIM data source. - -Generating a SCIM token -''''''''''''''''''''''' - -In order to be able to send SCIM requests to Wire, we first need to generate a SCIM token. This section explains how to do this. - -Once the token is generated, it should be noted/remembered, and it will be used in all subsequent SCIM uses/requests to authenticate the request as valid/authenticated. - -These are the steps to generate a new :term:`SCIM` token, which you will need to provide to your identity provider (:term:`IdP`), along with the target API URL, to enable :term:`SCIM` provisionning. - -* Step 1: Go to https://teams.wire.com/settings (Here replace "wire.com" with your own domain if you have an on-premise installation of Wire). - -.. image:: token-step-01.png - :align: center - -* Step 2: In the left menu, go to "Customization". - -.. image:: token-step-02.png - :align: center - -* Step 3: Go to "Automated User Management (:term:`SCIM`)" and click the "down" to expand - -.. image:: token-step-03.png - :align: center - -* Step 4: Click "Generate token", if your password is requested, enter it. - -.. image:: token-step-04.png - :align: center - -* Step 5: Once the token is generated, copy it into your clipboard and store it somewhere safe (eg., in the dashboard of your SCIM data source). - -.. image:: token-step-05.png - :align: center - -* Step 6: You're done! You can now view token information, delete the token, or create more tokens should you need them. - -.. image:: token-step-06.png - :align: center - -Tokens are now listed in this :term:`SCIM`-related area of the screen, you can generate up to 8 such tokens. - -Using SCIM via Curl -^^^^^^^^^^^^^^^^^^^ - -You can use the term:`Curl` command line HTTP tool to access tho wire backend (in particular the ``spar`` service) through the :term:`SCIM` API. - -This can be helpful to write your own tooling to interface with wire. - -Creating a SCIM token -''''''''''''''''''''' - -Before we can send commands to the :term:`SCIM` API/Spar service, we need to be authenticated. This is done through the creation of a :term:`SCIM` token. - -First, we need a little shell environment. Run the following in your terminal/shell: - -.. code-block:: bash - :linenos: - - export WIRE_BACKEND=https://prod-nginz-https.wire.com - export WIRE_ADMIN=... - export WIRE_PASSWD=... - -Wire's SCIM API currently supports a variant of HTTP basic auth. - -In order to create a token in your team, you need to authenticate using your team admin credentials. - -The way this works behind the scenes in your browser or cell phone, and in plain sight if you want to use curl, is you need to get a Wire token. - -First install the ``jq`` command (https://stedolan.github.io/jq/): - -.. code-block:: bash - - sudo apt install jq - -.. note:: - - If you don't want to install ``jq``, you can just call the ``curl`` command and copy the access token into the shell variable manually. - -Then run: - -.. code-block:: bash - :linenos: - - export BEARER=$(curl -X POST \ - --header 'Content-Type: application/json' \ - --header 'Accept: application/json' \ - -d '{"email":"'"$WIRE_ADMIN"'","password":"'"$WIRE_PASSWD"'"}' \ - $WIRE_BACKEND/login'?persist=false' | jq -r .access_token) - -This token will be good for 15 minutes; after that, just repeat the command above to get a new token. - -.. note:: - SCIM requests are authenticated with a SCIM token, see below. SCIM tokens and Wire tokens are different things. - - A Wire token is necessary to get a SCIM token. SCIM tokens do not expire, but need to be deleted explicitly. - -You can test that you are logged in with the following command: - -.. code-block:: bash - - curl -X GET --header "Authorization: Bearer $BEARER" $WIRE_BACKEND/self - -Now you are ready to create a SCIM token: - -.. code-block:: bash - :linenos: - - export SCIM_TOKEN_FULL=$(curl -X POST \ - --header "Authorization: Bearer $BEARER" \ - --header 'Content-Type: application/json;charset=utf-8' \ - -d '{ "description": "test '"`date`"'", "password": "'"$WIRE_PASSWD"'" }' \ - $WIRE_BACKEND/scim/auth-tokens) - export SCIM_TOKEN=$(echo $SCIM_TOKEN_FULL | jq -r .token) - export SCIM_TOKEN_ID=$(echo $SCIM_TOKEN_FULL | jq -r .info.id) - -The SCIM token is now contained in the ``SCIM_TOKEN`` environment variable. - -You can look it up again with: - -.. code-block:: bash - :linenos: - - curl -X GET --header "Authorization: Bearer $BEARER" \ - $WIRE_BACKEND/scim/auth-tokens - -And you can delete it with: - -.. code-block:: bash - :linenos: - - curl -X DELETE --header "Authorization: Bearer $BEARER" \ - $WIRE_BACKEND/scim/auth-tokens?id=$SCIM_TOKEN_ID - -Using a SCIM token to Create Read Update and Delete (CRUD) users -'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' - -Now that you have your SCIM token, you can use it to talk to the SCIM API to manipulate (create, read, update, delete) users, either individually or in bulk. - -**JSON encoding of SCIM Users** - -In order to manipulate users using commands, you need to specify user data. - -A minimal definition of a user is written in JSON format and looks like this: - -.. code-block:: json - :linenos: - - { - "schemas" : ["urn:ietf:params:scim:schemas:core:2.0:User"], - "externalId" : "nick@example.com", - "userName" : "nick", - "displayName" : "The Nick" - } - -You can store it in a variable using this sort of command: - -.. code-block:: bash - :linenos: - - export SCIM_USER='{ - "schemas" : ["urn:ietf:params:scim:schemas:core:2.0:User"], - "externalId" : "nick@example.com", - "userName" : "nick", - "displayName" : "The Nick" - }' - -The ``externalId`` is used to construct a SAML identity. Two cases are -currently supported: - -1. ``externalId`` contains a valid email address. - The SAML ``NameID`` has the form ``me@example.com``. -2. ``externalId`` contains anything that is *not* an email address. - The SAML ``NameID`` has the form ``...``. - -.. note:: - - It is important to configure your SAML provider to use ``nameid-format:emailAddress`` or ``nameid-format:unspecified``. Other nameid formats are not supported at this moment. - - See `FAQ `_ - -We also support custom fields that are used in rich profiles in this form (see: https://github.com/wireapp/wire-server/blob/develop/docs/reference/user/rich-info.md): - -.. code-block:: bash - :linenos: - - export SCIM_USER='{ - "schemas" : ["urn:ietf:params:scim:schemas:core:2.0:User", "urn:wire:scim:schemas:profile:1.0"], - "externalId" : "rnick@example.com", - "userName" : "rnick", - "displayName" : "The Rich Nick", - "urn:wire:scim:schemas:profile:1.0": { - "richInfo": [ - { - "type": "Department", - "value": "Sales & Marketing" - }, - { - "type": "Favorite color", - "value": "Blue" - } - ] - } - }' - -**How to create a user** - -You can create a user using the following command: - -.. code-block:: bash - :linenos: - - export STORED_USER=$(curl -X POST \ - --header "Authorization: Bearer $SCIM_TOKEN" \ - --header 'Content-Type: application/json;charset=utf-8' \ - -d "$SCIM_USER" \ - $WIRE_BACKEND/scim/v2/Users) - export STORED_USER_ID=$(echo $STORED_USER | jq -r .id) - -Note that ``$SCIM_USER`` is in the JSON format and is declared before running this commend as described in the section above. - -**Get a specific user** - -.. code-block:: bash - :linenos: - - curl -X GET \ - --header "Authorization: Bearer $SCIM_TOKEN" \ - --header 'Content-Type: application/json;charset=utf-8' \ - $WIRE_BACKEND/scim/v2/Users/$STORED_USER_ID - -**Search a specific user** - -SCIM user search is quite flexible. Wire currently only supports lookup by wire handle or email address. - -Email address (and/or SAML NameID, if /a): - -.. code-block:: bash - :linenos: - - curl -X GET \ - --header "Authorization: Bearer $SCIM_TOKEN" \ - --header 'Content-Type: application/json;charset=utf-8' \ - $WIRE_BACKEND/scim/v2/Users/'?filter=externalId%20eq%20%22me%40example.com%22' - -Wire handle: same request, just replace the query part with - -.. code-block:: bash - - '?filter=userName%20eq%20%22me%22' - -**Update a specific user** - -For each put request, you need to provide the full json object. All omitted fields will be set to ``null``. (If you do not have an up-to-date user present, just ``GET`` one right before the ``PUT``.) - -.. code-block:: bash - :linenos: - - export SCIM_USER='{ - "schemas" : ["urn:ietf:params:scim:schemas:core:2.0:User"], - "externalId" : "rnick@example.com", - "userName" : "newnick", - "displayName" : "The New Nick" - }' - -.. code-block:: bash - :linenos: - - curl -X PUT \ - --header "Authorization: Bearer $SCIM_TOKEN" \ - --header 'Content-Type: application/json;charset=utf-8' \ - -d "$SCIM_USER" \ - $WIRE_BACKEND/scim/v2/Users/$STORED_USER_ID - -**Deactivate user** - -It is possible to temporarily deactivate an user (and reactivate him later) by setting his ``active`` property to ``true/false`` without affecting his device history. (`active=false` changes the wire user status to `suspended`.) - -**Delete user** - -.. code-block:: bash - :linenos: - - curl -X DELETE \ - --header "Authorization: Bearer $SCIM_TOKEN" \ - $WIRE_BACKEND/scim/v2/Users/$STORED_USER_ID From 4bfc91a7d4e50df3bf6386e092a42ae0c948ab21 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Thu, 12 Jan 2023 17:41:05 +0100 Subject: [PATCH 03/38] Update Federation docs (#2982) --- .../how-to/install/configure-federation.md | 107 +++--- docs/src/understand/federation/api.md | 342 +++++++++--------- .../src/understand/federation/architecture.md | 313 +++------------- .../federation/backend-communication.md | 167 +++++++++ docs/src/understand/federation/glossary.md | 108 ------ .../federation/img/federation-apis-flow.png | Bin 0 -> 64907 bytes .../federation/img/federation-apis-flow.txt | 32 ++ .../federation/img/federation-flow.png | Bin 144554 -> 142892 bytes .../federation/img/federation-flow.txt | 55 ++- docs/src/understand/federation/index.md | 30 +- .../src/understand/federation/introduction.md | 45 --- docs/src/understand/federation/replace.sh | 11 - docs/src/understand/federation/roadmap.md | 112 ------ 13 files changed, 533 insertions(+), 789 deletions(-) create mode 100644 docs/src/understand/federation/backend-communication.md delete mode 100644 docs/src/understand/federation/glossary.md create mode 100644 docs/src/understand/federation/img/federation-apis-flow.png create mode 100644 docs/src/understand/federation/img/federation-apis-flow.txt delete mode 100644 docs/src/understand/federation/introduction.md delete mode 100644 docs/src/understand/federation/replace.sh delete mode 100644 docs/src/understand/federation/roadmap.md diff --git a/docs/src/how-to/install/configure-federation.md b/docs/src/how-to/install/configure-federation.md index 38d53af30f..c7e46849bc 100644 --- a/docs/src/how-to/install/configure-federation.md +++ b/docs/src/how-to/install/configure-federation.md @@ -1,20 +1,12 @@ (configure-federation)= # Configure Wire-Server for Federation - -## Background +See also {ref}`federation-understand`, which explains the architecture and concepts. -Please first understand the current scope and aim of wire-server -federation by reading -{ref}`Understanding federation `. - -```{warning} -As of October 2021, federation implementation is still work in progress. -Many features are not implemented yet, and it should be considered -\"alpha\": stability, and upgrade compatibility are not guaranteed. +```{note} +The Federation development is work in progress. ``` - ## Summary of necessary steps to configure federation The steps needed to configure federation are as follows and they will be @@ -22,32 +14,32 @@ detailed in the sections below: - Choose a backend domain name -- DNS setup for federation (including an `SRV` record) +- DNS setup for federation (including a `SRV` record) - Generate and configure TLS certificates: - > - server certificates - > - client certificates - > - a selection of CA certificates you trust when interacting with - > other backends + - server certificates + - client certificates + - a selection of CA certificates you trust when interacting with + other backends - Configure helm charts : federator and ingress and webapp subcharts - Test that your configurations work as expected. (choose-backend-domain)= -## Choose a {ref}`Backend Domain Name` - -As of the release \[helm chart 0.129.0, Wire docker version 2.94.0\] -from 2020-12-15, a Backend Domain (set as `federationDomain` in -configuration) is a mandatory configuration setting. Regardless of -whether you want to enable federation for a backend or not, you must -decide what its domain is going to be. This helps in keeping things -simpler across all components of Wire and also enables to turn on +## Choose a Backend Domain + +As of the release \[helm chart 0.129.0, Wire docker version 2.94.0\] from +2020-12-15, the `federationDomain` is a mandatory configuration setting, which +defines the {ref}`backend domain ` of your +installation. Regardless of whether you want to enable federation for a backend +or not, you must decide what its domain is going to be. This helps in keeping +things simpler across all components of Wire and also enables to turn on federation in the future if required. It is highly recommended that this domain is configured as something - * [ ] that is controlled by the administrator/operator(s). The actual servers +that is controlled by the administrator/operator(s). The actual servers do not need to be available on this domain, but you MUST be able to set an SRV record for `_wire-server-federator._tcp.` that informs other wire-server backends where to find your actual servers. @@ -57,41 +49,40 @@ breaking experience for all the users which are already using the backend. (consequences-backend-domain)= -## Consequences of the choice of Backend Domain +## Consequences of the choice of a backend domain -- You need control over a specific subdomain of this Backend Domain +- You need control over a specific subdomain of this backend domain (to set an SRV DNS record as explained in the next section). Without this control, you cannot federate with anyone. -- This Backend Domain becomes part of the underlying identify of all +- This backend domain becomes part of the underlying identity of all users on your servers. - > - Example: Let\'s say you choose `example.com` as your Backend - > Domain. Your user known to you as Alice, and known on your - > server with ID `ac41a202-2555-11ec-9341-00163e5e6c00` will - > become known for other servers you federate with as - > - > ``` json - > { - > "user": { - > "id": "ac41a202-2555-11ec-9341-00163e5e6c00", - > "domain": "example.com" - > } - > } - > ``` - -- As of October 2021, this domain is used in the User Interface - alongside user information. (This may or may not change in the - future) - - > - Example: Using the same example as above, for backends you - > federate with, Alice would be displayed with the - > human-readable username `@alice@example.com` for users on - > other backends. + Example: Let\'s say you choose `example.com` as your backend + domain. Your user known to you as Alice, and known on your + server with ID `ac41a202-2555-11ec-9341-00163e5e6c00` will + become known for other servers you federate with as + + ``` json + { + "user": { + "id": "ac41a202-2555-11ec-9341-00163e5e6c00", + "domain": "example.com" + } + } + ``` + +- This domain is shown in the User Interface + alongside user information. + + Example: Using the same example as above, for backends you + federate with, Alice would be displayed with the + human-readable username `@alice@example.com` for users on + other backends. ```{warning} -As of October 2021, *changing* this Backend Domain after existing user -activity with a recent version (versions later than \~May/June 2021) +*Changing* the backend domain after existing user +activity with a client version (versions later than May/June 2021) will lead to undefined behaviour (untested, not accounted for during development) on some or all client platforms (Web, Android, iOS) for those users: It is possible your clients could crash, or lose part of @@ -127,7 +118,7 @@ The fields of the SRV record need to be populated as follows - `weight`: \>0 for your server to be reachable. A good default value could be 10 - `port`: `443` -- `target`: \ +- `target`: the infra domain To give an example, assuming @@ -137,7 +128,7 @@ To give an example, assuming `.wire.example.org` then your federation -{ref}`Infra Domain ` +{ref}`Infrastructure Domain ` would be `federator.wire.example.org`. The SRV record would look as follows: @@ -159,6 +150,7 @@ alongside your other DNS records that point to the ingress component, also needs to point to the IP of your ingress, i.e. the IP you want to provide services on. +(federation-certificate-setup)= ## Generate and configure TLS server and client certificates Are your servers on the public internet? Then you have the option of @@ -169,7 +161,7 @@ public internet or you would like to use your own CA, go to subsection ```{admonition} Note -As of Jan 2022, we\'re using the +As of January 2023, we\'re using the [hs-tls](https://hackage.haskell.org/package/tls) library for outgoing TLS connections to other backends, which only supports P256 for ECDSA keys. Therefore, we have specified a [key size of 256 @@ -252,7 +244,7 @@ You can use one single certificate and key for both server and client certificate use. ```{note} -Currently (October 2021), due to a limitation of the TLS library in use +Due to a limitation of the TLS library in use for federation ([hs-tls](https://github.com/vincenthz/hs-tls)), only some ciphers are supported. Moving to an openssl-based library is planned, which will provide support for a wider range of ciphers. @@ -449,6 +441,7 @@ tls: verify_depth: 3 # default: 1 ``` +(configure-federation-allow-list)= ### Configure the allow list By default, federation is turned off (allow list set to the empty list): @@ -525,7 +518,7 @@ DOMAIN to your {ref}`federation infra domain `. They should include your domain as part of the SAN (Subject Alternative Names) and not have expired. -### Manually test that federation \"works\" +### Manually test that federation works Prerequisites: diff --git a/docs/src/understand/federation/api.md b/docs/src/understand/federation/api.md index a11e38397d..2a8325606c 100644 --- a/docs/src/understand/federation/api.md +++ b/docs/src/understand/federation/api.md @@ -1,48 +1,77 @@ (federation-api)= -# API +# Federation API -The Federation API consists of two *layers*: +(qualified-identifiers-and-names)= +## Qualified Identifiers and Names -1. Between two backends (i.e. between a *Federator* and a - *Federation Ingress*) -2. Between backend-internal components +The federated architecture is reflected in the structure of the various +identifiers and names used in the API. Identifiers, such as user ids, are unique +within the context of a backend. They are made unique within the context of all +federating backend by combining them with the {ref}`backend domain +`. -(qualified-identifiers-and-names)= +For example a user with user id `d389b370-5f7d-4efd-9f9a-8d525540ad93` on +backend `b.example.com` has the *qualified user id* +`d389b370-5f7d-4efd-9f9a-8d525540ad93@b.example.com`. In API request bodies +qualified identities are encoded as objects, e.g. -## Qualified Identifiers and Names +``` +{ + "user": { + "id": "d389b370-5f7d-4efd-9f9a-8d525540ad93", + "domain": "b.example.com" + } + ... +} -The federated (and consequently distributed) architecture is reflected -in the structure of the various identifiers and names used in the API. -Before federation, identifiers were only unique in the context of a -single backend; for federation, they are made globally unique by -combining them with the federation domain of their backend. We call -these combined identifiers *qualified* identifiers. While other parts of -some identifiers or names may change, the domain name (i.e. the -qualifying part) is static. - -In particular, we use the following identifiers throughout the API: - -- {ref}`glossary_qualified-user-id`: *user_uuid@backend-domain.com* -- {ref}`glossary_qualified-user-name`: *user_name@backend-domain.com* -- {ref}`glossary_qualified-client-id` attached to a QUID: *client_uuid.user_uuid@backend-domain.com* -- {ref}`glossary_qualified-conversation-id` / {ref}`glossary_qualified-group-id`: *backend-domain.com/groups/group_uuid* -- {ref}`glossary_qualified-team-id`: *backend-domain.com/teams/team_uuid* - -While the canonical representation for purposes of visualization is as -displayed above, the API often decomposes the qualified identifiers into -an (unqualified) id and a domain name. In the code and API -documentation, we sometimes call a username a \"handle\" and a qualified -username a \"qualified handle\". - -Besides the above names and identifiers, there are also user -{ref}`glossary_display-name` (sometimes also -referred to as \"profile names\"), which are not unique on the user\'s -backend, can be changed by the user at any time and are not qualified. +``` +In API path segments qualified identities are encoded with the domain first, e.g. +``` +POST /connections/b.example.com/d389b370-5f7d-4efd-9f9a-8d525540ad93 +``` +to send a connection request to a user. + +Any identifier on a backend can be qualified: + +- conversation ids +- team ids +- client ids +- user ids +- user handles, e.g. local handle `@alice` is displayed as `@alice@b.example.com` in federating users' devices + +User profile names (e.g. "Alice") which are not unique on the user\'s backend, +can be changed by the user at any time and are not qualified. (api-between-federators)= -## API between Federators +## Federated requests + +Every federated API request is made by a service component (e.g. brig, galley, +cargohold) in one backend and responded to by a service component in the other +backend. The *Federators* of the backends are relaying the request between the +components across backends . The components talk to each other via the +*Federator* in the originating domain and *Federator Ingress* in the receiving +domain (for details see {ref}`backend-to-backend-communication`). + + +```{figure} ./img/federation-apis-flow.png +--- +width: 100% +--- +Federators relaying a request between components. See {ref}`federation-back2back-example` to see the discovery, authentication and authorization steps that are omitted from this figure. +``` + +### API From Components to Federator + + +When making the call to the *Federator*, the components use HTTP2. They call the +Federator's `Outward` service, which accepts `POST` requests with path +`/rpc/:domain/:component/:rpc`. Such a request will be forwarded to the remote +Federator with the given {ref}`backend domain`, and converted +to the appropriate request of its `Inward` service. + +### API between Federators The layer between *Federator* acts as an envelope for communication between other components of wire server. The *Inward* service of @@ -68,19 +97,10 @@ See {ref}`api-from-federator-to-components` for more details on RPCs and their p (api-from-components-to-federator)= -## API From Components to Federator - -Between two federated backends, the components talk to each other via the -*Federator* in the originating domain and *Ingress* in the receiving domain. -When making the call to the *Federator*, the components use HTTP2. They call the -`Outward` service, which accepts `POST` requests with path -`/rpc/:domain/:component/:rpc`. Such a request will be forwarded to a remote -federator with the given {ref}`Backend-domains`, and converted to the -appropriate request for its `Inward` service. (api-from-federator-to-components)= -## API From Federator to Components +### API From Federator to Components The components expose a REST API over HTTP to be consumed by the *Federator*. All the paths start with `/federation`. When a *Federator* @@ -107,13 +127,10 @@ attacks such as attempting to access `/federation/../users/by-handle`. ## List of Federation APIs exposed by Components Each component of the backend provides an API towards the *Federator* -for access by other backends. For example on how these APIs are used, -see the section on -`end-to-end flows`{.interpreted-text role="ref"}. - +for access by other backends. ```{note} -This reflects status of API endpoints as of 2022-01-28. For latest APIs please +This reflects status of API endpoints as of 2023-01-10. For latest APIs please refer to the corresponding source code linked in the individual section. ``` @@ -139,10 +156,15 @@ the backend. w.r.t. that term. - `get-user-clients`: Given a list of user ids, return the lists of clients of each of the users. +- `get-user-clients`: Given a list of user ids, return a list of all their clients with public information +- `send-connection-action`: Make and also respond to user connection requests +- `on-user-deleted-connections`: Notify users that are connected to remote user about that user's deletion +- `get-mls-clients`: Request all [MLS](../../how-to/install/mls)-capable clients for a given user +- `claim-key-packages`: Claim a previously-uploaded KeyPackage of a remote user. User for adding users to MLS conversations. See [the brig source code](https://github.com/wireapp/wire-server/blob/master/libs/wire-api-federation/src/Wire/API/Federation/API/Brig.hs) -for the current list of federated endpoints of the *Brig*, as well as +for the current list of federated endpoints of *Brig*, as well as their precise inputs and outputs. (galley)= @@ -153,46 +175,63 @@ Each backend keeps a record of the conversations that each of its members is a part of. The purpose of the Galley API is to allow backends to synchronize the state of the conversations of their members. -- `on-conversation-created`: Given a name and a list of conversation - members, create a conversation locally. This is used to inform - another backend of a new conversation that involves their local - user(s). -- `get-conversations`: Given a qualified user id and a list of +- `get-conversations`: Given a qualified user id and a list of conversation ids, return the details of the conversations. This allows a remote backend to query conversation metadata of their local user from this backend. To avoid metadata leaks, the backend will check that the domain of the given user corresponds to the domain of the backend sending the request. -- `on-conversation-updated`: Given a qualified user id and a qualified +- `get-sub-conversation`: Get a MLS subconversation +- `leave-conversation`: Given a remote user and a conversation id, + remove the the remote user from the (local) conversation. +- `mls-welcome`: Send MLS welcome message to a new user owned by the called backend +- `on-client-removed`: Inform called backend that a client of a user has been deleted +- `on-conversation-created`: Given a name and a list of conversation + members, create a conversation locally. This is used to inform + another backend of a new conversation that involves their local + user(s). +- `on-conversation-updated`: Given a qualified user id and a qualified conversation id, update the conversation details locally with the other data provided. This is used to alert remote backend of updates in the conversation metadata of conversations in which at least one of their local users is involved. -- `leave-conversation`: Given a remote user and a conversation id, - remove the the remote user from the (local) conversation. -- `on-message-sent`: Given a remote message and a conversation id, +- `on-message-sent`: Given a remote message and a conversation id, propagate a message to local users. This is used whenever there is a remote user in a conversation (see end-to-end flows). -- `send-message`: Given a sender and a raw message request, send a +- `on-mls-message-sent`: Receive a MLS message that originates in the calling backend +- `on-new-remote-conversation`: Inform the called backend about a conversation that exists on the calling backend. This request is made before the first time the backend might learn about this conversation, e.g. when its first user is added to the conversation. +- `on-typing-indicator-updated`: Used by the calling backend (that owns a conversation) to inform the called backend about a change of the typing indicator status of remote user +- `on-user-deleted-conversations`: When a user on calling backend this request is made for all conversations on the called backend was part of +- `query-group-info`: Query the MLS public group state +- `send-message`: Given a sender and a raw message request, send a message to a conversation owned by another backend. This is used when the user sending a message is not on the same backend as the conversation the message is sent in. +- `send-mls-commit-bundle`: Send a MLS commit bundle to backend that owns the conversation +- `send-mls-message`: Send MLS message to backend that owns the conversation +- `update-conversation`: Calling backend requests a conversation action on the called backend which owns the conversation See [the galley source code](https://github.com/wireapp/wire-server/blob/master/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs) -for the current list of federated endpoints of the *Galley*, as well as +for the current list of federated endpoints of *Galley*, as well as their precise inputs and outputs. (end-to-end-flows)= -## End-to-End Flows +### Cargohold +- `get-asset`: Check if asset owned by called backend is available to calling backend +- `stream-asset`: Stream asset owned by the called backend -In the following end-to-end flows, we focus on the interaction between -the Brigs and Galleys of federated backends. While the interactions are -facilitated by the *Federator* and *Federation Ingress* components of -the backends involved, which handle the necessary discovery, -authentication and authorization steps, we won\'t mention these steps -explicitly each time to keep the flows simple. +See [the cargohold source +code](https://github.com/wireapp/wire-server/blob/master/libs/wire-api-federation/src/Wire/API/Federation/API/Cargohold.hs) +for the current list of federated endpoints of the *Cargohold*, as well as +their precise inputs and outputs. + +## Example End-to-End Flows + +In the following the interactions between *Federator* and *Federation Ingress* +components of the backends involved are omitted for simplicity. Also the backend +domain and infra domain are assumed the same. Additionally we assume that the backend domain and the infra domain of the respective backends involved are the same and each domain identifies @@ -202,125 +241,98 @@ a distinct backend. ### User Discovery -In this flow, the user *A* at *backend-a.com* tries to search for user -*B* at *backend-b.com*. +In this flow, the user *Alice* at *a.example.com* tries to search for user +*Bob* at *b.example.com*. -1. User *A@backend-a.com* enters the qualified user name of the target - user *B@backend-b.com* into the search field of their Wire client. +1. User *Alice* enters the qualified user name of the target + user *Bob* : `@bob@b.example.com` into the search field of their Wire client. 2. The client issues a query to `/search/contacts` of the Brig - searching for *B* at *backend-b.com*. -3. The Brig in *A*\'s backend asks its local *Federator* to query the - `search-users` endpoint of B\'s backend for *B*. -4. *A*\'s *Federator* queries *B*\'s Brig via *B*\'s *Federation + searching for *Bob* at *b.example.com*. +3. The Brig in *Alice*\'s backend asks its local *Federator* to query the + `search-users` endpoint in *Bob*\'s backend. +4. *Alice*\'s *Federator* queries *Bob*\'s Brig via *Bob*\'s *Federation Ingress* and *Federator* as requested. -5. *B*\'s Brig replies with *B*\'s user name and qualified handle, the - response goes through *B*\'s *Federator* and *Federation Ingress*, - as well as *A*\'s *Federator* before it reaches *A*\'s Brig. -6. *A*\'s Brig forwards that information to *A*\'s client. +5. *Bob*\'s Brig replies with *Bob*\'s user name and qualified handle, the + response goes through *Bob*\'s *Federator* and *Federation Ingress*, + as well as *Alice*\'s *Federator* before it reaches *A*\'s Brig. +6. *Alice*\'s Brig forwards that information to *A*\'s client. (conversation-establishment)= ### Conversation Establishment -After having discovered user *B* at *backend-b.com*, user *A* at -*backend-a.com* wants to establish a conversation with *B*. +After having discovered user *Bob* at *b.example.com*, user *Alice* at +*a.example.com* wants to establish a conversation with *Bob*. 1. From the search results of a {ref}`user discovery` - process, *A* chooses to create a conversation with *B*. -2. *A*\'s client issues a `/users/backend-b.com/B/prekeys` query to - *A*\'s Brig. -3. *A*\'s Brig asks its *Federator* to query the `claim-prekey-bundle` - endpoint of *B*\'s backend using *B*\'s user id. -4. *B*\'s *Federation Ingress* forwards the query to the *Federator*, + process, *Alice* chooses to create a conversation with *Bob*. +2. *Alice*\'s client issues a `/users/b.example.com//prekeys` query to + *Alice*\'s Brig. +3. *Alice*\'s Brig asks its *Federator* to query the `claim-prekey-bundle` + endpoint of *Bob*\'s backend using *Bob*\'s user id. +4. *Bob*\'s *Federation Ingress* forwards the query to the *Federator*, who in turn forwards it to the local Brig. -5. *B*\'s Brig replies with a prekey bundle for each of *B*\'s clients, - which is forwarded to *A*\'s Brig via *B*\'s *Federator* and - *Federation Ingress*, as well as *A*\'s *Federator*. -6. *A*\'s Brig forwards that information to *A*\'s client. -7. *A*\'s client queries the `/conversations` endpoint of its Galley - using *B*\'s user id. -8. *A*\'s Galley creates the conversation locally and queries the - `on-conversation-created` endpoint of *B*\'s Galley (again via its - local *Federator*, as well as *B*\'s *Federation Ingress* and +5. *Bob*\'s Brig replies with a prekey bundle for each of *Bob*\'s clients, + which is forwarded to *Alice*\'s Brig via *Bob*\'s *Federator* and + *Federation Ingress*, as well as *Alice*\'s *Federator*. +6. *Alice*\'s Brig forwards that information to *A*\'s client. +7. *Alice*\'s client queries the `/conversations` endpoint of its Galley + using *Bob*\'s user id. +8. *Alice*\'s Galley creates the conversation locally and queries the + `on-conversation-created` endpoint of *Bob*\'s Galley (again via its + local *Federator*, as well as *Bob*\'s *Federation Ingress* and *Federator*) to inform it about the new conversation, including the conversation metadata in the request. -9. *B*\'s Galley registers the conversation locally and confirms the +9. *Bob*\'s Galley registers the conversation locally and confirms the query. -10. *B*\'s Galley notifies *B*\'s client of the creation of the +10. *Bob*\'s Galley notifies *Bob*\'s client of the creation of the conversation. (message-sending-a)= -### Message Sending (A) - -Having established a conversation with user *B* at *backend-b.com*, user -*A* at *backend-a.com* wants to send a message to user *B*. - -1. In a conversation *conv-1@backend-a.com* on *A*\'s backend with - users *A@backend-a.com* and *B@backend-b.com*, *A* sends a message - by using the `/conversations/backend-a.com/conv-1/proteus/messages` - endpoint on *A*\'s Galley. -2. *A*\'s Galley checks if *A* included all necessary user devices in - their request. For that it makes a `get-user-clients` request to - *B*\'s Galley. *A*\'s Galley checks that the returned list of - clients matches the list of clients the message was encrypted for. -3. *A*\'s Galley sends the message to all clients in the conversation - that are part of *A*\'s backend. -4. *A*\'s Galley queries the `on-message-sent` endpoint on *B*\'s - Galley via its *Federator* and *B*\'s *Federation Ingress* and - *Federator*. -5. *B*\'s Galley will propagate the message to all local clients - involved in the conversation. - -(message-sending-b)= - -### Message Sending (B) +### Message Sending -Having received a message from user *A* at *backend-a.com*, user *B* at -*backend-b.com* wants send a reply. +Having established a conversation with user *Bob* at *b.example.com*, user +*Alice* at *a.example.com* wants to send a message to user *Bob*. -1. In a conversation *conv-1@backend-a.com* on *A*\'s backend with - users *A@backend-a.com* and *B@backend-b.com*, *B* sends a message - by using the `/conversations/backend-a.com/conv-1/proteus/messages` - endpoint on *B*\'s backend. -2. *B*\'s Galley queries the `send-message` endpoint on *A*\'s backend. - *Steps 3-6 below are essentially the same as steps 2-5 in Message - Sending (A)* -3. *A*\'s Galley checks if *A* included all necessary user devices in +1. In a conversation *\@a.example.com* on *Alice*\'s backend with + users *Alice* and *Bob*, *Alice* sends a message + by using the `/conversations/a.example.com//proteus/messages` + endpoint on *Alice*\'s Galley. +2. *Alice*\'s Galley checks if *A* included all necessary user devices in their request. For that it makes a `get-user-clients` request to - *B*\'s Galley. *A*\'s Galley checks that the returned list of + *Bob*\'s Galley. *Alice*\'s Galley checks that the returned list of clients matches the list of clients the message was encrypted for. -4. *A*\'s Galley sends the message to all clients in the conversation - that are part of *A*\'s backend. -5. *A*\'s Galley queries the `on-message-sent` endpoint on *B*\'s - Galley via its *Federator* and *B*\'s *Federation Ingress* and +3. *Alice*\'s Galley sends the message to all clients in the conversation + that are part of *Alice*\'s backend. +4. *Alice*\'s Galley queries the `on-message-sent` endpoint on *Bob*\'s + Galley via its *Federator* and *Bob*\'s *Federation Ingress* and *Federator*. -6. *B*\'s Galley will propagate the message to all local clients +5. *Bob*\'s Galley will propagate the message to all local clients involved in the conversation. -(error-codes)= - -## Error Codes - -This page describes the errors that can occur during federation. - -(authentication-errors)= - -### Authentication Errors - -TODO for now, we only describe the errors here. Later, we should add -exact error codes. - -TODO we might want to merge one or more of these errors - -- *authentication error*: occurs when a backend queries another - backend and provides either no client certificate, or a client - certificate that the receiving backend cannot authenticate -- *authorization error*: occurs when a sending backend - authenticates successfully, but is not on the allow list of the - receiving backend -- *discovery error*: occurs when a sending backend authenticates - successfully, but the [SRV]{.title-ref} record published for the - claimed domain of the sending backend doesn\'t match the SAN of the - sending backend\'s client certificate +## Ownership + +Wire uses the concept of **ownership** as a guiding principle in the design of +Federation. Every resource, e.g. user, conversation, asset, is **owned** by the +backend on which it was *created*. + +A backend that owns a resource is the source of truth for it. For example, for +users this means that information about user *Alice* which is owned by backend +*A* is stored only on backend *A*. If any federating backend needs information +about the user *Alice*, e.g. the profile information, it needs to request that +information from *A*. + +In some cases backends locally store partial information of resources they don't +own. For example a backend stores a reference to any remotely-owned conversation +any of its users is participating in. However, to get the full list of all +participants of a remote conversation, the owning backend needs to be queried. + +Ownership is reflected in the naming convention of federation RPCs. Any rpc +named with prefix `on-` is always invoked by the backend that owns the resource +to inform federating backends. For example, if a user leaves a remote +conversation its backend would call the `leave-conversation` rpc on the remote +conversation. The remote backend would remove the user and inform all other +federating backends that participate in that conversation of this change by +calling their `on-conversation-updated` rpc. diff --git a/docs/src/understand/federation/architecture.md b/docs/src/understand/federation/architecture.md index 5d3c7ec762..392806ac91 100644 --- a/docs/src/understand/federation/architecture.md +++ b/docs/src/understand/federation/architecture.md @@ -1,67 +1,54 @@ -(architecture-and-network)= +(federation-architecture)= +# Federation Achitecture -# Architecture and Network +(glossary_backend)= -(federation-architecture)= +## Backends -## Architecture +In the following we call a **backend** the set of servers, databases and DNS +configurations that together form one single Wire Server entity as seen from +outside. It can also be called a Wire \"instance\" or \"server\" or \"Wire +installation\". Every resource (e.g. users, conversations, assets and teams) +exists and is *owned* by a single backend, which we can refer to as that +resource\'s backend. -To facilitate connections between federated backends, two new components -are added to each backend: -{ref}`federation_ingress` and -{ref}`federator`. The -*Federation Ingress* is, as the name suggests the ingress point for -incoming connections from other backends, which are then forwarded to -the *Federator*. The *Federator* then further processes the requests. In -addition, the *Federator* also acts as *egress* point for requests from +The communication between federated backends is facilitated by two components in +each backend: {ref}`federation_ingress` and {ref}`federator`. The +*Federation Ingress* is, as the name suggests the +ingress point for incoming connections from other backends, which are then +forwarded to the *Federator*. The *Federator* forwards requests +to internal components. It also acts as a *egress* point for requests from internal backend components to other, remote backends. -![image](img/federated-backend-architecture.png){width="100.0%"} +![image](img/federated-backend-architecture.png) (backend-domains)= -### Backend domains +(glossary_infra_domain)= +(glossary_backend_domain)= -Each backend has two domain strings: an *infrastructure domain* and a -*backend domain*. +## Backend domains -The *infrastructure domain* is the domain name under which the backend +Each backend has two domain: an {ref}`infrastructure domain ` and a +{ref}`backend domain `. + +The **infrastructure domain** (short **infra domain**) is the domain name under which the backend is actually reachable via the network. It is also the domain name that each backend uses in authenticating itself to other backends. -Similarly, there is the *backend domain*, which is used to qualify the +Similarly, there is the **backend domain**, which is used to {ref}`qualify ` the names and identifiers of users local to an individual backend in the -context of federation. For example, a user with (unqualified) user name -*jane_doe* at a backend with backend domain *company-a.com* has the -qualified user name *jane_doe@company-a.com*, which is visible to users -of other backends in the context of federation. - -See -{ref}`qualified-identifiers-and-names` -The distinction between the two domains allows the owner of a (backend) -domain (e.g. *company-a.com*) to host their Wire backend under a -different (infra) domain (e.g. *wire.infra.company-a.com*). +context of federation. -(backend-components)= - -### Backend components - -In addition to the regular components of a Wire backend, two additional -components are added to enable federation with other backends: The -*Federation Ingress* and the *Federator*. Other Wire components use -these two components to contact other backends and respond to queries -originating from remote backends. - -The following subsections briefly introduce the individual components, -their state and their functionality. The semantics of backend-to-backend -communication will be explained in more detail in the Section on -{ref}`federation-api` +The distinction between the two domains allows the owner of a backend +domain, e.g. `example.com`, to host their Wire backend under a +different infra domain, e.g. `wire.infra.example.com`. (federation_ingress)= -#### Federation Ingress +## Federation Ingress -The *Federation Ingress* is a [kubernetes +The *Federation Ingress* is a [Kubernetes ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) and uses [nginx](https://nginx.org/en/) as its underlying software. @@ -72,16 +59,14 @@ other backends. Its functions are: -- terminate TLS connections - - perform mutual {ref}`authentication` as - part of the TLS connection establishment -- forward requests to the local - {ref}`federator` instance, - along with the remote backend\'s client certificate +- to terminate TLS connections +- to perform mutual {ref}`authentication` as part of the TLS connection establishment +- to forward requests to the local {ref}`federator` instance, along with the + remote backend\'s client certificate (federator)= -#### Federator +## Federator The *Federator* performs additional authorization checks after receiving federated requests from the *Federation Ingress* and acts as egress @@ -96,14 +81,11 @@ Additionally, it requires a connection to a DNS resolver to When receiving a request from an internal component, the *Federator* will: -1. If enabled, ensure the target domain is in the - {ref}`allow-list` -2. {ref}`discover ` the other - backend, -3. establish a - {ref}`mutually authenticated channel ` to the other backend using its client certificate, -4. send the request to the other backend and -5. forward the response back to the originating component (and +1. If enabled, ensure the target domain is in the allow list, +2. Discover the other backend, +3. Establish a {ref}`mutually authenticated channel ` to the other backend using its client certificate, +4. Send the request to the other backend and +5. Forward the response back to the originating component (and eventually to the originating Wire client). The *Federator* also implements the authorization logic for incoming @@ -112,20 +94,20 @@ the internal components. The *Federator* will, for incoming requests from remote backends (forwarded via the local {ref}`Federation Ingress `): -1. {ref}`Discover ` the mapping +1. Discover the mapping between backend domain claimed by the remote backend and its infra domain, -2. verify that the discovered infra domain matches the domain in the +2. Verify that the discovered infra domain matches the domain in the remote backend\'s client certificate, -3. if enabled, ensure that the backend domain of the other backend is - in the {ref}`allow list `, -4. forward requests to other wire-server components. +3. If enabled, ensure that the backend domain of the other backend is + in the allow list. +4. Forward requests to other wire-server components. (other-wire-server)= -#### Other wire-server components +## Service components -Components such as \'brig\', \'galley\', or \'gundeck\' are responsible +Components such as Brig, Galley, Cargohold are responsible for actual business logic and interfacing with databases and non-federation related external services. See [source code documentation](https://github.com/wireapp/wire-server). In the context @@ -134,205 +116,8 @@ of federation, their functions include: - For incoming requests from other backends: {ref}`per-request authorization` - Outgoing requests to other backends are always sent via a local - {ref}`Federator` instance. + Federator instance. For more information of the functionalities provided to remote backends through their *Federator*, see the {ref}`federated API documentation`. - -(backend-to-backend-communication)= - -## Backend to backend communication - -We require communication between the *Federator* of one (sending) -backend and the ingress of another (receiving) backend to be both -mutually authenticated and authorized. More specifically, both backends -need to ensure the following: - -- **Authentication** - - Determine the identity (infra domain name) of the other backend. - -- **Discovery** - - Ensure that the other backend is authorized to represent the backend - domain claimed by the other backend. - -- **Authorization** - - Ensure that this backend is authorized to federate with the other backend. - -(authentication)= - -### Authentication - -```{warning} -As of January 2022, the implementation of mutual backend-to-backend authentication is still subject to change. The behaviour described in this section should be considered a draft specification only. -``` - -Authentication between Wire backends is achieved using the mutual -authentication feature of TLS as defined in [RFC -8556](https://tools.ietf.org/html/rfc8446). - -In particular, this means that the ingress of each backend needs to be -provisioned with one or more certificates which the ingress trusts to -authenticate certificates provided by other backends when accepting -incoming connections. - -Conversely, every *Federator* needs to be provisioned with a (client) -certificate which it uses to authenticate itself towards other backends. - -Note that the client certificate is expected to be issued with the -backend\'s infra domain as one of the subject alternative names (SAN), -which is defined in [RFC 5280](https://tools.ietf.org/html/rfc5280). - -If a receiving backend fails to authenticate the client certificate, it should -reply with an *authentication error* (see {ref}`authentication-errors`) - -(discovery)= - -### Discovery - -The discovery process allows a backend to determine the infra domain of -a given backend domain. - -This step is necessary in two scenarios: - -- A backend would like to establish a connection to another backend - that it only knows the backend domain of. This is the case, for - example, when a user of a local backend searches for a - {ref}`qualified username `, which only includes that user\'s backend\'s backend - domain. -- When receiving a message from another backend that authenticates - with a given infra domain and claims to represent a given backend - domain, a backend would like to ensure the backend domain owner - authorized the owner of the infra domain to run their Wire backend. - -To make discovery possible, any party hosting a Wire backend has to -announce the infra domain via a DNS *SRV* record as defined in [RFC -2782](https://tools.ietf.org/html/rfc2782) with -`service = wire-server-federator, proto = tcp` and with `name` pointing -to the backend\'s domain and *target* to the backend\'s infra domain. - -For example, Company A with backend domain *company-a.com* and infra -domain *wire.company-a.com* could publish - -``` bash -_wire-server-federator._tcp.company-a.com. 600 IN SRV 10 5 443 federator.wire.company-a.com. -``` - -A backend can then be discovered, given its domain, by issuing a DNS -query for the SRV record specifying the *wire-server-federator* service. - -(dns-scope)= - -#### DNS Scope - -The network scope of the SRV record (as well as that of the DNS records -for backend and infra domain), depends on the desired federation -topology in the same way as other parameters such as the availability of -the CA certificate that allows authentication of the *Federation -Ingress*\' server certificate or the *Federator*\'s client certificate. -The general rule is that the SRV entry should be \"visible\" from the -point of view of the desired federation partners. The exact scope -strongly depends on the network architecture of the backends involved. - -(srv-ttl-and-caching)= - -#### SRV TTL and Caching - -After retrieving the SRV record for a given domain, the local backend -caches the *backend domain \<\--\> infra domain* mapping for the -duration indicated in the TTL field of the record. - -Due to this caching behaviour, the TTL value of the SRV record dictates -at which intervals remote backends will refresh their mapping of the -local backend\'s backend domain to infra domain. As a consequence a -value in the order of magnitude of 24 hours will reduce the amount of -overhead for remote backends. - -On the other hand in the setup phase of a backend, or when a change of -infra domain is required, a TTL value in the magnitude of a few minutes -allows remote backends to recover more quickly from a change of infra -domain. - -(authorization)= - -### Authorization - -After an incoming connection is authenticated, a second step is required -to ensure that the sending backend is authorized to connect to the -receiving backend. As the backend authenticates using its infra domain, -but the allow list contains backend domains (which is not necessarily -the same) the sending backend also needs to provide its backend domain. - -To make this possible, requests to remote backends are required to -contain a `Wire-Origin-Domain` header, which contains the remote -backend\'s domain. - -While the receiving backend has authenticated the sending backend as the -infra domain, it is not clear that the sending backend is indeed -authorized by the owner of the backend domain to host the Wire backend -of that particular domain. - -To perform this extra authorization step, the receiving backend follows -the process described in {ref}`discovery` and -checks that the discovered infra domain for the backend domain indicated -in the `Wire-Domain` header is one of the Subject Alternative Names -contained in the sending backend\'s client certificate. If this is not -the case, the receiving backend replies with a *discovery error* (see {ref}`authentication-errors`) - -Finally, the receiving backend checks if the domain of the sending -backend is in the {ref}`allow-list` and replies -with an `*authorization error*` (see {ref}`authentication-errors`) if it is not. - - -(allow-list)= - -#### Domain Allow List - -Federation can happen between any backends on a network (e.g. the open -internet); or it can be restricted via server configuration to happen -between a specified set of domains on an \'allow list\'. If an allow -list is configured, then: - -- outgoing requests will only happen if the requested domain is - contained in the allow list. -- incoming requests: if the domain of the sending backend is not in - the allow list, any request originating from that domain is replied - to with an - `authorization error `{.interpreted-text - role="ref"} - -(per-request-authorization)= - -#### Per-request authorization - -In addition to the general authorization step that is performed by the -federator when a new, mutually authenticated TLS connection is -established, the component processing the request performs an -additional, per-request authorization step. - -How this step is performed depends on the API endpoint, the contents of -the request and the context in which it is made. - -See the documentation of the individual {ref}`API endpoints ` for -details. - -(example)= - -### Example - -The following is an example for the message and information flow between -a backend with backend domain `a.com` and infra domain `infra.a.com` and -another backend with backend domain `b.com` and infra domain -`infra.b.com`. - -The content and format of the message is meant to be representative. For -the definitions of the actual payloads, please see the {ref}`federation -API` section. - -The scenario is that the brig at `infra.a.com` has received a user -search request from *Alice*, one of its clients. - -![image](img/federation-flow.png) diff --git a/docs/src/understand/federation/backend-communication.md b/docs/src/understand/federation/backend-communication.md new file mode 100644 index 0000000000..7fa3e71cb9 --- /dev/null +++ b/docs/src/understand/federation/backend-communication.md @@ -0,0 +1,167 @@ +(backend-to-backend-communication)= + +# Backend to backend communication + +We require communication between the {ref}`federator` of one (sending) +backend and the {ref}`federation_ingress` of another (receiving) backend to be both +mutually authenticated and authorized. More specifically, both backends +need to ensure the following: + +- **Authentication** + + Determine the identity (infra domain name) of the other backend. + +- **Discovery** + + Ensure that the other backend is authorized to represent the backend + domain claimed by the other backend. + +- **Authorization** + + Ensure that this backend is authorized to federate with the other backend. + +(authentication)= + +## Authentication + +Authentication between Wire backends is achieved using the mutual +authentication feature of TLS as defined in [RFC +8556](https://tools.ietf.org/html/rfc8446). + +In particular, this means that the ingress of each backend needs to be +provisioned with one or more trusted root certificates to authenticate +certificates provided by other backends when accepting incoming connections. + +Conversely, every *Federator* needs to be provisioned with a client +certificate which it uses to authenticate itself towards other backends. + +Note that the client certificate is required to be issued with the backend\'s +infra domain as one of the subject alternative names (SAN), which is defined in +[RFC 5280](https://tools.ietf.org/html/rfc5280). + +See {ref}`federation-certificate-setup` for technical instructions. + +If a receiving backend fails to authenticate the client certificate, it fails the request +with an `AuthenticationFailure` error. + +(discovery)= + +## Discovery + +The discovery process allows a backend to determine the infra domain of +a given backend domain. + +This step is necessary in two scenarios: + +- A backend would like to establish a connection to another backend + that it only knows the backend domain of. This is the case, for + example, when a user of a local backend searches for a + {ref}`qualified username `, which only includes that user\'s backend\'s backend + domain. +- When receiving a message from another backend that authenticates + with a given infra domain and claims to represent a given backend + domain, a backend would like to ensure the backend domain owner + authorized the owner of the infra domain to run their Wire backend. + +To make discovery possible, any party hosting a Wire backend has to +announce the infra domain via a DNS *SRV* record as defined in [RFC +2782](https://tools.ietf.org/html/rfc2782) with +`service = wire-server-federator, proto = tcp` and with `name` pointing +to the backend\'s domain and *target* to the backend\'s infra domain. + +For example, Company A with backend domain *company-a.com* and infra +domain *wire.company-a.com* could publish + +``` bash +_wire-server-federator._tcp.company-a.com. 600 IN SRV 10 5 443 federator.wire.company-a.com. +``` + +A backend can then be discovered, given its domain, by issuing a DNS +query for the SRV record specifying the *wire-server-federator* service. + +In case this process fails the Federator fails to forward the request with a `DiscoveryFailure` error. + +(dns-scope)= + +### DNS Scope + +The network scope of the SRV record (as well as that of the DNS records +for backend and infra domain), depends on the desired federation +topology in the same way as other parameters such as the availability of +the CA certificate that allows authentication of the *Federation +Ingress*\' server certificate or the *Federator*\'s client certificate. +The general rule is that the SRV entry should be \"visible\" from the +point of view of the desired federation partners. The exact scope +strongly depends on the network architecture of the backends involved. + +(srv-ttl-and-caching)= + +### SRV TTL and Caching + +After retrieving the SRV record for a given domain, the local backend +caches the *backend domain \<\--\> infra domain* mapping for the +duration indicated in the TTL field of the record. + +Due to this caching behavior, the TTL value of the SRV record dictates +at which intervals remote backends will refresh their mapping of the +local backend\'s backend domain to infra domain. As a consequence a +value in the order of magnitude of 24 hours will reduce the amount of +overhead for remote backends. + +On the other hand in the setup phase of a backend, or when a change of infra +domain is required, a TTL value in the magnitude of a few minutes allows remote +backends to recover more quickly from a change of the infra domain. + +(authorization)= + +(allow-list)= + +## Authorization + +After an incoming connection is authenticated the backend authorizes the +request. It does so by verifying that the backend domain of the sender is +contained in the {ref}`domain allow list `. + +Since the request is authenticated only by the infra domain the sending backend +is required to add its backend domain as a `Wire-Origin-Domain` header to the +request. The receiving backend follows the process described in {ref}`discovery` +and verifies that the discovered infra domain for the backend domain indicated +in the `Wire-Origin-Domain` header is one of the Subject Alternative Names +contained in the client certificate used to sign the request. If this is not the +case, the receiving backend fails the request with a `ValidationError`. + +(per-request-authorization)= + +### Per-request authorization + +In addition to the general authorization step that is performed by the +federator when a new, mutually authenticated TLS connection is +established, the component processing the request performs an +additional, per-request authorization step. + +How this step is performed depends on the API endpoint, the contents of +the request and the context in which it is made. + +See the documentation of the individual {ref}`API endpoints ` for +details. + +(federation-back2back-example)= + +## Example + +The following is an example for the message and information flow between +a backend with backend domain `a.com` and infra domain `infra.a.com` and +another backend with backend domain `b.com` and infra domain +`infra.b.com`. + +The content and format of the message is meant to be representative. For +the definitions of the actual payloads, please see the {ref}`federation +API` section. + +The scenario is that the brig at `infra.a.com` has received a user +search request from *Alice*, one of its clients. + +```{image} img/federation-flow.png +:width: 100% +:align: center +``` diff --git a/docs/src/understand/federation/glossary.md b/docs/src/understand/federation/glossary.md deleted file mode 100644 index cfb0cdea6f..0000000000 --- a/docs/src/understand/federation/glossary.md +++ /dev/null @@ -1,108 +0,0 @@ -(glossary)= - -# Federation Glossary - -(glossary_backend)= -## Backend -> A set of servers, databases and DNS configurations together forming one single -> Wire Server entity as seen from outside. This set of servers can be owned and -> administrated by different legal entities in different countries. Sometimes -> also called a Wire \"instance\" or \"server\" or \"Wire installation\". Every -> resource (e.g. users, conversations, assets and teams) exists and is owned by -> one specific backend, which we can refer to as that resource\'s backend - -(glossary_backend_domain)= -## Backend Domain - -> The domain of a backend, which is used to qualify the names and -> identifiers of resources (users, clients, groups, etc) that are local -> to a given backend. See also -> {ref}`consequences-backend-domain` - -(glossary_infra_domain)= - -## Infrastructure Domain or Infra Domain - -> The domain under which the -> `Federator `{.interpreted-text role="ref"} of a -> given backend is reachable (via that backend\'s -> `Ingress `{.interpreted-text role="ref"}) -> for other, remote backends. - -(glossary_federation_ingress)= - -## Federation Ingress - -> Federation Ingress is the first point of contact of a given `backend -> `{.interpreted-text role="ref"} for other, remote -> backends. It also deals with the `authentication`{.interpreted-text -> role="ref"} of incoming requests. See -> `here `{.interpreted-text role="ref"} for more -> information. - -(glossary_federator)= - -## Federator - -> The [Federator]{.title-ref} is the local point of contact for -> `other backend -> components `{.interpreted-text role="ref"} that -> want to make calls to remote backends. It is also the component that -> deals with the `authorization`{.interpreted-text role="ref"} of -> incoming requests from other backends after they have passed the -> `Federation Ingress -> `{.interpreted-text role="ref"}. See -> `here `{.interpreted-text role="ref"} for more information. - -(glossary_asset)= -## Asset - -> Any file or image sent via Wire (uploaded to and downloaded from a -> backend). - -(glossary_qualified-user-id)= -## Qualified User Identifier (QUID) - -> A combination of a UUID (unique on the user\'s backend) and a domain. - -(glossary_qualified-user-name)= -## Qualified User Name (QUN) - -> A combination of a name that is unique on the user\'s backend and a -> domain. The name is a string consisting of 2-256 characters which are -> either lower case alphanumeric, dashes, underscores or dots. See -> [here](https://github.com/wireapp/wire-server/blob/f683299a03207acb505254ff3121213383d0b672/libs/types-common/src/Data/Handle.hs#L76-L93) -> for the code defining the rules for user names. Note that in the -> wire-server source code, user names are called \'Handle\' and -> qualified user names \'Qualified Handle\'. - -(glossary_qualified-client-id)= -## Qualified Client Identifier (QDID) - -> A combination of a client identifier (a hash of the public key -> generated for a user\'s client) concatenated with a dot and the QUID -> of the associated user. - -(glossary_qualified-group-id)= -## Qualified Group Identifier (QGID) - -> The string [backend-domain.com/groups/]{.title-ref} concatenated with -> a UUID that is unique on a given backend. - -(glossary_qualified-conversation-id)= -## Qualified Conversation Identifier (QCID) - -> The same as a `QGID `{.interpreted-text -> role="ref"}. - -(glossary_qualified-team-id)= -## Qualified Team Identifier (QTID) - -> The string [backend-domain.com/teams/]{.title-ref} concatenated with a -> UUID that is unique on a given backend. - -(glossary_display-name)= -## (User) Profile/Display Name - -> The profile/display name of a user is a UTF-8 encoded string with -> 1-128 characters. diff --git a/docs/src/understand/federation/img/federation-apis-flow.png b/docs/src/understand/federation/img/federation-apis-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..faf81be98897896a7493840ef6a67b5dfaff28fc GIT binary patch literal 64907 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

>J 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 literal 0 HcmV?d00001 diff --git a/docs/src/understand/federation/img/federation-apis-flow.txt b/docs/src/understand/federation/img/federation-apis-flow.txt new file mode 100644 index 0000000000..5771f1cb4b --- /dev/null +++ b/docs/src/understand/federation/img/federation-apis-flow.txt @@ -0,0 +1,32 @@ +title: Federated request from galley to remote brig + +Galley@a.com -> Federator@a.com: request + +note: +- API: From component to Federator +- `/rpc/b.com/brig/get-user-by-handle` + +Federator@a.com -> Federator@b.com: federated request + + +note: +- API: Federation API +- `Wire-Origin-Domain: a.com` +- `/federation/brig/get-user-by-handle` + +//group: TLS-secured backend-internal channel + + +Federator@b.com -> Brig@b.com: request + +note: +- API: Federator to component +- `Wire-Origin-Domain: a.com` +- `/federation/get-user-by-handle` + + +Brig@b.com -> Federator@b.com: response + +Federator@b.com -> Federator@a.com: response + +Federator@a.com -> Galley@a.com: response diff --git a/docs/src/understand/federation/img/federation-flow.png b/docs/src/understand/federation/img/federation-flow.png index f6558c63df36f7b36f009e74f2f97cd4a0a92305..25a0014e24be2657af54cdd2d8b168ba8a8af233 100644 GIT binary patch literal 142892 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∾ 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

v(&NT8UX?Y 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 P-pYLRuwz4}|M5Qn Federator @infra.a.com: (domain="b.com", component="brig", handle="alice") +Brig @infra.a.com -> Federator @infra.a.com: federated request + +note: +- `/rpc/b.com/brig/get-user-by-handle` +- `{"handle": "alice"}` + + +Federator @infra.a.com -> DNS Resolver: DNS lookup + +note: +`SRV _wire-server-federator._tcp.b.com` -Federator @infra.a.com -> DNS Resolver: DNS query: (service: "wire-server-federator", proto: "tcp", name: "b.com") -DNS Resolver -> Federator @infra.a.com: DNS response: (target: "infra.b.com") +DNS Resolver -> Federator @infra.a.com: DNS response: `infra.b.com` Federator @infra.a.com -> Ingress @infra.b.com: mTLS session establishment @@ -15,36 +24,52 @@ Ingress @infra.b.com -> Federator @infra.a.com: mTLS session establishment respo note: The channel between infra.a.com and infra.b.com is now encrypted and mutually authenticated. -Federator @infra.a.com -> Ingress @infra.b.com: (originDomain="a.com", component="brig", path="get-user-by-handle", body="alice") +Federator @infra.a.com -> Ingress @infra.b.com : request + +note: +- `Wire-Origin-Domain: a.com` +- `/federation/brig/get-user-by-handle` //group: TLS-secured backend-internal channel -Ingress @infra.b.com -> Federator @infra.b.com: (domain= "a.com", client_cert="", component="brig", path="get-user-by-handle", body="alice") +Ingress @infra.b.com -> Federator @infra.b.com: request + cert + +note: +- `X-SSL-Certificate: ` //end -Federator @infra.b.com -> DNS Resolver: DNS query: (service: "wire-server-federator", proto: "tcp", name: "a.com") +Federator @infra.b.com -> DNS Resolver: DNS query -DNS Resolver -> Federator @infra.b.com: DNS response: (target: "infra.a.com") +note: +`SRV _wire-server-federator._tcp.a.com` -//group: TLS-secured backend-internal channel +DNS Resolver -> Federator @infra.b.com: DNS response: `infra.a.com` -note: Check that the content of the _target_ field in the DNS response is one of the SANs in the client cert and that the content of the _domain_ field is on the allow list. +//group: TLS-secured backend-internal channel -Federator @infra.b.com -> Brig @infra.b.com: (originDomain= "a.com", component="brig", path="federation/get-user-by-handle" handle="alice") +note: +Check that +- that the `infra.a.com` is listed as one of SANs in the client cert +- `a.com` is in the allow list +Federator @infra.b.com -> Brig @infra.b.com: request -note: Perform per-request authorization. +note: +- `Wire-Origin-Domain: a.com` +- `/federation/get-user-by-handle` +- `{"handle": "alice"}` -Brig @infra.b.com -> Federator @infra.b.com: (UserProfile(Alice)) +note: Brig perform per-request authorization. -Federator @infra.b.com -> Ingress @infra.b.com: (UserProfile(Alice)) +Brig @infra.b.com -> Federator @infra.b.com: response: alice's user profile +Federator @infra.b.com -> Ingress @infra.b.com: response: alice's user profile //end -Ingress @infra.b.com -> Federator @infra.a.com: (UserProfile(Alice)) +Ingress @infra.b.com -> Federator @infra.a.com: response: alice's user profile note: Via the encrypted, mutually authenticated channel. -Federator @infra.a.com -> Brig @infra.a.com: (UserProfile(Alice)) +Federator @infra.a.com -> Brig @infra.a.com: response: alice's user profile diff --git a/docs/src/understand/federation/index.md b/docs/src/understand/federation/index.md index 48e78ea649..a1dc6b6cfa 100644 --- a/docs/src/understand/federation/index.md +++ b/docs/src/understand/federation/index.md @@ -2,23 +2,29 @@ # Wire Federation -Wire Federation, once implemented, aims to allow multiple Wire-server {ref}`backends ` to federate with each other. That means that a user 1 registered on backend A and a user 2 registered on backend B should be able to interact with each other as if they belonged to the same backend. +Wire Federation aims to allow multiple Wire-server +{ref}`backends ` to federate with each other: Users on on +different backends are be able to interact with each other as if they +are on the the same backend. -```{note} -Federation is as of January 2022 still work in progress, since the implementation of federation is ongoing, and certain design decision are still subject to change. Where possible documentation will indicate the state of implementation. +Federated backends are be able to identify, discover and authenticate +one-another using the domain names under which they are reachable via the +network. To enable federation, administrators of a Wire backend can decide to +either specifically list the backends that they want to federate with, or to +allow federation with all Wire backends reachable from the network. See +{ref}`configure-federation`. -Some sections of the documentation are still incomplete (indicated with a 'TODO' comment). Check back later for updates. +```{note} +The Federation development is work in progress. ``` -% comment: The toctree directive below takes a list of the pages you want to appear in order, -% and '*' is used to include any other pages in the federation directory in alphabetical order - ```{toctree} -:glob: true -:maxdepth: 2 -:numbered: true - -introduction +--- +maxdepth: 2 +numbered: true +glob: true +--- architecture +backend-communication * ``` diff --git a/docs/src/understand/federation/introduction.md b/docs/src/understand/federation/introduction.md deleted file mode 100644 index 02e4d057a8..0000000000 --- a/docs/src/understand/federation/introduction.md +++ /dev/null @@ -1,45 +0,0 @@ -(introduction)= - -# Introduction - -Federation is a feature that allows a collection of Wire backends to -enable the establishment of connections among their respective users. - -(goals)= - -## Goals - -If two Wire backends A and B are *federated*, the goal is for users of -backend A to be able to communicate with users of backend B and -vice-versa in the same way as if they were both part of the same -backend. - -Federated backends should be able to identify, discover and authenticate -one-another using the domain names under which they are reachable via -the network. - -To enable federation, administrators of a Wire backend can decide to -either specifically list the backends that they want to federate with, -or to allow federation with all Wire backends reachable from the -network. - -Federation is facilitated by two backend components: the *Federation -Ingress*, which, as the name suggests, acts as ingress point for -federated traffic and the *Federator*, which acts as egress point and -processes all ingress requests from the Federation Ingress after the -authentication step. - -(non-goals)= - -## Non-Goals - -We aim to integrate federation into the Wire backend following a -step-by-step process as described in the -{ref}`federation-roadmap`. -Early versions are not meant to enable a completely open federation, but -rather a closed network of federated backends with a restricted set of -features. - -The aim of federation is not to replace the existing organizational -structures for Wire users such as teams and groups, but rather to -complement them. diff --git a/docs/src/understand/federation/replace.sh b/docs/src/understand/federation/replace.sh deleted file mode 100644 index 7b4da2997f..0000000000 --- a/docs/src/understand/federation/replace.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env sh - -set -x - -for f in *.rst; do - if [ "$f" = "index.rst" ]; then - continue; - fi - pandoc -f rst -t commonmark_x < "$f" > "${f%.rst}.md" - rm "$f" -done diff --git a/docs/src/understand/federation/roadmap.md b/docs/src/understand/federation/roadmap.md deleted file mode 100644 index 87fb85834a..0000000000 --- a/docs/src/understand/federation/roadmap.md +++ /dev/null @@ -1,112 +0,0 @@ -(federation-roadmap)= - -# Implementation Roadmap - -Internally at Wire, we have divided implemention of federation into -multiple milestones. Only the milestone on which implementation has -started will be shown here (as later milestones are subject to internal -change and re-ordering) - -(m1-federation-with-proteus-mvp)= - -## M1 federation with proteus MVP - -The first milestone **M1** is a minimum-viable-product that allows users -on different Wire backends to send textual messages to users on other -backends. - -M1 included support for: - -- user search -- creating group conversations -- message sending -- visual UX for showing federation. -- a way for on-premise (self-hosted) installations of wire to try out - this implementation of federation by explicitly enabling it via - configuration flags. -- Android, Web and iOS will be supported -- server2server discovery and authentication -- a way to specify an allow list of backends to federate with - -(m2-federation-with-callingconferencing-and-assets)= - -## M2 federation with calling/conferencing and assets - -The second milestone **M2** focused on: - -- federated calling -- federated conferencing -- basic federated asset support. - -**M2** also incorporated a previous interim release which added the -following in a federated environment: - -- likes -- mentions -- read receipts and delivery notifications -- pings -- edit and delete messages - -Caveats: - -- Message delivery guarantees are weak if any backends are temporarily - unavailable. -- If any backends are unavailable, data inconsistencies may occur. -- Federation with the production cloud version of wire.com is not yet - supported. -- Federated conferencing requires an SFT in each domain represented in - the conversation. The caller\'s SFT is the \"anchor\" SFT, to which - federated SFTs connect: - - SFTs must have valid certificates suitable for mutual - authentication with federated SFTs. - - Currently all video streams are exchanged between the anchor SFT - and each federated SFT. The SFTs select the relevant streams for - each client as today, but inter-SFT traffic could use - substantially more bandwidth than an SFT to client stream. - - The administrator needs to open ports between their SFTs and - federated SFTs for signalling and media. -- Assets will be stored on the backend of the sender and fetched via - the sender\'s backend with every access (there is no caching on a - federated domain). If federated domains have different policies for - allowed asset types or sizes, a user may receive notification of an - asset which it is not allowed to fetch or view. - - -```{note} -A rough (Backend) Implementation Status as of January 2022: - -Tested in M2 scope: - -- Federator as Egress, and Ingress support to allow - backend-backend communication -- Long-running test environments -- Backend Discovery via SRV records -- Backend allow list support -- User search via exact handle -- Get user profile, user clients, and prekeys for their clients -- Create conversation with remote users -- Send a message in a conversation with remote users -- Server2server authentication -- connections -- Assets -- Calling -- Conferencing - -Partially done: - -- client-server API changes for federation -- Other conversation features (removing users, archived/muted, - \...) -``` - -(additional-milestones)= - -## Additional Milestones - -Some additional milestones planned include the following features: - -- support more features (guest users, bots, \...) -- support better message delivery guarantees -- federation API versioning strategy -- support for wire-server installations to federate with wire.com -- MLS support From 4bcf4ea76c1e8f43cff4066d0ae77f20c06923a9 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Thu, 12 Jan 2023 17:58:55 +0100 Subject: [PATCH 04/38] Inline "Administrator's Guide" one level and integrate post-install in the install guide --- docs/src/how-to/administrate/index.md | 2 +- docs/src/how-to/index.md | 20 ------- docs/src/how-to/install/index.md | 3 +- .../post-install.md} | 57 ++++++++++++++++++- docs/src/how-to/post-install/index.md | 15 ----- docs/src/how-to/post-install/ntp-check.md | 44 -------------- docs/src/index.md | 6 +- 7 files changed, 62 insertions(+), 85 deletions(-) delete mode 100644 docs/src/how-to/index.md rename docs/src/how-to/{post-install/logrotation-check.md => install/post-install.md} (52%) delete mode 100644 docs/src/how-to/post-install/index.md delete mode 100644 docs/src/how-to/post-install/ntp-check.md diff --git a/docs/src/how-to/administrate/index.md b/docs/src/how-to/administrate/index.md index 5f6dd1ab72..79a04fa649 100644 --- a/docs/src/how-to/administrate/index.md +++ b/docs/src/how-to/administrate/index.md @@ -1,4 +1,4 @@ -# Administrate components after successful installation +# Administration ```{toctree} :glob: true diff --git a/docs/src/how-to/index.md b/docs/src/how-to/index.md deleted file mode 100644 index 46bf098a9a..0000000000 --- a/docs/src/how-to/index.md +++ /dev/null @@ -1,20 +0,0 @@ -# Administrator's Guide - -Documentation on the installation, deployment and administration of Wire -server components. - -```{warning} -If you already installed Wire by using `poetry`, please refer to the -[old version](https://docs.wire.com/versions/install-with-poetry/how-to/index.html) of -the installation guide. -``` - -```{toctree} -:glob: true -:maxdepth: 2 - - How to install wire-server - How to verify your wire-server installation - How to administrate servers after successful installation - How to connect the public wire clients to your wire-server installation -``` diff --git a/docs/src/how-to/install/index.md b/docs/src/how-to/install/index.md index 183215c603..0b4119e9ee 100644 --- a/docs/src/how-to/install/index.md +++ b/docs/src/how-to/install/index.md @@ -1,4 +1,4 @@ -# Installing wire-server +# Installation ```{toctree} :glob: true @@ -27,4 +27,5 @@ How to install and set up Legal Hold Managing authentication with ansible Using tinc Troubleshooting during installation +Verifying your installation ``` diff --git a/docs/src/how-to/post-install/logrotation-check.md b/docs/src/how-to/install/post-install.md similarity index 52% rename from docs/src/how-to/post-install/logrotation-check.md rename to docs/src/how-to/install/post-install.md index 17bcdde7db..6a513f0ece 100644 --- a/docs/src/how-to/post-install/logrotation-check.md +++ b/docs/src/how-to/install/post-install.md @@ -1,12 +1,63 @@ +# Verifying your installation + +After a successful installation of wire-server and its components, there are some useful checks to be run to ensure the proper functioning of the system. Here's a non-exhaustive list of checks to run on the hosts: + + +(ntp-check)= + +## NTP Checks + +Ensure that NTP is properly set up on all nodes. Particularly for Cassandra **DO NOT** use anything else other than ntp. Here are some helpful blogs that explain why: + +> - +> - + +### How can I see if NTP is correctly set up? + +This is an important part of your setup, particularly for your Cassandra nodes. You should use `ntpd` and our ansible scripts to ensure it is installed correctly - but you can still check it manually if you prefer. The following 2 sub-sections explain both approaches. + +#### I used your ansible scripts and prefer to have automated checks + +Then the easiest way is to use [this ansible playbook](https://github.com/wireapp/wire-server-deploy/blob/develop/ansible/cassandra-verify-ntp.yml) + +#### I am not using ansible and like to SSH into hosts and checking things manually + +The following shows how to check for existing servers connected to (assumes `ntpq` is installed) + +```sh +ntpq -pn +``` + +which should yield something like this: + +```sh + remote refid st t when poll reach delay offset jitter +============================================================================== + time.example. .POOL. 16 p - 64 0 0.000 0.000 0.000 ++ 2 u 498 512 377 0.759 0.039 0.081 +* 2 u 412 512 377 1.251 -0.670 0.063 +``` + +if your output shows \_ONLY\_ the entry with a `.POOL.` as `refid` and a lot of 0s, something is probably wrong, i.e.: + +```sh + remote refid st t when poll reach delay offset jitter +============================================================================== + time.example. .POOL. 16 p - 64 0 0.000 0.000 0.000 +``` + +What should you do if this is the case? Ensure that `ntp` is installed and that the servers in the pool (typically at `/etc/ntp.conf`) are reachable. + + (logrotation-check)= -# Logs and Data Protection checks +## Logs and Data Protection checks On Wire.com, we keep logs for a maximum of 72 hours as described in the [privacy whitepaper](https://wire.com/en/security/) We recommend you do the same and limit the amount of logs kept on your servers. -## How can I see how far in the past access logs are still available on my servers? +### How can I see how far in the past access logs are still available on my servers? Look at the timestamps of your earliest nginz logs: @@ -23,7 +74,7 @@ If the timestamp is more than 3 days in the past, your logs are kept for unneces You can use [the kubernetes_logging.yml ansible playbook](https://github.com/wireapp/wire-server-deploy/blob/develop/ansible/kubernetes_logging.yml) -### I am not using ansible and like to SSH into hosts and configure things manually +#### I am not using ansible and like to SSH into hosts and configure things manually SSH into one of your kubernetes worker machines. diff --git a/docs/src/how-to/post-install/index.md b/docs/src/how-to/post-install/index.md deleted file mode 100644 index 2dd2009af9..0000000000 --- a/docs/src/how-to/post-install/index.md +++ /dev/null @@ -1,15 +0,0 @@ -(checks)= - -# Verifying your wire-server installation - -After a successful installation of wire-server and its components, there are some useful checks to be run to ensure the proper functioning of the system. Here's a non-exhaustive list of checks to run on the hosts: - -NOTE: This page is a work in progress, more sections to be added soon. - -```{toctree} -:glob: true -:maxdepth: 1 - - Verifying NTP - Verifying data retention for logs don't exceed 72 hours -``` diff --git a/docs/src/how-to/post-install/ntp-check.md b/docs/src/how-to/post-install/ntp-check.md deleted file mode 100644 index f093998eea..0000000000 --- a/docs/src/how-to/post-install/ntp-check.md +++ /dev/null @@ -1,44 +0,0 @@ -(ntp-check)= - -# NTP Checks - -Ensure that NTP is properly set up on all nodes. Particularly for Cassandra **DO NOT** use anything else other than ntp. Here are some helpful blogs that explain why: - -> - -> - - -## How can I see if NTP is correctly set up? - -This is an important part of your setup, particularly for your Cassandra nodes. You should use `ntpd` and our ansible scripts to ensure it is installed correctly - but you can still check it manually if you prefer. The following 2 sub-sections explain both approaches. - -### I used your ansible scripts and prefer to have automated checks - -Then the easiest way is to use [this ansible playbook](https://github.com/wireapp/wire-server-deploy/blob/develop/ansible/cassandra-verify-ntp.yml) - -### I am not using ansible and like to SSH into hosts and checking things manually - -The following shows how to check for existing servers connected to (assumes `ntpq` is installed) - -```sh -ntpq -pn -``` - -which should yield something like this: - -```sh - remote refid st t when poll reach delay offset jitter -============================================================================== - time.example. .POOL. 16 p - 64 0 0.000 0.000 0.000 -+ 2 u 498 512 377 0.759 0.039 0.081 -* 2 u 412 512 377 1.251 -0.670 0.063 -``` - -if your output shows \_ONLY\_ the entry with a `.POOL.` as `refid` and a lot of 0s, something is probably wrong, i.e.: - -```sh - remote refid st t when poll reach delay offset jitter -============================================================================== - time.example. .POOL. 16 p - 64 0 0.000 0.000 0.000 -``` - -What should you do if this is the case? Ensure that `ntp` is installed and that the servers in the pool (typically at `/etc/ntp.conf`) are reachable. diff --git a/docs/src/index.md b/docs/src/index.md index c5b3d5b4db..135a0aa03f 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -23,7 +23,11 @@ This documentation may be expanded in the future to cover other aspects of Wire. :maxdepth: 1 Release notes -Administrator's Guide + +Installation +Administration +Connecting Wire Clients +Optional Configuration Understanding wire-server components Single-Sign-On and user provisioning Client API documentation From 40201f59d0a476a9f0132d5df219a6887de8d7ab Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Thu, 12 Jan 2023 18:10:16 +0100 Subject: [PATCH 05/38] Move configuration options out of install --- docs/src/{how-to/install => }/configuration-options.md | 0 docs/src/how-to/install/index.md | 1 - 2 files changed, 1 deletion(-) rename docs/src/{how-to/install => }/configuration-options.md (100%) diff --git a/docs/src/how-to/install/configuration-options.md b/docs/src/configuration-options.md similarity index 100% rename from docs/src/how-to/install/configuration-options.md rename to docs/src/configuration-options.md diff --git a/docs/src/how-to/install/index.md b/docs/src/how-to/install/index.md index 0b4119e9ee..2758ad819a 100644 --- a/docs/src/how-to/install/index.md +++ b/docs/src/how-to/install/index.md @@ -15,7 +15,6 @@ dependencies (production) How to install wire-server using Helm (production) How to monitor wire-server (production) How to see centralized logs for wire-server -(production) Other configuration options Server and team feature settings Messaging Layer Security (MLS) Web app settings From 460e3b8cace832e401944abe73b8b2b1c8b989ff Mon Sep 17 00:00:00 2001 From: fisx Date: Fri, 13 Jan 2023 10:40:59 +0100 Subject: [PATCH 06/38] Fedcalls cli tool. (#2973) * fedcalls cli tool. * Cleanup * Output both dot and csv. * Fixup * Fixup * Changelog. * Fixup * Fixup * Update tools/fedcalls/README.md --- cabal.project | 7 +- changelog.d/4-docs/pr-2973 | 1 + nix/local-haskell-packages.nix | 1 + tools/fedcalls/.ormolu | 1 + tools/fedcalls/README.md | 31 +++++ tools/fedcalls/default.nix | 38 ++++++ tools/fedcalls/example.png | Bin 0 -> 110931 bytes tools/fedcalls/fedcalls.cabal | 74 +++++++++++ tools/fedcalls/src/Main.hs | 220 +++++++++++++++++++++++++++++++++ 9 files changed, 370 insertions(+), 3 deletions(-) create mode 100644 changelog.d/4-docs/pr-2973 create mode 120000 tools/fedcalls/.ormolu create mode 100644 tools/fedcalls/README.md create mode 100644 tools/fedcalls/default.nix create mode 100644 tools/fedcalls/example.png create mode 100644 tools/fedcalls/fedcalls.cabal create mode 100644 tools/fedcalls/src/Main.hs diff --git a/cabal.project b/cabal.project index c9f7daf5a0..b1c4c70161 100644 --- a/cabal.project +++ b/cabal.project @@ -42,13 +42,14 @@ packages: , tools/api-simulations/ , tools/db/assets/ , tools/db/auto-whitelist/ - , tools/db/migrate-sso-feature-flag/ - , tools/db/service-backfill/ , tools/db/billing-team-member-backfill/ , tools/db/find-undead/ + , tools/db/inconsistencies/ + , tools/db/migrate-sso-feature-flag/ , tools/db/move-team/ , tools/db/repair-handles/ - , tools/db/inconsistencies/ + , tools/db/service-backfill/ + , tools/fedcalls/ , tools/rex/ , tools/stern/ diff --git a/changelog.d/4-docs/pr-2973 b/changelog.d/4-docs/pr-2973 new file mode 100644 index 0000000000..89fbeb8be6 --- /dev/null +++ b/changelog.d/4-docs/pr-2973 @@ -0,0 +1 @@ +Tool for dumping fed call graphs (dot/graphviz and csv); see README for details \ No newline at end of file diff --git a/nix/local-haskell-packages.nix b/nix/local-haskell-packages.nix index 387f117aa1..aea935c787 100644 --- a/nix/local-haskell-packages.nix +++ b/nix/local-haskell-packages.nix @@ -51,6 +51,7 @@ move-team = hself.callPackage ../tools/db/move-team/default.nix { inherit gitignoreSource; }; repair-handles = hself.callPackage ../tools/db/repair-handles/default.nix { inherit gitignoreSource; }; service-backfill = hself.callPackage ../tools/db/service-backfill/default.nix { inherit gitignoreSource; }; + fedcalls = hself.callPackage ../tools/fedcalls/default.nix { inherit gitignoreSource; }; rex = hself.callPackage ../tools/rex/default.nix { inherit gitignoreSource; }; stern = hself.callPackage ../tools/stern/default.nix { inherit gitignoreSource; }; } diff --git a/tools/fedcalls/.ormolu b/tools/fedcalls/.ormolu new file mode 120000 index 0000000000..157b212d7c --- /dev/null +++ b/tools/fedcalls/.ormolu @@ -0,0 +1 @@ +../../.ormolu \ No newline at end of file diff --git a/tools/fedcalls/README.md b/tools/fedcalls/README.md new file mode 100644 index 0000000000..e43f95e14a --- /dev/null +++ b/tools/fedcalls/README.md @@ -0,0 +1,31 @@ +our swaggger docs contain information about which end-points call +which federation end-points internally. this command line tool +extracts that information from the swagger json and converts it into +two files: dot (for feeding into graphviz), and csv. + +### try it out + +``` +cabal run fedcalls +ls wire-fedcalls.* # these names are hard-coded (sorry!) +dot -Tpng wire-fedcalls.dot > wire-fedcalls.png +``` + +`dot` layouts only work for small data sets (at least without tweaking). for a better one paste into [sketchvis](https://sketchviz.com/new). + +### links + +- `./example.png` +- https://sketchviz.com/new +- https://graphviz.org/doc/info/lang.html +- `/libs/wire-api/src/Wire/API/MakesFederatedCall.hs` + +### swagger-ui + +you can get the same data for the public API in the swagger-ui output. just load the page, open your javascript console, and type: + +``` +window.ui.getConfigs().showExtensions = true +``` + +then drop down on things like normal, and you'll see federated calls. diff --git a/tools/fedcalls/default.nix b/tools/fedcalls/default.nix new file mode 100644 index 0000000000..1fa52660c6 --- /dev/null +++ b/tools/fedcalls/default.nix @@ -0,0 +1,38 @@ +# WARNING: GENERATED FILE, DO NOT EDIT. +# This file is generated by running hack/bin/generate-local-nix-packages.sh and +# must be regenerated whenever local packages are added or removed, or +# dependencies are added or removed. +{ mkDerivation +, aeson +, base +, containers +, gitignoreSource +, imports +, insert-ordered-containers +, language-dot +, lib +, swagger2 +, text +, wire-api +}: +mkDerivation { + pname = "fedcalls"; + version = "1.0.0"; + src = gitignoreSource ./.; + isLibrary = false; + isExecutable = true; + executableHaskellDepends = [ + aeson + base + containers + imports + insert-ordered-containers + language-dot + swagger2 + text + wire-api + ]; + description = "Generate a dot file from swagger docs representing calls to federated instances"; + license = lib.licenses.agpl3Only; + mainProgram = "fedcalls"; +} diff --git a/tools/fedcalls/example.png b/tools/fedcalls/example.png new file mode 100644 index 0000000000000000000000000000000000000000..26bc63134fc15f6e76dcc857c138989e2a94a0ca GIT binary patch literal 110931 zcmeFZbx@wmvM>A+f;+(_5Fog_ySuvtcX#*TA-FpPclQJd?iSqL-QhmTT6^t%>fWkb zr|P>^-yi2mAiVSR^i22k^mPAvNSM5=7y>K~EC>WbkPsJE1c4ygK%h6f&~Jbi$?^bY z5a=DJhqAhpqP{DkorA53xs@@Yle?WUp|P8}2?*r2P>^csg4>!H{A!5C^EN+_9~#0C(qW+VU6#j>qn2pWU^j2 zMe)qO_zg=i3Ew=aKqR7Q29z&ja~XZ<51#aIE|k)FeVaygXjop|Cg?Qoy{>7r>N^at zc(gb==pZW=fjtq z&z>`|Z<~0Ekt&~l7JaIPH0H5NNIJ-;#l04NTY45X}OUo9GpbzG>vMDk(Q`fNm^COVv%fii;{Vj z(t_Cqi^{x$#I*WJ3(NAx2}3!*Z`YHSw(YkJ$&U0FYbQS-42RjLhNqUOYe!ih@ggoX z+~+AvD;gefxK8SRMC-ULTi^1?vd2#j^vQBu*G-Kz#V^R5>{vD~c`P2gdYK$&%&z*p z5?QWi)ZZCNV$P10ou7c?jv))!84O~>M9@Mp>L$E_<)k$22u{yqEKI+DzYiNhJsDq% zAJ4#;EEnq1Eq0>qWvra?=({~K^CY#h@yg;TTl_dQ{G8dCRns6MbU5&<6b@ZEYp+9T zQO91FNYF*nvaWx03Eie8>6`W31!bKZtY{V{V<0S)2L03cB5FxMZ2;^C%%jNP4YY)zVUaLMn-_`BWl5 zDw8682KmwM!D8ELb`BQXaAbU9px*CTf0?Pavo<~9A!8p;-Dj?fpP>2>|3!i`z0S=vmFNdHxocAS=Ri?^#}V`G|%&|5oIm+(*X zC$UqbQkEBGG>FzhVo;7@)m}X@s^Ls68ss)N%=uK1Aw@XBE>=y{7CF0D1 zWXf_xHn_wnI1%;uy3CZiABBx8qj5^OqUsN|L#$A)VU_c>$ai4;b>V7Rd-wUOp#88W*kwhSy9=YIkpj1sV)kS2%~ED#$D`cYn$F<~_;jeB0NqsM~@|>wQwF zzv;$0ORW^%hUXM#p&R_5+^#gQweVrlpnkjX*s}OE_{E*_R?`~4^2?|8)y*d~BTtiO zq4cbqQg@cmy2EE>8<0c3sr~JyEqPh28>US!SVw#^+q}CEbMnv0 za~De^MkiVpd;tllW&!+$pIN99bV#Yw_@(ndhH^4Jq+i2beZ8m*c!JpP0MkLg>U^W# zRSFs&_mu;a7fwU(oRst0iF9VbmUKrY=TF!nv4hDKkzDvvrv&4PpC))YaAx-YGg2PL z`YUIZc&W=LrlMoe>KfeJ7VEGOPrAPB<+u2C^nF;*0j}CBM9*D>E0CW^rXBORA89^8 zW{6x(7Ws>YFi8%aDMa*;?rGmTdM2TQyu=oOvFUN{W z#&Dkc=JB&T^BS}ZN1^l*oGS<48vAtDN3&}9rhG6d4tlKbTOlmyAkC+$UEE$MhlH`Q z7`ZwgHWcSn3S6`izpbS;8;X$g67|px;S(t&OsiT7Wg^@5py6^qjxGI(Iu;kvUX@R+ zIl2ceMtFgX~ZPq3{%Np!s# zQF_frKIz`0Wi2Mmq@4I%!OMvY6br^`P~A}Q(?OyAy}7>|KkgDhExg6mWS^rk z4*jd{XTO%ABVa*Wi``+IY^0_ZgONLA-)axLD!^jvSLF|JSIy->Nw{W{F9jx;z5T=w zJKJ88vLj5akanX;DemIW_fk71-lpFmT#GI&XoaN_|Bj_Ke3%01YzR5%>FMa@IDn70 zyJ*utbiS09MKC zEPTsukdzO_7yqMKbgYWof>-X{pjIM8#1C@1=jIcQ?=;8UbON?Duc%KWFSRG83yTv94hGw8J~TiF%;wM%N?k-T zp8ER`hMgG9`7KR^qEI~Gqe|~ z(msx2En-Uzu_*dR7KI7F2BB8I^gHe|4PKA`4T@*qcdf`U57InBmt_c#kw)jn+ z5nbU&a)Q-^`zMzZ8--FP64+(kt?$O4Wm^m1BJ#KhLdn}E*wbFgy@#U#n?>Y*9Nd

Tbxy{$pAmxnVa6KLeBbjq+C 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 literal 0 HcmV?d00001 diff --git a/tools/fedcalls/fedcalls.cabal b/tools/fedcalls/fedcalls.cabal new file mode 100644 index 0000000000..2e42d6f9bb --- /dev/null +++ b/tools/fedcalls/fedcalls.cabal @@ -0,0 +1,74 @@ +cabal-version: 1.12 +name: fedcalls +version: 1.0.0 +synopsis: + Generate a dot file from swagger docs representing calls to federated instances. + +category: Network +author: Wire Swiss GmbH +maintainer: Wire Swiss GmbH +copyright: (c) 2020 Wire Swiss GmbH +license: AGPL-3 +build-type: Simple + +executable fedcalls + main-is: Main.hs + hs-source-dirs: src + default-extensions: + NoImplicitPrelude + AllowAmbiguousTypes + BangPatterns + ConstraintKinds + DataKinds + DefaultSignatures + DeriveFunctor + DeriveGeneric + DeriveLift + DeriveTraversable + DerivingStrategies + DerivingVia + EmptyCase + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + InstanceSigs + KindSignatures + LambdaCase + MultiParamTypeClasses + MultiWayIf + NamedFieldPuns + OverloadedStrings + PackageImports + PatternSynonyms + PolyKinds + QuasiQuotes + RankNTypes + ScopedTypeVariables + StandaloneDeriving + TupleSections + TypeApplications + TypeFamilies + TypeFamilyDependencies + TypeOperators + UndecidableInstances + ViewPatterns + + ghc-options: + -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates + -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -funbox-strict-fields -threaded -with-rtsopts=-N -with-rtsopts=-T + -rtsopts + + build-depends: + aeson + , base + , containers + , imports + , insert-ordered-containers + , language-dot + , swagger2 + , text + , wire-api + + default-language: Haskell2010 diff --git a/tools/fedcalls/src/Main.hs b/tools/fedcalls/src/Main.hs new file mode 100644 index 0000000000..7a717e75ef --- /dev/null +++ b/tools/fedcalls/src/Main.hs @@ -0,0 +1,220 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Main + ( main, + ) +where + +import Control.Exception (assert) +import Data.Aeson as A +import qualified Data.Aeson.Types as A +import qualified Data.HashMap.Strict.InsOrd as HM +import qualified Data.Map as M +import Data.Swagger + ( PathItem, + Swagger, + _operationExtensions, + _pathItemDelete, + _pathItemGet, + _pathItemHead, + _pathItemOptions, + _pathItemPatch, + _pathItemPost, + _pathItemPut, + _swaggerPaths, + ) +import Imports +import Language.Dot as D +import qualified Wire.API.Routes.Internal.Brig as BrigIRoutes +import qualified Wire.API.Routes.Public.Brig as BrigRoutes +import qualified Wire.API.Routes.Public.Cannon as CannonRoutes +import qualified Wire.API.Routes.Public.Cargohold as CargoholdRoutes +import qualified Wire.API.Routes.Public.Galley as GalleyRoutes +import qualified Wire.API.Routes.Public.Gundeck as GundeckRoutes +import qualified Wire.API.Routes.Public.Proxy as ProxyRoutes +-- import qualified Wire.API.Routes.Internal.Cannon as CannonIRoutes +-- import qualified Wire.API.Routes.Internal.Cargohold as CargoholdIRoutes +-- import qualified Wire.API.Routes.Internal.LegalHold as LegalHoldIRoutes +import qualified Wire.API.Routes.Public.Spar as SparRoutes + +------------------------------ + +main :: IO () +main = do + writeFile "wire-fedcalls.dot" . D.renderDot . mkDotGraph $ calls + writeFile "wire-fedcalls.csv" . toCsv $ calls + +calls :: [MakesCallTo] +calls = assert (calls' == nub calls') calls' + where + calls' = mconcat $ parse <$> swaggers + +swaggers :: [Swagger] +swaggers = + [ -- TODO: introduce allSwaggerDocs in wire-api that collects these for all + -- services, use that in /services/brig/src/Brig/API/Public.hs instead of + -- doing it by hand. + + BrigRoutes.brigSwagger, -- TODO: s/brigSwagger/swaggerDoc/ like everybody else! + CannonRoutes.swaggerDoc, + CargoholdRoutes.swaggerDoc, + GalleyRoutes.swaggerDoc, + GundeckRoutes.swaggerDoc, + ProxyRoutes.swaggerDoc, + SparRoutes.swaggerDoc, + -- TODO: collect all internal apis somewhere else (brig?), and expose them + -- via an internal swagger api end-point. + + BrigIRoutes.swaggerDoc + -- CannonIRoutes.swaggerDoc, + -- CargoholdIRoutes.swaggerDoc, + -- LegalHoldIRoutes.swaggerDoc + ] + +------------------------------ + +data MakesCallTo = MakesCallTo + { -- who is calling? + sourcePath :: String, + sourceMethod :: String, + -- where does the call go? + targetComp :: String, + targetName :: String + } + deriving (Eq, Show) + +------------------------------ + +parse :: Swagger -> [MakesCallTo] +parse = + mconcat + . fmap parseOperationExtensions + . mconcat + . fmap flattenPathItems + . HM.toList + . _swaggerPaths + +-- | extract path, method, and operation extensions +flattenPathItems :: (FilePath, PathItem) -> [((FilePath, String), HM.InsOrdHashMap Text Value)] +flattenPathItems (path, item) = + filter ((/= mempty) . snd) $ + catMaybes + [ ((path, "get"),) . _operationExtensions <$> _pathItemGet item, + ((path, "put"),) . _operationExtensions <$> _pathItemPut item, + ((path, "post"),) . _operationExtensions <$> _pathItemPost item, + ((path, "delete"),) . _operationExtensions <$> _pathItemDelete item, + ((path, "options"),) . _operationExtensions <$> _pathItemOptions item, + ((path, "head"),) . _operationExtensions <$> _pathItemHead item, + ((path, "patch"),) . _operationExtensions <$> _pathItemPatch item + ] + +parseOperationExtensions :: ((FilePath, String), HM.InsOrdHashMap Text Value) -> [MakesCallTo] +parseOperationExtensions ((path, method), hm) = uncurry (MakesCallTo path method) <$> findCallsFedInfo hm + +findCallsFedInfo :: HM.InsOrdHashMap Text Value -> [(String, String)] +findCallsFedInfo hm = case A.parse parseJSON <$> HM.lookup "wire-makes-federated-call-to" hm of + Just (A.Success (fedcalls :: [(String, String)])) -> fedcalls + Just bad -> error $ "invalid extension `wire-makes-federated-call-to`: expected `[(comp, name), ...]`, got " <> show bad + Nothing -> [] + +------------------------------ + +-- | (this function can be simplified by tossing the serial numbers for nodes, but they might +-- be useful for fine-tuning the output or rendering later.) +-- +-- the layout isn't very useful on realistic data sets. maybe we can tweak it with +-- [layers](https://www.graphviz.org/docs/attr-types/layerRange/)? +mkDotGraph :: [MakesCallTo] -> D.Graph +mkDotGraph inbound = Graph StrictGraph DirectedGraph Nothing (mods <> nodes <> edges) + where + mods = + [ AttributeStatement GraphAttributeStatement [AttributeSetValue (NameId "rankdir") (NameId "LR")], + AttributeStatement NodeAttributeStatement [AttributeSetValue (NameId "shape") (NameId "rectangle")], + AttributeStatement EdgeAttributeStatement [AttributeSetValue (NameId "style") (NameId "dashed")] + ] + nodes = + [ SubgraphStatement (NewSubgraph Nothing (mkCallingNode <$> M.toList callingNodes)), + SubgraphStatement (NewSubgraph Nothing (mkCalledNode <$> M.toList calledNodes)) + ] + edges = mkEdge <$> inbound + + itemSourceNode :: MakesCallTo -> String + itemSourceNode (MakesCallTo path method _ _) = method <> " " <> path + + itemTargetNode :: MakesCallTo -> String + itemTargetNode (MakesCallTo _ _ comp name) = "[" <> comp <> "]:" <> name + + callingNodes :: Map String Integer + callingNodes = + foldl + (\mp (i, caller) -> M.insert caller i mp) + mempty + ((zip [0 ..] . nub $ itemSourceNode <$> inbound) :: [(Integer, String)]) + + calledNodes :: Map String Integer + calledNodes = + foldl + (\mp (i, called) -> M.insert called i mp) + mempty + ((zip [(fromIntegral $ M.size callingNodes) ..] . nub $ itemTargetNode <$> inbound) :: [(Integer, String)]) + + mkCallingNode :: (String, Integer) -> Statement + mkCallingNode n = + NodeStatement (mkCallingNodeId n) [] + + mkCallingNodeId :: (String, Integer) -> NodeId + mkCallingNodeId (caller, i) = + NodeId (NameId . show $ show i <> ": " <> caller) (Just (PortC CompassW)) + + mkCalledNode :: (String, Integer) -> Statement + mkCalledNode n = + NodeStatement (mkCalledNodeId n) [] + + mkCalledNodeId :: (String, Integer) -> NodeId + mkCalledNodeId (callee, i) = + NodeId (NameId . show $ show i <> ": " <> callee) (Just (PortC CompassE)) + + mkEdge :: MakesCallTo -> Statement + mkEdge item = + EdgeStatement + [ ENodeId NoEdge (mkCallingNodeId (caller, callerId)), + ENodeId DirectedEdge (mkCalledNodeId (callee, calleeId)) + ] + [] + where + caller = itemSourceNode item + callee = itemTargetNode item + callerId = fromMaybe (error "impossible") $ M.lookup caller callingNodes + calleeId = fromMaybe (error "impossible") $ M.lookup callee calledNodes + +------------------------------ + +toCsv :: [MakesCallTo] -> String +toCsv = + intercalate "\n" + . fmap (intercalate ",") + . addhdr + . fmap dolines + where + addhdr :: [[String]] -> [[String]] + addhdr = (["source method", "source path", "target component", "target name"] :) + + dolines :: MakesCallTo -> [String] + dolines (MakesCallTo spath smeth tcomp tname) = [smeth, spath, tcomp, tname] From 6657b855c67d126982d87948eecb7d36007af463 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Fri, 13 Jan 2023 11:29:06 +0100 Subject: [PATCH 07/38] Add "edit on github" button docs (#2983) --- docs/src/conf.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/src/conf.py b/docs/src/conf.py index 1d2c7fa490..ee4d992b35 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -113,6 +113,13 @@ html_favicon = '_static/favicon/favicon.ico' html_logo = '_static/image/Wire_logo.svg' +html_context = { + 'display_github': True, + 'github_user': 'wireapp', + 'github_repo': 'wire-server', + 'github_version': 'develop/docs/src/', +} + smv_tag_whitelist = '' smv_branch_whitelist = r'^(install-with-poetry)$' smv_remote_whitelist = r'^(origin)$' From d83c6793bb42b0d41f83237622e7cb2e22ea70b1 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 13 Jan 2023 13:28:03 +0100 Subject: [PATCH 08/38] [SQSERVICES-1828] pagination of team members does not work properly for certain team sizes (#2968) --- changelog.d/3-bug-fixes/pr-2968 | 1 + .../src/Brig/User/Search/TeamUserSearch.hs | 18 +++++++++++++++--- .../test/integration/API/TeamUserSearch.hs | 17 +++++++++++------ 3 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 changelog.d/3-bug-fixes/pr-2968 diff --git a/changelog.d/3-bug-fixes/pr-2968 b/changelog.d/3-bug-fixes/pr-2968 new file mode 100644 index 0000000000..e32c978a07 --- /dev/null +++ b/changelog.d/3-bug-fixes/pr-2968 @@ -0,0 +1 @@ +Fix pagination in team user search (make search key unique) diff --git a/services/brig/src/Brig/User/Search/TeamUserSearch.hs b/services/brig/src/Brig/User/Search/TeamUserSearch.hs index dea5b4dd37..3731f02ab6 100644 --- a/services/brig/src/Brig/User/Search/TeamUserSearch.hs +++ b/services/brig/src/Brig/User/Search/TeamUserSearch.hs @@ -102,12 +102,24 @@ teamUserSearchQuery tid mbSearchText _mRoleFilter mSortBy mSortOrder = mbQStr ) teamFilter - ( maybe + -- in combination with pagination a non-unique search specification can lead to missing results + -- therefore we use the unique `_doc` value as a tie breaker + -- - see https://www.elastic.co/guide/en/elasticsearch/reference/6.8/search-request-sort.html for details on `_doc` + -- - see https://www.elastic.co/guide/en/elasticsearch/reference/6.8/search-request-search-after.html for details on pagination and tie breaker + -- in the latter article it "is advised to duplicate (client side or [...]) the content of the _id field + -- in another field that has doc value enabled and to use this new field as the tiebreaker for the sort" + -- so alternatively we could use the user ID as a tie breaker, but this would require a change in the index mapping + (sorting ++ sortingTieBreaker) + where + sorting :: [ES.DefaultSort] + sorting = + maybe [defaultSort SortByCreatedAt SortOrderDesc | isNothing mbQStr] (\tuSortBy -> [defaultSort tuSortBy (fromMaybe SortOrderAsc mSortOrder)]) mSortBy - ) - where + sortingTieBreaker :: [ES.DefaultSort] + sortingTieBreaker = [ES.DefaultSort (ES.FieldName "_doc") ES.Ascending Nothing Nothing Nothing Nothing] + mbQStr :: Maybe Text mbQStr = case mbSearchText of diff --git a/services/brig/test/integration/API/TeamUserSearch.hs b/services/brig/test/integration/API/TeamUserSearch.hs index ef4ab14088..a57301bb0f 100644 --- a/services/brig/test/integration/API/TeamUserSearch.hs +++ b/services/brig/test/integration/API/TeamUserSearch.hs @@ -111,7 +111,7 @@ testSort brig = do let sortByProperty' :: (TestConstraints m, Ord a) => TeamUserSearchSortBy -> (User -> a) -> TeamUserSearchSortOrder -> m () sortByProperty' = sortByProperty tid users ownerId for_ [SortOrderAsc, SortOrderDesc] $ \sortOrder -> do - -- FUTUREWORK: Test SortByRole when role is avaible in index + -- FUTUREWORK: Test SortByRole when role is available in index sortByProperty' SortByEmail userEmail sortOrder sortByProperty' SortByName userDisplayName sortOrder sortByProperty' SortByHandle (fmap fromHandle . userHandle) sortOrder @@ -144,12 +144,17 @@ testEmptyQuerySortedWithPagination :: TestConstraints m => Brig -> m () testEmptyQuerySortedWithPagination brig = do (tid, userId -> ownerId, _) <- createPopulatedBindingTeamWithNamesAndHandles brig 20 refreshIndex brig - searchResultFirst10 <- executeTeamUserSearchWithMaybeState brig tid ownerId (Just "") Nothing Nothing Nothing (Just $ unsafeRange 10) Nothing - searchResultLast11 <- executeTeamUserSearchWithMaybeState brig tid ownerId (Just "") Nothing Nothing Nothing Nothing (searchPagingState searchResultFirst10) + let teamUserSearch mPs = executeTeamUserSearchWithMaybeState brig tid ownerId (Just "") Nothing (Just SortByRole) (Just SortOrderAsc) (Just $ unsafeRange 10) mPs + searchResultFirst10 <- teamUserSearch Nothing + searchResultNext10 <- teamUserSearch (searchPagingState searchResultFirst10) + searchResultLast1 <- teamUserSearch (searchPagingState searchResultNext10) liftIO $ do searchReturned searchResultFirst10 @?= 10 searchFound searchResultFirst10 @?= 21 searchHasMore searchResultFirst10 @?= Just True - searchReturned searchResultLast11 @?= 11 - searchFound searchResultLast11 @?= 21 - searchHasMore searchResultLast11 @?= Just False + searchReturned searchResultNext10 @?= 10 + searchFound searchResultNext10 @?= 21 + searchHasMore searchResultNext10 @?= Just True + searchReturned searchResultLast1 @?= 1 + searchFound searchResultLast1 @?= 21 + searchHasMore searchResultLast1 @?= Just False From 8760b4978ccb039b229d458b7a08136a05e12ff9 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Fri, 13 Jan 2023 14:21:16 +0100 Subject: [PATCH 09/38] Fix Federation Docs errata (#2984) --- .../how-to/install/configure-federation.md | 14 +++--- docs/src/understand/federation/api.md | 4 +- .../src/understand/federation/architecture.md | 23 +++++----- .../federation/backend-communication.md | 46 +++++++------------ 4 files changed, 37 insertions(+), 50 deletions(-) diff --git a/docs/src/how-to/install/configure-federation.md b/docs/src/how-to/install/configure-federation.md index c7e46849bc..69396c92b5 100644 --- a/docs/src/how-to/install/configure-federation.md +++ b/docs/src/how-to/install/configure-federation.md @@ -14,7 +14,7 @@ detailed in the sections below: - Choose a backend domain name -- DNS setup for federation (including a `SRV` record) +- DNS setup for federation (including an `SRV` record) - Generate and configure TLS certificates: @@ -31,7 +31,7 @@ detailed in the sections below: ## Choose a Backend Domain As of the release \[helm chart 0.129.0, Wire docker version 2.94.0\] from -2020-12-15, the `federationDomain` is a mandatory configuration setting, which +2020-12-15, `federationDomain` is a mandatory configuration setting, which defines the {ref}`backend domain ` of your installation. Regardless of whether you want to enable federation for a backend or not, you must decide what its domain is going to be. This helps in keeping @@ -118,7 +118,7 @@ The fields of the SRV record need to be populated as follows - `weight`: \>0 for your server to be reachable. A good default value could be 10 - `port`: `443` -- `target`: the infra domain +- `target`: the infrastructure domain To give an example, assuming @@ -237,7 +237,7 @@ trust when interacting with other backends. ### (B) Manual server and client certificates Use your usual method of obtaining X.509 certificates for your {ref}`federation -infra domain ` (alongside the other domains needed for a +infrastructure domain ` (alongside the other domains needed for a wire-server installation). You can use one single certificate and key for both server and client @@ -266,7 +266,7 @@ X509v3 extensions: TLS Web Server Authentication, TLS Web Client Authentication ``` -And your {ref}`federation infra domain ` (e.g. +And your {ref}`federation infrastructure domain ` (e.g. `federator.wire.example.com` from the running example) needs to either figure explictly in the list of your SAN (Subject Alternative Name): @@ -304,7 +304,7 @@ The *server certificate* and *private key* need to be configured in just the federator component. If you have installed wire-server before without federation, server certificates may already be configured *(though you probably need to create new certificates to include the -federation infra domain if you\'re not making use of wildcard +federation infrastructure domain if you\'re not making use of wildcard certificates)*. Server certificates go here: ``` yaml @@ -515,7 +515,7 @@ Ensure that the IP matches where your backend ingress runs. Refer to {ref}`how-to-see-tls-certs` and set DOMAIN to your -{ref}`federation infra domain `. They should include your domain as part of the SAN (Subject +{ref}`federation infrastructure domain `. They should include your domain as part of the SAN (Subject Alternative Names) and not have expired. ### Manually test that federation works diff --git a/docs/src/understand/federation/api.md b/docs/src/understand/federation/api.md index 2a8325606c..e48e642294 100644 --- a/docs/src/understand/federation/api.md +++ b/docs/src/understand/federation/api.md @@ -231,9 +231,9 @@ their precise inputs and outputs. In the following the interactions between *Federator* and *Federation Ingress* components of the backends involved are omitted for simplicity. Also the backend -domain and infra domain are assumed the same. +domain and infrastructure domain are assumed the same. -Additionally we assume that the backend domain and the infra domain of +Additionally we assume that the backend domain and the infrastructure domain of the respective backends involved are the same and each domain identifies a distinct backend. diff --git a/docs/src/understand/federation/architecture.md b/docs/src/understand/federation/architecture.md index 392806ac91..6bb1e782bc 100644 --- a/docs/src/understand/federation/architecture.md +++ b/docs/src/understand/federation/architecture.md @@ -6,19 +6,18 @@ ## Backends In the following we call a **backend** the set of servers, databases and DNS -configurations that together form one single Wire Server entity as seen from +configurations that together form one single Wire Server entity as seen from the outside. It can also be called a Wire \"instance\" or \"server\" or \"Wire installation\". Every resource (e.g. users, conversations, assets and teams) exists and is *owned* by a single backend, which we can refer to as that resource\'s backend. The communication between federated backends is facilitated by two components in -each backend: {ref}`federation_ingress` and {ref}`federator`. The -*Federation Ingress* is, as the name suggests the -ingress point for incoming connections from other backends, which are then -forwarded to the *Federator*. The *Federator* forwards requests -to internal components. It also acts as a *egress* point for requests from -internal backend components to other, remote backends. +each backend: {ref}`federation_ingress` and {ref}`federator`. The *Federation +Ingress* is, as the name suggests, the ingress point for incoming connections +from other backends, which are then forwarded to the *Federator*. The +*Federator* forwards requests to internal components. It also acts as a *egress* +point for requests from internal backend components to other, remote backends. ![image](img/federated-backend-architecture.png) @@ -32,7 +31,7 @@ internal backend components to other, remote backends. Each backend has two domain: an {ref}`infrastructure domain ` and a {ref}`backend domain `. -The **infrastructure domain** (short **infra domain**) is the domain name under which the backend +The **infrastructure domain** is the domain name under which the backend is actually reachable via the network. It is also the domain name that each backend uses in authenticating itself to other backends. @@ -42,7 +41,7 @@ context of federation. The distinction between the two domains allows the owner of a backend domain, e.g. `example.com`, to host their Wire backend under a -different infra domain, e.g. `wire.infra.example.com`. +different infrastructure domain, e.g. `wire.infra.example.com`. (federation_ingress)= @@ -53,7 +52,7 @@ ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) and uses [nginx](https://nginx.org/en/) as its underlying software. It is configured with a set of X.509 certificates, which acts as root of -trust for the authentication of the infra domain of remote backends, as +trust for the authentication of the infrastructure domain of remote backends, as well as with a certificate, which it uses to authenticate itself toward other backends. @@ -74,7 +73,7 @@ point for other backend components. It can be configured to use an {ref}`allow list ` to authorize incoming and outgoing connections, and it keeps an X.509 client certificate for the -backend\'s infra domain to authenticate itself towards other backends. +backend\'s infrastructure domain to authenticate itself towards other backends. Additionally, it requires a connection to a DNS resolver to {ref}`discover` other backends. @@ -97,7 +96,7 @@ from remote backends (forwarded via the local 1. Discover the mapping between backend domain claimed by the remote backend and its infra domain, -2. Verify that the discovered infra domain matches the domain in the +2. Verify that the discovered infrastructure domain matches the domain in the remote backend\'s client certificate, 3. If enabled, ensure that the backend domain of the other backend is in the allow list. diff --git a/docs/src/understand/federation/backend-communication.md b/docs/src/understand/federation/backend-communication.md index 7fa3e71cb9..a71c6e158b 100644 --- a/docs/src/understand/federation/backend-communication.md +++ b/docs/src/understand/federation/backend-communication.md @@ -9,7 +9,7 @@ need to ensure the following: - **Authentication** - Determine the identity (infra domain name) of the other backend. + Determine the identity (infrastructure domain name) of the other backend. - **Discovery** @@ -36,7 +36,7 @@ Conversely, every *Federator* needs to be provisioned with a client certificate which it uses to authenticate itself towards other backends. Note that the client certificate is required to be issued with the backend\'s -infra domain as one of the subject alternative names (SAN), which is defined in +infrastructure domain as one of the subject alternative names (SAN), which is defined in [RFC 5280](https://tools.ietf.org/html/rfc5280). See {ref}`federation-certificate-setup` for technical instructions. @@ -48,7 +48,7 @@ with an `AuthenticationFailure` error. ## Discovery -The discovery process allows a backend to determine the infra domain of +The discovery process allows a backend to determine the infrastructure domain of a given backend domain. This step is necessary in two scenarios: @@ -56,21 +56,19 @@ This step is necessary in two scenarios: - A backend would like to establish a connection to another backend that it only knows the backend domain of. This is the case, for example, when a user of a local backend searches for a - {ref}`qualified username `, which only includes that user\'s backend\'s backend - domain. + {ref}`qualified username `, which only includes the backend domain of that user's backend. - When receiving a message from another backend that authenticates - with a given infra domain and claims to represent a given backend + with a given infrastructure domain and claims to represent a given backend domain, a backend would like to ensure the backend domain owner - authorized the owner of the infra domain to run their Wire backend. + authorized the owner of the infrastructure domain to run their Wire backend. To make discovery possible, any party hosting a Wire backend has to -announce the infra domain via a DNS *SRV* record as defined in [RFC +announce the infrastructure domain via a DNS *SRV* record as defined in [RFC 2782](https://tools.ietf.org/html/rfc2782) with `service = wire-server-federator, proto = tcp` and with `name` pointing -to the backend\'s domain and *target* to the backend\'s infra domain. +to the backend\'s domain and *target* to the backend\'s infrastructure domain. -For example, Company A with backend domain *company-a.com* and infra -domain *wire.company-a.com* could publish +For example, Company A with backend domain *company-a.com* and infrastructure domain *wire.company-a.com* could publish ``` bash _wire-server-federator._tcp.company-a.com. 600 IN SRV 10 5 443 federator.wire.company-a.com. @@ -83,34 +81,24 @@ In case this process fails the Federator fails to forward the request with a `Di (dns-scope)= -### DNS Scope - -The network scope of the SRV record (as well as that of the DNS records -for backend and infra domain), depends on the desired federation -topology in the same way as other parameters such as the availability of -the CA certificate that allows authentication of the *Federation -Ingress*\' server certificate or the *Federator*\'s client certificate. -The general rule is that the SRV entry should be \"visible\" from the -point of view of the desired federation partners. The exact scope -strongly depends on the network architecture of the backends involved. (srv-ttl-and-caching)= ### SRV TTL and Caching After retrieving the SRV record for a given domain, the local backend -caches the *backend domain \<\--\> infra domain* mapping for the +caches the *backend domain \<\--\> infrastructure domain* mapping for the duration indicated in the TTL field of the record. Due to this caching behavior, the TTL value of the SRV record dictates at which intervals remote backends will refresh their mapping of the -local backend\'s backend domain to infra domain. As a consequence a +local backend\'s backend domain to infrastructure domain. As a consequence a value in the order of magnitude of 24 hours will reduce the amount of overhead for remote backends. -On the other hand in the setup phase of a backend, or when a change of infra +On the other hand in the setup phase of a backend, or when a change of infrastructure domain is required, a TTL value in the magnitude of a few minutes allows remote -backends to recover more quickly from a change of the infra domain. +backends to recover more quickly from a change of the infrastructure domain. (authorization)= @@ -122,10 +110,10 @@ After an incoming connection is authenticated the backend authorizes the request. It does so by verifying that the backend domain of the sender is contained in the {ref}`domain allow list `. -Since the request is authenticated only by the infra domain the sending backend +Since the request is authenticated only by the infrastructure domain the sending backend is required to add its backend domain as a `Wire-Origin-Domain` header to the request. The receiving backend follows the process described in {ref}`discovery` -and verifies that the discovered infra domain for the backend domain indicated +and verifies that the discovered infrastructure domain for the backend domain indicated in the `Wire-Origin-Domain` header is one of the Subject Alternative Names contained in the client certificate used to sign the request. If this is not the case, the receiving backend fails the request with a `ValidationError`. @@ -150,8 +138,8 @@ details. ## Example The following is an example for the message and information flow between -a backend with backend domain `a.com` and infra domain `infra.a.com` and -another backend with backend domain `b.com` and infra domain +a backend with backend domain `a.com` and infrastructure domain `infra.a.com` and +another backend with backend domain `b.com` and infrastructure domain `infra.b.com`. The content and format of the message is meant to be representative. For From 8816b3fe327bad6b40c6fc5f0022fb755832406d Mon Sep 17 00:00:00 2001 From: fisx Date: Mon, 16 Jan 2023 12:17:22 +0100 Subject: [PATCH 10/38] Fix `ToSchema Version` instance. (#2978) This partially reverts commit 6f91151 (#2965). --- changelog.d/5-internal/pr-2965 | 1 + libs/wire-api/src/Wire/API/Routes/Version.hs | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelog.d/5-internal/pr-2965 diff --git a/changelog.d/5-internal/pr-2965 b/changelog.d/5-internal/pr-2965 new file mode 100644 index 0000000000..9dffecdd84 --- /dev/null +++ b/changelog.d/5-internal/pr-2965 @@ -0,0 +1 @@ +Fix `make clean` (#2965, #2978) diff --git a/libs/wire-api/src/Wire/API/Routes/Version.hs b/libs/wire-api/src/Wire/API/Routes/Version.hs index 5586be14ba..68d46bf8ee 100644 --- a/libs/wire-api/src/Wire/API/Routes/Version.hs +++ b/libs/wire-api/src/Wire/API/Routes/Version.hs @@ -64,7 +64,11 @@ data Version = V0 | V1 | V2 | V3 instance ToSchema Version where schema = enum @Integer "Version" . mconcat $ - (\v -> element (fromIntegral $ fromEnum v) v) <$> [minBound @Version ..] + [ element 0 V0, + element 1 V1, + element 2 V2, + element 3 V3 + ] mkVersion :: Integer -> Maybe Version mkVersion n = case Aeson.fromJSON (Aeson.Number (fromIntegral n)) of From b58e47708e8c399dee2066590fbc1d8ccd4002df Mon Sep 17 00:00:00 2001 From: Sven Tennie Date: Mon, 16 Jan 2023 15:19:56 +0100 Subject: [PATCH 11/38] Add Helm chart for K8ssandra in test systems (#2981) This is not for production usage: E.g. the chart does not provide backups (medusa.) It's meant to simplify k8ssandra setups in test systems. Co-authored-by: jschaul --- Makefile | 7 +- .../5-internal/k8ssandra-cluster-helm-chart | 1 + charts/k8ssandra-test-cluster/.helmignore | 23 +++++ charts/k8ssandra-test-cluster/Chart.yaml | 9 ++ charts/k8ssandra-test-cluster/README.md | 89 +++++++++++++++++++ .../templates/check-cluster-job.yaml | 19 ++++ .../templates/k8ssandra-cluster.yaml | 43 +++++++++ charts/k8ssandra-test-cluster/values.yaml | 13 +++ 8 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 changelog.d/5-internal/k8ssandra-cluster-helm-chart create mode 100644 charts/k8ssandra-test-cluster/.helmignore create mode 100644 charts/k8ssandra-test-cluster/Chart.yaml create mode 100644 charts/k8ssandra-test-cluster/README.md create mode 100644 charts/k8ssandra-test-cluster/templates/check-cluster-job.yaml create mode 100644 charts/k8ssandra-test-cluster/templates/k8ssandra-cluster.yaml create mode 100644 charts/k8ssandra-test-cluster/values.yaml diff --git a/Makefile b/Makefile index df625369b5..9b44c21278 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,12 @@ CHARTS_INTEGRATION := wire-server databases-ephemeral redis-cluster fake-aws # (e.g. move charts/brig to charts/wire-server/brig) # this list could be generated from the folder names under ./charts/ like so: # CHARTS_RELEASE := $(shell find charts/ -maxdepth 1 -type d | xargs -n 1 basename | grep -v charts) -CHARTS_RELEASE := wire-server redis-ephemeral redis-cluster databases-ephemeral fake-aws fake-aws-s3 fake-aws-sqs aws-ingress fluent-bit kibana backoffice calling-test demo-smtp elasticsearch-curator elasticsearch-external elasticsearch-ephemeral minio-external cassandra-external nginx-ingress-controller nginx-ingress-services reaper sftd restund coturn inbucket +CHARTS_RELEASE := wire-server redis-ephemeral redis-cluster databases-ephemeral \ +fake-aws fake-aws-s3 fake-aws-sqs aws-ingress fluent-bit kibana backoffice \ +calling-test demo-smtp elasticsearch-curator elasticsearch-external \ +elasticsearch-ephemeral minio-external cassandra-external \ +nginx-ingress-controller nginx-ingress-services reaper sftd restund coturn \ +inbucket k8ssandra-test-cluster KIND_CLUSTER_NAME := wire-server package ?= all diff --git a/changelog.d/5-internal/k8ssandra-cluster-helm-chart b/changelog.d/5-internal/k8ssandra-cluster-helm-chart new file mode 100644 index 0000000000..ce269c8ef6 --- /dev/null +++ b/changelog.d/5-internal/k8ssandra-cluster-helm-chart @@ -0,0 +1 @@ +Add Helm chart to configure clusters managed by k8ssandra-operator for test environments. diff --git a/charts/k8ssandra-test-cluster/.helmignore b/charts/k8ssandra-test-cluster/.helmignore new file mode 100644 index 0000000000..0e8a0eb36f --- /dev/null +++ b/charts/k8ssandra-test-cluster/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/k8ssandra-test-cluster/Chart.yaml b/charts/k8ssandra-test-cluster/Chart.yaml new file mode 100644 index 0000000000..b67746d307 --- /dev/null +++ b/charts/k8ssandra-test-cluster/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +name: k8ssandra-test-cluster +description: K8ssandra (Cassandra cluster) K8ssandraCluster object for wire test servers. (This does not install K8ssandra itself!) + +type: application + +version: 0.1.0 + +appVersion: "0.39.2" diff --git a/charts/k8ssandra-test-cluster/README.md b/charts/k8ssandra-test-cluster/README.md new file mode 100644 index 0000000000..865183c156 --- /dev/null +++ b/charts/k8ssandra-test-cluster/README.md @@ -0,0 +1,89 @@ +# k8ssandra-test-cluster Helm chart + +`k8ssandra-test-cluster` provides a `K8ssandraCluster` object to create a +*Cassandra* database with +[`k8ssandra-operator`](https://artifacthub.io/packages/helm/k8ssandra/k8ssandra-operator). +**It does not install `k8ssandra-operator` itself!** This configuration is meant +to be used in test environments: **It lacks crucial parts like backups +(`medusa`)!** + +## Usage in Helmfile + +### Prerequisites + +You need a *storage class* that can automatically request storage volumes. For +Hetzner's cloud see: [Container Storage Interface driver for Hetzner +Cloud](https://github.com/hetznercloud/csi-driver) + +### Usage + +These entries are used in the `helfile` file: + +``` yaml +... + +repositories: + - name: wire + url: 'https://s3-eu-west-1.amazonaws.com/public.wire.com/charts' + - name: k8ssandra-stable + url: https://helm.k8ssandra.io/stable + +... + +releases: + - name: k8ssandra-operator + chart: 'k8ssandra-stable/k8ssandra-operator' + namespace: databases + version: 0.39.2 + values: + # Use a cass-operator image that is compatible to the K8s cluster version + - cass-operator: + image: + tag: v1.10.5 + + # Installs CDRs automatically + - name: k8ssandra-test-cluster + chart: "wire/k8ssandra-test-cluster" + namespace: "databases" + version: {{ .Values.wireChartVersion | quote }} + needs: + - 'databases/k8ssandra-operator' + wait: true + waitForJobs: true + + - name: 'wire-server' + namespace: 'wire' + chart: 'wire/wire-server' + version: {{ .Values.wireChartVersion | quote }} + values: + - './helm_vars/wire-server/values.yaml.gotmpl' + secrets: + - './helm_vars/wire-server/secrets.yaml' + needs: + - 'databases/k8ssandra-test-cluster' + +... +``` + +Please note the `needs` relations of the releases: `wire-server` *needs* +`k8ssandra-test-cluster` which *needs* `k8ssandra-operator`. + +`wait` and `waitForJobs` are mandatory for `k8ssandra-test-cluster` in this +setup: These settings ensure that the database really exists before resuming +with the deployment. + +## Implementation details + +### k8ssandra-cluster.yaml + +Contains the `K8ssandraCluster` object. Its schema is described in the [CRD +reference](https://docs-v2.k8ssandra.io/reference/crd/k8ssandra-operator-crds-latest/#k8ssandracluster) + +The specified *Cassandra* cluster runs on a single Node with reasonable +resources for test environments. + +### check-cluster-job.yaml + +Defines a job that tries to connect to the final *Cassandra* database. Other +deployments can wait on this. This is useful because `wire-server` needs a +working database right from the beginning of it's deployment. diff --git a/charts/k8ssandra-test-cluster/templates/check-cluster-job.yaml b/charts/k8ssandra-test-cluster/templates/check-cluster-job.yaml new file mode 100644 index 0000000000..dbff6e0332 --- /dev/null +++ b/charts/k8ssandra-test-cluster/templates/check-cluster-job.yaml @@ -0,0 +1,19 @@ +# This job fails until the Cassandra created database is reachable. The Helmfile +# deployment can wait for it. This is used to start wire-server deployments only +# with a reachable database. +apiVersion: batch/v1 +kind: Job +metadata: + name: check-cluster-job + namespace: databases +spec: + template: + spec: + containers: + - name: cassandra + image: cassandra:3.11 + command: ["cqlsh", "k8ssandra-cluster-datacenter-1-service"] + restartPolicy: OnFailure + # Default is 6 retries. 8 is a bit arbitrary, but should be sufficient for + # low resource environments (e.g. Wire-in-a-box.) + backoffLimit: 8 diff --git a/charts/k8ssandra-test-cluster/templates/k8ssandra-cluster.yaml b/charts/k8ssandra-test-cluster/templates/k8ssandra-cluster.yaml new file mode 100644 index 0000000000..4139f672e5 --- /dev/null +++ b/charts/k8ssandra-test-cluster/templates/k8ssandra-cluster.yaml @@ -0,0 +1,43 @@ +apiVersion: k8ssandra.io/v1alpha1 +kind: K8ssandraCluster +metadata: + name: k8ssandra-cluster + namespace: databases +spec: + auth: false + cassandra: + serverVersion: "3.11.11" + telemetry: + prometheus: + enabled: true + resources: + requests: + cpu: 1 + memory: "4.0Gi" + limits: + memory: "4.0Gi" + config: + jvmOptions: + # Intentionally, half of the available memory + heap_max_size: "2G" + heap_initial_size: "2G" + gc_g1_rset_updating_pause_time_percent: 5 + gc: "G1GC" + gc_g1_max_gc_pause_ms: 300 + gc_g1_initiating_heap_occupancy_percent: 55 + gc_g1_parallel_threads: 16 + datacenters: + - metadata: + name: datacenter-1 + size: 1 + storageConfig: + cassandraDataVolumeClaimSpec: + storageClassName: {{ .Values.storageClassName }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.storageSize }} + reaper: + autoScheduling: + enabled: true diff --git a/charts/k8ssandra-test-cluster/values.yaml b/charts/k8ssandra-test-cluster/values.yaml new file mode 100644 index 0000000000..3aabc8db1a --- /dev/null +++ b/charts/k8ssandra-test-cluster/values.yaml @@ -0,0 +1,13 @@ +# The values in k8ssandra-cluster.yaml are well choosen. Please only export and +# override them if you are confident the change is needed. + +# storageClassName: the name storageClass to use. This defines where the data is +# stored. Storage is automatically requested if the storage class is correctly +# setup. +storageClassName: hcloud-volumes-encrypted + +# storageSize: Size of the storage (persistent volume claim) to request. At +# Hetzner's cloud the smallest volume is 10GB. So, even if you need much less +# storage, it's fine to request 10GB. The memory units are described here: +# https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#meaning-of-memory +storageSize: 10G From b2c183aedf83439ce0398d991d7561dfcb0e5efe Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 17 Jan 2023 10:13:30 +0100 Subject: [PATCH 12/38] [REFACTORING] ID tag expand abbreviation to full name (#2991) --- changelog.d/5-internal/pr-2991 | 1 + libs/types-common/src/Data/Id.hs | 58 ++++++++++++++++++-------------- 2 files changed, 34 insertions(+), 25 deletions(-) create mode 100644 changelog.d/5-internal/pr-2991 diff --git a/changelog.d/5-internal/pr-2991 b/changelog.d/5-internal/pr-2991 new file mode 100644 index 0000000000..150d50cf30 --- /dev/null +++ b/changelog.d/5-internal/pr-2991 @@ -0,0 +1 @@ +Make ID tags more readable by expanding abbreviations to full names. diff --git a/libs/types-common/src/Data/Id.hs b/libs/types-common/src/Data/Id.hs index bcdd57aa12..5663626362 100644 --- a/libs/types-common/src/Data/Id.hs +++ b/libs/types-common/src/Data/Id.hs @@ -87,54 +87,62 @@ import Servant (FromHttpApiData (..), ToHttpApiData (..)) import Test.QuickCheck import Test.QuickCheck.Instances () -data IdTag = A | C | I | U | P | S | T | STo +data IdTag + = Asset + | Conversation + | Invitation + | User + | Provider + | Service + | Team + | ScimToken idTagName :: IdTag -> Text -idTagName A = "Asset" -idTagName C = "Conv" -idTagName I = "Invitation" -idTagName U = "User" -idTagName P = "Provider" -idTagName S = "Service" -idTagName T = "Team" -idTagName STo = "ScimToken" +idTagName Asset = "Asset" +idTagName Conversation = "Conv" +idTagName Invitation = "Invitation" +idTagName User = "User" +idTagName Provider = "Provider" +idTagName Service = "Service" +idTagName Team = "Team" +idTagName ScimToken = "ScimToken" class KnownIdTag (t :: IdTag) where idTagValue :: IdTag -instance KnownIdTag 'A where idTagValue = A +instance KnownIdTag 'Asset where idTagValue = Asset -instance KnownIdTag 'C where idTagValue = C +instance KnownIdTag 'Conversation where idTagValue = Conversation -instance KnownIdTag 'I where idTagValue = I +instance KnownIdTag 'Invitation where idTagValue = Invitation -instance KnownIdTag 'U where idTagValue = U +instance KnownIdTag 'User where idTagValue = User -instance KnownIdTag 'P where idTagValue = P +instance KnownIdTag 'Provider where idTagValue = Provider -instance KnownIdTag 'S where idTagValue = S +instance KnownIdTag 'Service where idTagValue = Service -instance KnownIdTag 'T where idTagValue = T +instance KnownIdTag 'Team where idTagValue = Team -instance KnownIdTag 'STo where idTagValue = STo +instance KnownIdTag 'ScimToken where idTagValue = ScimToken -type AssetId = Id 'A +type AssetId = Id 'Asset -type InvitationId = Id 'I +type InvitationId = Id 'Invitation -- | A local conversation ID -type ConvId = Id 'C +type ConvId = Id 'Conversation -- | A local user ID -type UserId = Id 'U +type UserId = Id 'User -type ProviderId = Id 'P +type ProviderId = Id 'Provider -type ServiceId = Id 'S +type ServiceId = Id 'Service -type TeamId = Id 'T +type TeamId = Id 'Team -type ScimTokenId = Id 'STo +type ScimTokenId = Id 'ScimToken -- Id ------------------------------------------------------------------------- From 37d9b3f9dbc295c0ffd760ea5b4622ac08db1746 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Tue, 17 Jan 2023 15:25:28 +0100 Subject: [PATCH 13/38] Refactor Writetime (#2994) --- changelog.d/5-internal/refactor-writetime | 1 + libs/cassandra-util/src/Cassandra/Util.hs | 56 +++++++++++++++---- services/brig/src/Brig/User/Search/Index.hs | 33 +++++++---- .../brig/test/integration/API/Internal.hs | 23 +++++++- services/galley/src/Galley/Cassandra/Team.hs | 2 +- tools/db/find-undead/src/Work.hs | 4 +- .../db/inconsistencies/src/DanglingHandles.hs | 3 +- 7 files changed, 94 insertions(+), 28 deletions(-) create mode 100644 changelog.d/5-internal/refactor-writetime diff --git a/changelog.d/5-internal/refactor-writetime b/changelog.d/5-internal/refactor-writetime new file mode 100644 index 0000000000..fb0f680472 --- /dev/null +++ b/changelog.d/5-internal/refactor-writetime @@ -0,0 +1 @@ +Refactor Writetime from Int64 to wrapper of UTCTime diff --git a/libs/cassandra-util/src/Cassandra/Util.hs b/libs/cassandra-util/src/Cassandra/Util.hs index 062d9913a9..1b033038da 100644 --- a/libs/cassandra-util/src/Cassandra/Util.hs +++ b/libs/cassandra-util/src/Cassandra/Util.hs @@ -14,28 +14,28 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . +{-# LANGUAGE NumericUnderscores #-} module Cassandra.Util - ( writeTimeToUTC, - defInitCassandra, - Writetime, + ( defInitCassandra, + Writetime (..), + writetimeToInt64, ) where -import Cassandra (ClientState, Keyspace (Keyspace), init) +import Cassandra (ClientState, init) +import Cassandra.CQL import Cassandra.Settings (defSettings, setContacts, setKeyspace, setLogger, setPortNumber) +import Data.Aeson +import Data.Fixed import Data.Text (unpack) -import Data.Time (UTCTime) -import Data.Time.Clock.POSIX (posixSecondsToUTCTime) +import Data.Time (UTCTime, nominalDiffTimeToSeconds) +import Data.Time.Clock (secondsToNominalDiffTime) +import Data.Time.Clock.POSIX import qualified Database.CQL.IO.Tinylog as CT import Imports hiding (init) import qualified System.Logger as Log -type Writetime a = Int64 - -writeTimeToUTC :: Writetime a -> UTCTime -writeTimeToUTC = posixSecondsToUTCTime . fromIntegral . (`div` 1000000) - defInitCassandra :: Text -> Text -> Word16 -> Log.Logger -> IO ClientState defInitCassandra ks h p lg = init @@ -44,3 +44,37 @@ defInitCassandra ks h p lg = . setContacts (unpack h) [] . setKeyspace (Keyspace ks) $ defSettings + +-- | Read cassandra's writetimes https://docs.datastax.com/en/dse/5.1/cql/cql/cql_using/useWritetime.html +-- as UTCTime values without any loss of precision +newtype Writetime a = Writetime {writetimeToUTC :: UTCTime} + +instance Cql (Writetime a) where + ctype = Tagged BigIntColumn + toCql = CqlBigInt . writetimeToInt64 + fromCql (CqlBigInt n) = + pure + . Writetime + . posixSecondsToUTCTime + . secondsToNominalDiffTime + . MkFixed + . (* 1_000_000) + . fromIntegral @Int64 @Integer + $ n + fromCql _ = Left "Writetime: bigint expected" + +-- | This yields the same int as it is returned by WRITETIME() +writetimeToInt64 :: Writetime a -> Int64 +writetimeToInt64 = + fromIntegral @Integer @Int64 + . (`div` 1_000_000) + . unfixed + . nominalDiffTimeToSeconds + . utcTimeToPOSIXSeconds + . writetimeToUTC + where + unfixed :: Fixed a -> Integer + unfixed (MkFixed n) = n + +instance ToJSON (Writetime a) where + toJSON = toJSON . writetimeToInt64 diff --git a/services/brig/src/Brig/User/Search/Index.hs b/services/brig/src/Brig/User/Search/Index.hs index 8dfc420437..bd460bee31 100644 --- a/services/brig/src/Brig/User/Search/Index.hs +++ b/services/brig/src/Brig/User/Search/Index.hs @@ -62,6 +62,7 @@ import Brig.Types.Intra import Brig.Types.Search (SearchVisibilityInbound, defaultSearchVisibilityInbound, searchVisibilityInboundFromFeatureStatus) import Brig.User.Search.Index.Types as Types import qualified Cassandra as C +import Cassandra.Util import Control.Lens hiding ((#), (.=)) import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow, throwM, try) import Control.Monad.Except @@ -73,7 +74,6 @@ import Data.ByteString.Builder (Builder, toLazyByteString) import Data.ByteString.Conversion (toByteString') import qualified Data.ByteString.Conversion as Bytes import qualified Data.ByteString.Lazy as BL -import Data.Fixed (Fixed (MkFixed)) import Data.Handle (Handle) import Data.Id import qualified Data.Map as Map @@ -85,8 +85,6 @@ import Data.Text.Encoding (decodeUtf8, encodeUtf8) import qualified Data.Text.Lazy as LT import Data.Text.Lazy.Builder.Int (decimal) import Data.Text.Lens hiding (text) -import Data.Time (UTCTime, secondsToNominalDiffTime) -import Data.Time.Clock.POSIX (posixSecondsToUTCTime) import qualified Data.UUID as UUID import qualified Database.Bloodhound as ES import Imports hiding (log, searchable) @@ -775,12 +773,6 @@ scanForIndex num = do type Activated = Bool -type Writetime a = Int64 - --- Note: Writetime is in microseconds (e-6) https://docs.datastax.com/en/dse/5.1/cql/cql/cql_using/useWritetime.html -writeTimeToUTC :: Writetime a -> UTCTime -writeTimeToUTC = posixSecondsToUTCTime . secondsToNominalDiffTime . MkFixed . (* 1_000_000) . fromIntegral @Int64 @Integer - type ReindexRow = ( UserId, Maybe TeamId, @@ -837,7 +829,20 @@ reindexRowToIndexUser ) searchVisInbound = do - iu <- mkIndexUser u <$> version [Just tName, tStatus, tHandle, tEmail, Just tColour, Just tActivated, tService, tManagedBy, tSsoId, tEmailUnvalidated] + iu <- + mkIndexUser u + <$> version + [ Just (v tName), + v <$> tStatus, + v <$> tHandle, + v <$> tEmail, + Just (v tColour), + Just (v tActivated), + v <$> tService, + v <$> tManagedBy, + v <$> tSsoId, + v <$> tEmailUnvalidated + ] pure $ if shouldIndex then @@ -850,7 +855,7 @@ reindexRowToIndexUser . set iuAccountStatus status . set iuSAMLIdP (idpUrl =<< ssoId) . set iuManagedBy managedBy - . set iuCreatedAt (Just (writeTimeToUTC tActivated)) + . set iuCreatedAt (Just (writetimeToUTC tActivated)) . set iuSearchVisibilityInbound (Just searchVisInbound) . set iuScimExternalId (join $ User.scimExternalId <$> managedBy <*> ssoId) . set iuSso (sso =<< ssoId) @@ -861,8 +866,12 @@ reindexRowToIndexUser -- It's mostly empty, but having the status here might be useful in the future. & set iuAccountStatus status where - version :: [Maybe (Writetime Name)] -> m IndexVersion + v :: Writetime a -> Int64 + v = writetimeToInt64 + + version :: [Maybe Int64] -> m IndexVersion version = mkIndexVersion . getMax . mconcat . fmap Max . catMaybes + shouldIndex = ( case status of Nothing -> True diff --git a/services/brig/test/integration/API/Internal.hs b/services/brig/test/integration/API/Internal.hs index c8bc85804b..cc2e3900f1 100644 --- a/services/brig/test/integration/API/Internal.hs +++ b/services/brig/test/integration/API/Internal.hs @@ -14,6 +14,7 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . +{-# LANGUAGE NumericUnderscores #-} module API.Internal ( tests, @@ -28,7 +29,9 @@ import Bilge.Assert import Brig.Data.User (lookupFeatureConferenceCalling, lookupStatus, userExists) import qualified Brig.Options as Opt import Brig.Types.Intra +import qualified Cassandra as C import qualified Cassandra as Cass +import Cassandra.Util import Control.Exception (ErrorCall (ErrorCall), throwIO) import Control.Lens ((^.), (^?!)) import Control.Monad.Catch @@ -77,7 +80,8 @@ tests opts mgr db brig brigep gundeck galley = do test mgr "get,get" $ testKpcGetGet brig, test mgr "put,put" $ testKpcPutPut brig, test mgr "add key package ref" $ testAddKeyPackageRef brig - ] + ], + test mgr "writetimeToInt64" $ testWritetimeRepresentation opts mgr db brig brigep galley ] testSuspendUser :: forall m. TestConstraints m => Cass.ClientState -> Brig -> m () @@ -370,3 +374,20 @@ getFeatureConfig galley uid = do getAllFeatureConfigs :: (MonadIO m, MonadHttp m, HasCallStack) => (Request -> Request) -> UserId -> m ResponseLBS getAllFeatureConfigs galley uid = do get $ galley . paths ["feature-configs"] . zUser uid + +testWritetimeRepresentation :: forall m. TestConstraints m => Opt.Opts -> Manager -> Cass.ClientState -> Brig -> Endpoint -> Galley -> m () +testWritetimeRepresentation _ _mgr db brig _brigep _galley = do + quid <- userQualifiedId <$> randomUser brig + let uid = qUnqualified quid + + ref <- fromJust <$> (runIdentity <$$> Cass.runClient db (C.query1 q1 (C.params C.LocalQuorum (Identity uid)))) + + wt <- fromJust <$> (runIdentity <$$> Cass.runClient db (C.query1 q2 (C.params C.LocalQuorum (Identity uid)))) + + liftIO $ assertEqual "writetimeToInt64() does not match WRITETIME(status)" ref (writetimeToInt64 wt) + where + q1 :: C.PrepQuery C.R (Identity UserId) (Identity Int64) + q1 = "SELECT WRITETIME(status) from user where id = ?" + + q2 :: C.PrepQuery C.R (Identity UserId) (Identity (Writetime ())) + q2 = "SELECT WRITETIME(status) from user where id = ?" diff --git a/services/galley/src/Galley/Cassandra/Team.hs b/services/galley/src/Galley/Cassandra/Team.hs index 1dc85be7a7..ff03dc0062 100644 --- a/services/galley/src/Galley/Cassandra/Team.hs +++ b/services/galley/src/Galley/Cassandra/Team.hs @@ -258,7 +258,7 @@ team tid = toTeam (u, n, i, k, d, s, st, b, ss) = let t = newTeam tid u n i (fromMaybe NonBinding b) & teamIconKey .~ k & teamSplashScreen .~ fromMaybe DefaultIcon ss status = if d then PendingDelete else fromMaybe Active s - in TeamData t status (writeTimeToUTC <$> st) + in TeamData t status (writetimeToUTC <$> st) teamIdsOf :: UserId -> [TeamId] -> Client [TeamId] teamIdsOf usr tids = diff --git a/tools/db/find-undead/src/Work.hs b/tools/db/find-undead/src/Work.hs index d7b366efdb..3ab6ca4d74 100644 --- a/tools/db/find-undead/src/Work.hs +++ b/tools/db/find-undead/src/Work.hs @@ -23,7 +23,7 @@ module Work where import Brig.Types.Intra (AccountStatus (..)) import Cassandra -import Cassandra.Util (Writetime, writeTimeToUTC) +import Cassandra.Util (Writetime, writetimeToUTC) import Conduit import Control.Lens (view, _1, _2) import Data.Aeson (FromJSON, (.:)) @@ -72,7 +72,7 @@ logUUID l f (uuid, _, time) = Log.info l $ Log.msg f . Log.field "uuid" (show uuid) - . Log.field "write time" (show $ writeTimeToUTC <$> time) + . Log.field "write time" (show $ writetimeToUTC <$> time) getScrolled :: (ES.MonadBH m, MonadThrow m) => ES.IndexName -> ES.MappingName -> ConduitM () [UUID] m () getScrolled index mapping = processRes =<< lift (ES.getInitialScroll index mapping esSearch) diff --git a/tools/db/inconsistencies/src/DanglingHandles.hs b/tools/db/inconsistencies/src/DanglingHandles.hs index 5a208bd868..613d6deabf 100644 --- a/tools/db/inconsistencies/src/DanglingHandles.hs +++ b/tools/db/inconsistencies/src/DanglingHandles.hs @@ -138,8 +138,9 @@ freeHandle l handle = do handleDelete = "DELETE FROM user_handle WHERE handle = ?" checkUser :: Logger -> ClientState -> Handle -> UserId -> Writetime UserId -> Bool -> IO (Maybe HandleInfo) -checkUser l brig claimedHandle userId handleClaimTime fixClaim = do +checkUser l brig claimedHandle userId handleClaimTime' fixClaim = do maybeDetails <- runClient brig $ getUserDetails userId + let handleClaimTime = Writetime . writetimeToUTC $ handleClaimTime' case maybeDetails of Nothing -> do let status = Nothing From b2d243a2bc665283a9040a4791cda3414928e7df Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 17 Jan 2023 17:48:36 +0100 Subject: [PATCH 14/38] [SQSERVICES-1848] Flaky test team user search query with paging state (#2996) --- changelog.d/5-internal/sqservices-1848 | 1 + docs/convert/shell.nix | 2 +- services/brig/src/Brig/User/Search/Index.hs | 1 - services/brig/test/integration/API/Internal.hs | 1 - services/brig/test/integration/API/TeamUserSearch.hs | 2 +- 5 files changed, 3 insertions(+), 4 deletions(-) create mode 100644 changelog.d/5-internal/sqservices-1848 diff --git a/changelog.d/5-internal/sqservices-1848 b/changelog.d/5-internal/sqservices-1848 new file mode 100644 index 0000000000..ed89235a80 --- /dev/null +++ b/changelog.d/5-internal/sqservices-1848 @@ -0,0 +1 @@ +Fixed flaky team user search integration test diff --git a/docs/convert/shell.nix b/docs/convert/shell.nix index 130c456c6d..2016f223c2 100644 --- a/docs/convert/shell.nix +++ b/docs/convert/shell.nix @@ -1,4 +1,4 @@ -{ pkgs ? import {} }: +{ pkgs ? import { } }: (pkgs.buildFHSUserEnv { name = "pipzone"; targetPkgs = pkgs: (with pkgs; [ diff --git a/services/brig/src/Brig/User/Search/Index.hs b/services/brig/src/Brig/User/Search/Index.hs index bd460bee31..28e3b41e5e 100644 --- a/services/brig/src/Brig/User/Search/Index.hs +++ b/services/brig/src/Brig/User/Search/Index.hs @@ -1,5 +1,4 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE StrictData #-} -- This file is part of the Wire Server implementation. diff --git a/services/brig/test/integration/API/Internal.hs b/services/brig/test/integration/API/Internal.hs index cc2e3900f1..f3fbeae676 100644 --- a/services/brig/test/integration/API/Internal.hs +++ b/services/brig/test/integration/API/Internal.hs @@ -14,7 +14,6 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -{-# LANGUAGE NumericUnderscores #-} module API.Internal ( tests, diff --git a/services/brig/test/integration/API/TeamUserSearch.hs b/services/brig/test/integration/API/TeamUserSearch.hs index a57301bb0f..10e762aa89 100644 --- a/services/brig/test/integration/API/TeamUserSearch.hs +++ b/services/brig/test/integration/API/TeamUserSearch.hs @@ -144,7 +144,7 @@ testEmptyQuerySortedWithPagination :: TestConstraints m => Brig -> m () testEmptyQuerySortedWithPagination brig = do (tid, userId -> ownerId, _) <- createPopulatedBindingTeamWithNamesAndHandles brig 20 refreshIndex brig - let teamUserSearch mPs = executeTeamUserSearchWithMaybeState brig tid ownerId (Just "") Nothing (Just SortByRole) (Just SortOrderAsc) (Just $ unsafeRange 10) mPs + let teamUserSearch mPs = executeTeamUserSearchWithMaybeState brig tid ownerId (Just "") Nothing Nothing Nothing (Just $ unsafeRange 10) mPs searchResultFirst10 <- teamUserSearch Nothing searchResultNext10 <- teamUserSearch (searchPagingState searchResultFirst10) searchResultLast1 <- teamUserSearch (searchPagingState searchResultNext10) From c21e6a33f96c670d260a10b3728e5e5892bd7026 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Wed, 18 Jan 2023 10:03:32 +0100 Subject: [PATCH 15/38] Fix v3 leak in get-conversation endpoints (#2992) * Use v2 types in legacy get-conversation endpoints * Use V3 get-conversation in most tests * Test get-conversation@v2 endpoint directly * Use galley v3 in brig's integration tests and RPC --- .../1-api-changes/get-conversation-v3-leak | 1 + .../API/Routes/Public/Galley/Conversation.hs | 20 ++- .../brig/src/Brig/Effects/GalleyProvider.hs | 3 +- .../src/Brig/Effects/GalleyProvider/RPC.hs | 27 +++- services/brig/src/Brig/Provider/API.hs | 6 +- .../brig/test/integration/API/Provider.hs | 6 +- .../brig/test/integration/API/User/Auth.hs | 4 +- .../test/integration/API/User/Connection.hs | 26 +-- services/brig/test/integration/Util.hs | 7 - .../src/Galley/API/Public/Conversation.hs | 1 + services/galley/test/integration/API.hs | 148 ++++++++++-------- .../galley/test/integration/API/Federation.hs | 3 +- services/galley/test/integration/API/MLS.hs | 4 +- .../test/integration/API/MessageTimer.hs | 56 +++---- services/galley/test/integration/API/Roles.hs | 15 +- services/galley/test/integration/API/Teams.hs | 2 +- services/galley/test/integration/API/Util.hs | 131 +++++++--------- services/galley/test/integration/Main.hs | 2 +- 18 files changed, 245 insertions(+), 217 deletions(-) create mode 100644 changelog.d/1-api-changes/get-conversation-v3-leak diff --git a/changelog.d/1-api-changes/get-conversation-v3-leak b/changelog.d/1-api-changes/get-conversation-v3-leak new file mode 100644 index 0000000000..8f5f313314 --- /dev/null +++ b/changelog.d/1-api-changes/get-conversation-v3-leak @@ -0,0 +1 @@ +The unqualified `GET /conversations/:id` endpoint has been removed from API v3, and is restored to the previous behaviour of returning a Conversation using the v2 schema. Similarly, its qualified counterpart `GET /conversations/:domain/:id` now returns a v2 Conversation when accessed through API v2. diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index 3c877fe475..65dc97b08b 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -102,12 +102,13 @@ type ConversationAPI = Named "get-unqualified-conversation" ( Summary "Get a conversation by ID" + :> Until 'V3 :> CanThrow 'ConvNotFound :> CanThrow 'ConvAccessDenied :> ZLocalUser :> "conversations" :> Capture "cnv" ConvId - :> Get '[Servant.JSON] Conversation + :> MultiVerb1 'GET '[JSON] (VersionedRespond 'V2 200 "Conversation" Conversation) ) :<|> Named "get-unqualified-conversation-legalhold-alias" @@ -120,18 +121,31 @@ type ConversationAPI = :> "legalhold" :> "conversations" :> Capture "cnv" ConvId - :> Get '[Servant.JSON] Conversation + :> MultiVerb1 'GET '[JSON] (VersionedRespond 'V2 200 "Conversation" Conversation) + ) + :<|> Named + "get-conversation@v2" + ( Summary "Get a conversation by ID" + :> Until 'V3 + :> MakesFederatedCall 'Galley "get-conversations" + :> CanThrow 'ConvNotFound + :> CanThrow 'ConvAccessDenied + :> ZLocalUser + :> "conversations" + :> QualifiedCapture "cnv" ConvId + :> MultiVerb1 'GET '[JSON] (VersionedRespond 'V2 200 "Conversation" Conversation) ) :<|> Named "get-conversation" ( Summary "Get a conversation by ID" + :> From 'V3 :> MakesFederatedCall 'Galley "get-conversations" :> CanThrow 'ConvNotFound :> CanThrow 'ConvAccessDenied :> ZLocalUser :> "conversations" :> QualifiedCapture "cnv" ConvId - :> Get '[Servant.JSON] Conversation + :> Get '[JSON] Conversation ) :<|> Named "get-conversation-roles" diff --git a/services/brig/src/Brig/Effects/GalleyProvider.hs b/services/brig/src/Brig/Effects/GalleyProvider.hs index e92c14e720..afbe6b5875 100644 --- a/services/brig/src/Brig/Effects/GalleyProvider.hs +++ b/services/brig/src/Brig/Effects/GalleyProvider.hs @@ -7,6 +7,7 @@ import Brig.Team.Types (ShowOrHideInvitationUrl (..)) import qualified Data.Currency as Currency import Data.Id import Data.Json.Util (UTCTimeMillis) +import Data.Qualified import qualified Galley.Types.Teams.Intra as Team import Imports import qualified Network.Wai.Utilities.Error as Wai @@ -25,7 +26,7 @@ data GalleyProvider m a where GalleyProvider m () GetConv :: UserId -> - ConvId -> + Local ConvId -> GalleyProvider m (Maybe Conversation) GetTeamConv :: UserId -> diff --git a/services/brig/src/Brig/Effects/GalleyProvider/RPC.hs b/services/brig/src/Brig/Effects/GalleyProvider/RPC.hs index e84ef002e0..a3cb6c2e37 100644 --- a/services/brig/src/Brig/Effects/GalleyProvider/RPC.hs +++ b/services/brig/src/Brig/Effects/GalleyProvider/RPC.hs @@ -18,6 +18,7 @@ import Data.Coerce (coerce) import qualified Data.Currency as Currency import Data.Id import Data.Json.Util (UTCTimeMillis) +import Data.Qualified import Data.Range import qualified Galley.Types.Teams as Team import qualified Galley.Types.Teams.Intra as Team @@ -27,8 +28,10 @@ import Network.HTTP.Types.Status import qualified Network.Wai.Utilities.Error as Wai import Polysemy import Polysemy.Error +import Servant.API (toHeader) import System.Logger (Msg, field, msg, val) import Wire.API.Conversation hiding (Member) +import Wire.API.Routes.Version import Wire.API.Team import qualified Wire.API.Team.Conversation as Conv import Wire.API.Team.Feature @@ -84,7 +87,7 @@ createSelfConv u = do void $ ServiceRPC.request @'Galley POST req where req = - path "/conversations/self" + paths ["v" <> toHeader (maxBound :: Version), "conversations", "self"] . zUser u . expect2xx @@ -97,12 +100,13 @@ getConv :: ] r => UserId -> - ConvId -> + Local ConvId -> Sem r (Maybe Conversation) -getConv usr cnv = do +getConv usr lcnv = do debug $ remote "galley" - . field "conv" (toByteString cnv) + . field "domain" (toByteString (tDomain lcnv)) + . field "conv" (toByteString (tUnqualified lcnv)) . msg (val "Getting conversation") rs <- ServiceRPC.request @'Galley GET req case Bilge.statusCode rs of @@ -110,7 +114,12 @@ getConv usr cnv = do _ -> pure Nothing where req = - paths ["conversations", toByteString' cnv] + paths + [ "v" <> toHeader (maxBound :: Version), + "conversations", + toByteString' (tDomain lcnv), + toByteString' (tUnqualified lcnv) + ] . zUser usr . expect [status200, status404] @@ -137,7 +146,13 @@ getTeamConv usr tid cnv = do _ -> pure Nothing where req = - paths ["teams", toByteString' tid, "conversations", toByteString' cnv] + paths + [ "v" <> toHeader (maxBound :: Version), + "teams", + toByteString' tid, + "conversations", + toByteString' cnv + ] . zUser usr . expect [status200, status404] diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 84ea17a47a..529cc5a776 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -897,7 +897,8 @@ addBot zuid zcon cid add = do let pid = addBotProvider add let sid = addBotService add -- Get the conversation and check preconditions - cnv <- lift (liftSem $ GalleyProvider.getConv zuid cid) >>= maybeConvNotFound + lcid <- qualifyLocal cid + cnv <- lift (liftSem $ GalleyProvider.getConv zuid lcid) >>= maybeConvNotFound -- Check that the user is a conversation admin and therefore is allowed to add a bot to this conversation. -- Note that this precondition is also checked in the internal galley API, -- but by having this check here we prevent any (useless) data to be written to the database @@ -981,7 +982,8 @@ removeBotH (zusr ::: zcon ::: cid ::: bid) = do removeBot :: Members '[GalleyProvider] r => UserId -> ConnId -> ConvId -> BotId -> (Handler r) (Maybe Public.RemoveBotResponse) removeBot zusr zcon cid bid = do -- Get the conversation and check preconditions - cnv <- lift (liftSem $ GalleyProvider.getConv zusr cid) >>= maybeConvNotFound + lcid <- qualifyLocal cid + cnv <- lift (liftSem $ GalleyProvider.getConv zusr lcid) >>= maybeConvNotFound -- Check that the user is a conversation admin and therefore is allowed to remove a bot from the conversation. -- Note that this precondition is also checked in the internal galley API. -- However, in case we refine the roles model in the future, this check might not be granular enough. diff --git a/services/brig/test/integration/API/Provider.hs b/services/brig/test/integration/API/Provider.hs index 02e73d1ce5..93172e6ad6 100644 --- a/services/brig/test/integration/API/Provider.hs +++ b/services/brig/test/integration/API/Provider.hs @@ -734,7 +734,7 @@ testDeleteConvBotTeam config db brig galley cannon = withTestService config db b svcAssertConvDelete buf quid2 qcid -- Check that the conversation no longer exists forM_ [uid1, uid2] $ \uid -> - getConversation galley uid cid !!! const 404 === statusCode + getConversationQualified galley uid qcid !!! const 404 === statusCode getBotConv galley bid cid !!! const 404 === statusCode testDeleteTeamBotTeam :: Config -> DB.ClientState -> Brig -> Galley -> Cannon -> Http () @@ -758,7 +758,7 @@ testDeleteTeamBotTeam config db brig galley cannon = withTestService config db b forM_ [uid1, uid2] $ \uid -> do void $ retryWhileN 20 (/= Intra.Deleted) (getStatus brig uid) chkStatus brig uid Intra.Deleted - aFewTimes 11 (getConversation galley uid cid) ((== 404) . statusCode) + aFewTimes 11 (getConversationQualified galley uid qcid) ((== 404) . statusCode) -- Check the bot cannot see the conversation either getBotConv galley bid cid !!! const 404 === statusCode @@ -2088,7 +2088,7 @@ testMessageBotUtil quid uc cid pid sid sref buf brig galley cannon = do assertEqual "id" cid (bcnv ^. Ext.botConvId) assertEqual "members" [OtherMember quid Nothing roleNameWireAdmin] (bcnv ^. Ext.botConvMembers) -- The user can identify the bot in the member list - mems <- fmap cnvMembers . responseJsonError =<< getConversation galley uid cid + mems <- fmap cnvMembers . responseJsonError =<< getConversationQualified galley uid qcid let other = listToMaybe (cmOthers mems) liftIO $ do assertEqual "id" (Just buid) (qUnqualified . omQualifiedId <$> other) diff --git a/services/brig/test/integration/API/User/Auth.hs b/services/brig/test/integration/API/User/Auth.hs index 834bd46a69..3005674924 100644 --- a/services/brig/test/integration/API/User/Auth.hs +++ b/services/brig/test/integration/API/User/Auth.hs @@ -46,7 +46,7 @@ import Data.Handle (Handle (Handle)) import Data.Id import Data.Misc (PlainTextPassword (..)) import Data.Proxy -import Data.Qualified (Qualified (qUnqualified)) +import Data.Qualified import Data.Range (unsafeRange) import qualified Data.Text as Text import Data.Text.Ascii (AsciiChars (validate)) @@ -256,7 +256,7 @@ testNginzLegalHold b g n = do get (apiVersion "v1" . n . paths ["legalhold", "conversations", toByteString' (qUnqualified qconv)] . header "Authorization" ("Bearer " <> toByteString' t)) !!! const 200 === statusCode - get (n . paths ["conversations", toByteString' (qUnqualified qconv)] . header "Authorization" ("Bearer " <> toByteString' t)) !!! const 200 === statusCode + get (apiVersion "v2" . n . paths ["conversations", toByteString' (qUnqualified qconv)] . header "Authorization" ("Bearer " <> toByteString' t)) !!! const 200 === statusCode -- | Corner case for 'testNginz': when upgrading a wire backend from the old behavior (setting -- cookie domain to eg. @*.wire.com@) to the new behavior (leaving cookie domain empty, diff --git a/services/brig/test/integration/API/User/Connection.hs b/services/brig/test/integration/API/User/Connection.hs index d7f0bcd432..f25a2f594d 100644 --- a/services/brig/test/integration/API/User/Connection.hs +++ b/services/brig/test/integration/API/User/Connection.hs @@ -178,11 +178,11 @@ testCreateMutualConnections brig galley = do assertConnections brig uid2 [ConnectionStatus uid2 uid1 Accepted] case responseJsonMaybe rsp >>= ucConvId of Nothing -> liftIO $ assertFailure "incomplete connection" - Just (Qualified cnv _) -> do - getConversation galley uid1 cnv !!! do + Just qcnv -> do + getConversationQualified galley uid1 qcnv !!! do const 200 === statusCode const (Just One2OneConv) === fmap cnvType . responseJsonMaybe - getConversation galley uid2 cnv !!! do + getConversationQualified galley uid2 qcnv !!! do const 200 === statusCode const (Just One2OneConv) === fmap cnvType . responseJsonMaybe @@ -304,32 +304,32 @@ testCancelConnection2 brig galley = do rsp <- putConnection brig uid1 uid2 Cancelled do conv <- responseJsonMaybe rs Just (cnvType conv) -- A is a past member, cannot see the conversation - getConversation galley uid1 cnv !!! do + getConversationQualified galley uid1 qcnv !!! do const 403 === statusCode -- A finally accepts putConnection brig uid1 uid2 Accepted !!! const 200 === statusCode assertConnections brig uid1 [ConnectionStatus uid1 uid2 Accepted] assertConnections brig uid2 [ConnectionStatus uid2 uid1 Accepted] - getConversation galley uid1 cnv !!! do + getConversationQualified galley uid1 qcnv !!! do const 200 === statusCode - getConversation galley uid2 cnv !!! do + getConversationQualified galley uid2 qcnv !!! do const 200 === statusCode testCancelConnectionQualified2 :: Brig -> Galley -> Http () @@ -483,10 +483,10 @@ testBlockAndResendConnection brig galley = do assertConnections brig uid1 [ConnectionStatus uid1 uid2 Accepted] assertConnections brig uid2 [ConnectionStatus uid2 uid1 Blocked] -- B never accepted and thus does not see the conversation - let Just (Qualified cnv _) = ucConvId =<< responseJsonMaybe rsp - getConversation galley uid2 cnv !!! const 403 === statusCode + let Just qcnv = ucConvId =<< responseJsonMaybe rsp + getConversationQualified galley uid2 qcnv !!! const 403 === statusCode -- A can see the conversation and is a current member - getConversation galley uid1 cnv !!! do + getConversationQualified galley uid1 qcnv !!! do const 200 === statusCode testBlockAndResendConnectionQualified :: Brig -> Galley -> Http () diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index 649195714d..37cfac2e5e 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -710,13 +710,6 @@ getTeamMember u tid galley = . expect2xx ) -getConversation :: (MonadIO m, MonadHttp m) => Galley -> UserId -> ConvId -> m ResponseLBS -getConversation galley usr cnv = - get $ - galley - . paths ["conversations", toByteString' cnv] - . zAuthAccess usr "conn" - getConversationQualified :: (MonadIO m, MonadHttp m) => Galley -> UserId -> Qualified ConvId -> m ResponseLBS getConversationQualified galley usr cnv = get $ diff --git a/services/galley/src/Galley/API/Public/Conversation.hs b/services/galley/src/Galley/API/Public/Conversation.hs index c080d83b04..800c9f4654 100644 --- a/services/galley/src/Galley/API/Public/Conversation.hs +++ b/services/galley/src/Galley/API/Public/Conversation.hs @@ -32,6 +32,7 @@ conversationAPI :: API ConversationAPI GalleyEffects conversationAPI = mkNamedAPI @"get-unqualified-conversation" getUnqualifiedConversation <@> mkNamedAPI @"get-unqualified-conversation-legalhold-alias" getUnqualifiedConversation + <@> mkNamedAPI @"get-conversation@v2" (callsFed getConversation) <@> mkNamedAPI @"get-conversation" (callsFed getConversation) <@> mkNamedAPI @"get-conversation-roles" getConversationRoles <@> mkNamedAPI @"get-group-info" (callsFed getGroupInfo) diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index f193cbfee6..7c001655ca 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -98,6 +98,8 @@ import Wire.API.Message import qualified Wire.API.Message as Message import Wire.API.Routes.MultiTablePaging import Wire.API.Routes.Named +import Wire.API.Routes.Version +import Wire.API.Routes.Versioned import qualified Wire.API.Team.Feature as Public import qualified Wire.API.Team.Member as Teams import Wire.API.User @@ -125,6 +127,7 @@ tests s = "Main Conversations API" [ test s "status" status, test s "metrics" metrics, + test s "fetch conversation by qualified ID (v2)" testGetConvQualifiedV2, test s "create Proteus conversation" postProteusConvOk, test s "create conversation with remote users" postConvWithRemoteUsersOk, test s "get empty conversations" getConvsOk, @@ -270,24 +273,46 @@ metrics = do -- Should contain the request duration metric in its output const (Just "TYPE http_request_duration_seconds histogram") =~= responseBody +testGetConvQualifiedV2 :: TestM () +testGetConvQualifiedV2 = do + alice <- randomUser + bob <- randomUser + connectUsers alice (list1 bob []) + conv <- + responseJsonError + =<< postConvQualified + alice + defNewProteusConv + { newConvUsers = [bob] + } + do rsp <- postConv alice [bob, jane] (Just nameMaxSize) [] Nothing Nothing getConv usr cnv + convView cnv usr = do + r <- getConv usr cnv responseJsonError r checkWs qalice (cnv, ws) = WS.awaitMatch (5 # Second) ws $ \n -> do ntfTransient n @?= False let e = List1.head (WS.unpackPayload n) @@ -318,7 +343,8 @@ postConvWithRemoteUsersOk = do withTempMockFederator (const ()) $ postConvQualified alice defNewProteusConv {newConvName = checked nameMaxSize, newConvQualifiedUsers = [qAlex, qAmy, qChad, qCharlie, qDee]} getConv usr cnv + convView cnv usr = do + r <- getConv usr cnv responseJsonError r checkWs qalice (cnv, ws) = WS.awaitMatch (5 # Second) ws $ \n -> do ntfTransient n @?= False let e = List1.head (WS.unpackPayload n) @@ -2141,14 +2169,13 @@ postSelfConvOk = do postO2OConvOk :: TestM () postO2OConvOk = do - qalice <- randomQualifiedUser - let alice = qUnqualified qalice - bob <- randomUser + (alice, qalice) <- randomUserTuple + (bob, qbob) <- randomUserTuple connectUsers alice (singleton bob) a <- postO2OConv alice bob Nothing postConnectConv alice bob "Alice" "come to zeta!" Nothing - putConvAccept bob cnv !!! const 200 === statusCode - getConv alice cnv !!! do + qcnv <- decodeQualifiedConvId <$> postConnectConv alice bob "Alice" "come to zeta!" Nothing + putConvAccept bob (qUnqualified qcnv) !!! const 200 === statusCode + getConvQualified alice qcnv !!! do const 200 === statusCode const (Just One2OneConv) === fmap cnvType . responseJsonUnsafe - getConv bob cnv !!! do + getConvQualified bob qcnv !!! do const 200 === statusCode const (Just One2OneConv) === fmap cnvType . responseJsonUnsafe @@ -2223,7 +2250,7 @@ postMutualConnectConvOk = do getConv bob convId + cnvX <- responseJsonUnsafeWithMsg "conversation" <$> getConvQualified bob qconvId liftIO $ do ConnectConv @=? cnvType cnvX Just "B" @=? cnvName cnvX privateAccess @=? cnvAccess cnvX -- Alice accepts, finally turning it into a 1-1 putConvAccept alice convId !!! const 200 === statusCode - cnv4 <- responseJsonUnsafeWithMsg "conversation" <$> getConv alice convId + cnv4 <- responseJsonUnsafeWithMsg "conversation" <$> getConvQualified alice qconvId liftIO $ do One2OneConv @=? cnvType cnv4 Just "B" @=? cnvName cnv4 @@ -2286,29 +2314,30 @@ putBlockConvOk = do alice <- randomUser bob <- randomUser conv <- responseJsonUnsafeWithMsg "conversation" <$> postConnectConv alice bob "Alice" "connect with me!" (Just "me@me.com") - let convId = qUnqualified . cnvQualifiedId $ conv - getConv alice convId !!! const 200 === statusCode - getConv bob convId !!! const 403 === statusCode + let qconvId = cnvQualifiedId conv + let convId = qUnqualified qconvId + getConvQualified alice qconvId !!! const 200 === statusCode + getConvQualified bob qconvId !!! const 403 === statusCode put (g . paths ["/i/conversations", toByteString' convId, "block"] . zUser bob) !!! const 200 === statusCode -- A is still the only member of the 1-1 - getConv alice convId !!! do + getConvQualified alice qconvId !!! do const 200 === statusCode const (cnvMembers conv) === cnvMembers . responseJsonUnsafeWithMsg "conversation" -- B accepts the conversation by unblocking put (g . paths ["/i/conversations", toByteString' convId, "unblock"] . zUser bob) !!! const 200 === statusCode - getConv bob convId !!! const 200 === statusCode + getConvQualified bob qconvId !!! const 200 === statusCode -- B blocks A in the 1-1 put (g . paths ["/i/conversations", toByteString' convId, "block"] . zUser bob) !!! const 200 === statusCode -- B no longer sees the 1-1 - getConv bob convId !!! const 403 === statusCode + getConvQualified bob qconvId !!! const 403 === statusCode -- B unblocks A in the 1-1 put (g . paths ["/i/conversations", toByteString' convId, "unblock"] . zUser bob) !!! const 200 === statusCode -- B sees the blocked 1-1 again - getConv bob convId !!! do + getConvQualified bob qconvId !!! do const 200 === statusCode getConvOk :: TestM () @@ -2677,7 +2706,8 @@ testAddRemoteMemberFederationDisabled :: TestM () testAddRemoteMemberFederationDisabled = do alice <- randomUser remoteBob <- flip Qualified (Domain "some-remote-backend.example.com") <$> randomId - convId <- decodeConvId <$> postConv alice [] (Just "remote gossip") [] Nothing Nothing + qconvId <- decodeQualifiedConvId <$> postConv alice [] (Just "remote gossip") [] Nothing Nothing + let convId = qUnqualified qconvId connectWithRemoteUser alice remoteBob -- federator endpoint not configured is equivalent to federation being disabled @@ -2689,14 +2719,15 @@ testAddRemoteMemberFederationDisabled = do const (Right "federation-not-enabled") === fmap label . responseJsonEither -- the member is not actually added to the conversation - conv <- responseJsonError =<< getConv alice convId randomId - convId <- decodeConvId <$> postConv alice [] (Just "remote gossip") [] Nothing Nothing + qconvId <- decodeQualifiedConvId <$> postConv alice [] (Just "remote gossip") [] Nothing Nothing + let convId = qUnqualified qconvId connectWithRemoteUser alice remoteBob -- federator endpoint being configured in brig and/or galley, but not being @@ -2711,7 +2742,7 @@ testAddRemoteMemberFederationUnavailable = do -- in this case, we discover that federation is unavailable too late, and the -- member has already been added to the conversation - conv <- responseJsonError =<< getConv alice convId assertFailure $ "Unexpected event data: " ++ show x -- Verify new member state - rs <- getConv bob conv responseJsonUnsafe rs liftIO $ do assertBool "user" (isJust bob') @@ -3496,13 +3527,13 @@ putReceiptModeOk = do let qcnv = Qualified cnv (qDomain qalice) WS.bracketR3 c alice bob jane $ \(_wsA, wsB, _wsJ) -> do -- By default, nothing is set - getConv alice cnv !!! do + getConvQualified alice qcnv !!! do const 200 === statusCode const (Just Nothing) === fmap cnvReceiptMode . responseJsonUnsafe -- Set receipt mode putReceiptMode alice cnv (ReceiptMode 0) !!! const 200 === statusCode -- Ensure the field is properly set - getConv alice cnv !!! do + getConvQualified alice qcnv !!! do const 200 === statusCode const (Just $ Just (ReceiptMode 0)) === fmap cnvReceiptMode . responseJsonUnsafe void . liftIO $ checkWs qalice (qcnv, wsB) @@ -3511,11 +3542,11 @@ putReceiptModeOk = do -- No event should have been generated WS.assertNoEvent (1 # Second) [wsB] -- Ensure that the new field remains unchanged - getConv alice cnv !!! do + getConvQualified alice qcnv !!! do const 200 === statusCode const (Just $ Just (ReceiptMode 0)) === fmap cnvReceiptMode . responseJsonUnsafe - cnv' <- decodeConvId <$> postConvWithReceipt alice [bob, jane] (Just "gossip") [] Nothing Nothing (ReceiptMode 0) - getConv alice cnv' !!! do + qcnv' <- decodeQualifiedConvId <$> postConvWithReceipt alice [bob, jane] (Just "gossip") [] Nothing Nothing (ReceiptMode 0) + getConvQualified alice qcnv' !!! do const 200 === statusCode const (Just (Just (ReceiptMode 0))) === fmap cnvReceiptMode . responseJsonUnsafe where @@ -3775,11 +3806,9 @@ removeUserNoFederation = do connectUsers alice' (list1 bob' [carl']) - conv1 <- decodeConvId <$> postConv alice' [bob'] (Just "gossip") [] Nothing Nothing - conv2 <- decodeConvId <$> postConv alice' [bob', carl'] (Just "gossip2") [] Nothing Nothing - conv3 <- decodeConvId <$> postConv alice' [carl'] (Just "gossip3") [] Nothing Nothing - let qconv1 = Qualified conv1 (qDomain bob) - qconv2 = Qualified conv2 (qDomain bob) + qconv1 <- decodeQualifiedConvId <$> postConv alice' [bob'] (Just "gossip") [] Nothing Nothing + qconv2 <- decodeQualifiedConvId <$> postConv alice' [bob', carl'] (Just "gossip2") [] Nothing Nothing + qconv3 <- decodeQualifiedConvId <$> postConv alice' [carl'] (Just "gossip3") [] Nothing Nothing WS.bracketR3 c alice' bob' carl' $ \(wsA, wsB, wsC) -> do deleteUser bob' !!! const 200 === statusCode @@ -3791,9 +3820,9 @@ removeUserNoFederation = do WS.assertMatchN (5 # Second) [wsA, wsB, wsC] $ wsAssertMembersLeave qconv2 bob [bob] -- Check memberships - mems1 <- fmap cnvMembers . responseJsonUnsafe <$> getConv alice' conv1 - mems2 <- fmap cnvMembers . responseJsonUnsafe <$> getConv alice' conv2 - mems3 <- fmap cnvMembers . responseJsonUnsafe <$> getConv alice' conv3 + mems1 <- fmap cnvMembers . responseJsonUnsafe <$> getConvQualified alice' qconv1 + mems2 <- fmap cnvMembers . responseJsonUnsafe <$> getConvQualified alice' qconv2 + mems3 <- fmap cnvMembers . responseJsonUnsafe <$> getConvQualified alice' qconv3 let other u = find ((== u) . omQualifiedId) . cmOthers liftIO $ do (mems1 >>= other bob) @?= Nothing @@ -3828,17 +3857,14 @@ removeUser = do connectWithRemoteUser alice' dwight connectWithRemoteUser alexDel' dory - convA1 <- decodeConvId <$> postConv alice' [alexDel'] (Just "gossip") [] Nothing Nothing - convA2 <- decodeConvId <$> postConvWithRemoteUsers alice' defNewProteusConv {newConvQualifiedUsers = [alexDel, amy, berta, dwight]} - convA3 <- decodeConvId <$> postConv alice' [amy'] (Just "gossip3") [] Nothing Nothing - convA4 <- decodeConvId <$> postConvWithRemoteUsers alice' defNewProteusConv {newConvQualifiedUsers = [alexDel, bart, carl]} + qconvA1 <- decodeQualifiedConvId <$> postConv alice' [alexDel'] (Just "gossip") [] Nothing Nothing + qconvA2 <- decodeQualifiedConvId <$> postConvWithRemoteUsers alice' defNewProteusConv {newConvQualifiedUsers = [alexDel, amy, berta, dwight]} + qconvA3 <- decodeQualifiedConvId <$> postConv alice' [amy'] (Just "gossip3") [] Nothing Nothing + qconvA4 <- decodeQualifiedConvId <$> postConvWithRemoteUsers alice' defNewProteusConv {newConvQualifiedUsers = [alexDel, bart, carl]} convB1 <- randomId -- a remote conversation at 'bDomain' that Alice, AlexDel and Bart will be in convB2 <- randomId -- a remote conversation at 'bDomain' that AlexDel and Bart will be in convC1 <- randomId -- a remote conversation at 'cDomain' that AlexDel and Carl will be in convD1 <- randomId -- a remote conversation at 'cDomain' that AlexDel and Dory will be in - let qconvA1 = Qualified convA1 (qDomain alexDel) - qconvA2 = Qualified convA2 (qDomain alexDel) - now <- liftIO getCurrentTime fedGalleyClient <- view tsFedGalleyClient let nc cid creator quids = @@ -3912,12 +3938,12 @@ removeUser = do let bConvUpdateRPCs = filter (matchFedRequest bDomain "on-conversation-updated") fedRequests bConvUpdates <- mapM (assertRight . eitherDecode . frBody) bConvUpdateRPCs - bConvUpdatesA2 <- assertOne $ filter (\cu -> cuConvId cu == convA2) bConvUpdates + bConvUpdatesA2 <- assertOne $ filter (\cu -> cuConvId cu == qUnqualified qconvA2) bConvUpdates cuOrigUserId bConvUpdatesA2 @?= alexDel cuAction bConvUpdatesA2 @?= SomeConversationAction (sing @'ConversationLeaveTag) () cuAlreadyPresentUsers bConvUpdatesA2 @?= [qUnqualified berta] - bConvUpdatesA4 <- assertOne $ filter (\cu -> cuConvId cu == convA4) bConvUpdates + bConvUpdatesA4 <- assertOne $ filter (\cu -> cuConvId cu == qUnqualified qconvA4) bConvUpdates cuOrigUserId bConvUpdatesA4 @?= alexDel cuAction bConvUpdatesA4 @?= SomeConversationAction (sing @'ConversationLeaveTag) () cuAlreadyPresentUsers bConvUpdatesA4 @?= [qUnqualified bart] @@ -3925,7 +3951,7 @@ removeUser = do liftIO $ do cConvUpdateRPC <- assertOne $ filter (matchFedRequest cDomain "on-conversation-updated") fedRequests Right convUpdate <- pure . eitherDecode . frBody $ cConvUpdateRPC - cuConvId convUpdate @?= convA4 + cuConvId convUpdate @?= qUnqualified qconvA4 cuOrigUserId convUpdate @?= alexDel cuAction convUpdate @?= SomeConversationAction (sing @'ConversationLeaveTag) () cuAlreadyPresentUsers convUpdate @?= [qUnqualified carl] @@ -3933,16 +3959,16 @@ removeUser = do liftIO $ do dConvUpdateRPC <- assertOne $ filter (matchFedRequest dDomain "on-conversation-updated") fedRequests Right convUpdate <- pure . eitherDecode . frBody $ dConvUpdateRPC - cuConvId convUpdate @?= convA2 + cuConvId convUpdate @?= qUnqualified qconvA2 cuOrigUserId convUpdate @?= alexDel cuAction convUpdate @?= SomeConversationAction (sing @'ConversationLeaveTag) () cuAlreadyPresentUsers convUpdate @?= [qUnqualified dwight] -- Check memberships - mems1 <- fmap cnvMembers . responseJsonError =<< getConv alice' convA1 - mems2 <- fmap cnvMembers . responseJsonError =<< getConv alice' convA2 - mems3 <- fmap cnvMembers . responseJsonError =<< getConv alice' convA3 - mems4 <- fmap cnvMembers . responseJsonError =<< getConv alice' convA4 + mems1 <- fmap cnvMembers . responseJsonError =<< getConvQualified alice' qconvA1 + mems2 <- fmap cnvMembers . responseJsonError =<< getConvQualified alice' qconvA2 + mems3 <- fmap cnvMembers . responseJsonError =<< getConvQualified alice' qconvA3 + mems4 <- fmap cnvMembers . responseJsonError =<< getConvQualified alice' qconvA4 let findOther u = find ((== u) . omQualifiedId) . cmOthers liftIO $ do findOther alexDel mems1 @?= Nothing diff --git a/services/galley/test/integration/API/Federation.hs b/services/galley/test/integration/API/Federation.hs index 23bb350eed..e33915ce83 100644 --- a/services/galley/test/integration/API/Federation.hs +++ b/services/galley/test/integration/API/Federation.hs @@ -1064,8 +1064,7 @@ updateConversationByRemoteAdmin = do postConvQualified alice defNewProteusConv {newConvName = checked convName, newConvQualifiedUsers = [qbob, qcharlie]} viewFederationDomain + [(alice, qalice), (bob, qbob), (jane, qjane)] <- replicateM 3 randomUserTuple connectUsers alice (list1 bob [jane]) rsp <- postConv alice [bob, jane] Nothing [] Nothing mtimer viewFederationDomain + [(alice, qalice), (bob, qbob), (jane, qjane)] <- replicateM 3 randomUserTuple connectUsers alice (list1 bob [jane]) rsp <- postConv alice [bob, jane] Nothing [] Nothing Nothing viewFederationDomain + [(alice, qalice), (bob, qbob), (jane, qjane)] <- replicateM 3 randomUserTuple connectUsers alice (list1 bob [jane]) rsp <- postConv alice [bob, jane] Nothing [] Nothing Nothing cid -- Try to change the timer (as a non admin, guest user) and observe failure putMessageTimerUpdate guest cid (ConversationMessageTimerUpdate timer1sec) !!! do const 403 === statusCode const "action-denied" === (label . responseJsonUnsafeWithMsg "error label") - getConv guest cid + getConvQualified guest qcid !!! const Nothing === (cnvMessageTimer <=< responseJsonUnsafe) -- Try to change the timer (as a non admin, team member) and observe failure too putMessageTimerUpdate member cid (ConversationMessageTimerUpdate timer1sec) !!! do @@ -203,43 +200,40 @@ messageTimerChangeWithoutAllowedAction = do -- Finally try to change the timer (as an admin) and observe success putMessageTimerUpdate owner cid (ConversationMessageTimerUpdate timer1sec) !!! do const 200 === statusCode - getConv guest cid + getConvQualified guest qcid !!! const timer1sec === (cnvMessageTimer <=< responseJsonUnsafe) messageTimerChangeO2O :: TestM () messageTimerChangeO2O = do -- Create a 1:1 conversation - [alice, bob] <- randomUsers 2 - qAlice <- Qualified alice <$> viewFederationDomain + [(alice, qalice), (bob, qbob)] <- replicateM 2 randomUserTuple connectUsers alice (singleton bob) rsp <- postO2OConv alice bob Nothing viewFederationDomain + [(alice, qalice), (bob, qbob)] <- replicateM 2 randomUserTuple connectUsers alice (singleton bob) rsp <- postConv alice [bob] Nothing [] Nothing Nothing do let update = ConversationMessageTimerUpdate timer1sec - qcid = Qualified cid localDomain - qalice = Qualified alice localDomain putMessageTimerUpdate alice cid update !!! const 200 === statusCode void . liftIO $ diff --git a/services/galley/test/integration/API/Roles.hs b/services/galley/test/integration/API/Roles.hs index ed769f7d96..b5ed4cb27c 100644 --- a/services/galley/test/integration/API/Roles.hs +++ b/services/galley/test/integration/API/Roles.hs @@ -84,8 +84,8 @@ handleConversationRoleAdmin = do localDomain <- viewFederationDomain c <- view tsCannon (alice, qalice) <- randomUserTuple - bob <- randomUser - chuck <- randomUser + (bob, qbob) <- randomUserTuple + (chuck, qchuck) <- randomUserTuple (eve, qeve) <- randomUserTuple (jack, qjack) <- randomUserTuple connectUsers alice (list1 bob [chuck, eve, jack]) @@ -94,7 +94,7 @@ handleConversationRoleAdmin = do let role = roleNameWireAdmin cid <- WS.bracketR3 c alice bob chuck $ \(wsA, wsB, wsC) -> do rsp <- postConvWithRole alice [bob, chuck] (Just "gossip") [] Nothing Nothing role - void $ assertConvWithRole rsp RegularConv alice qalice [bob, chuck] (Just "gossip") Nothing role + void $ assertConvWithRole rsp RegularConv alice qalice [qbob, qchuck] (Just "gossip") Nothing role let cid = decodeConvId rsp qcid = Qualified cid localDomain -- Make sure everyone gets the correct event @@ -123,10 +123,9 @@ handleConversationRoleMember :: TestM () handleConversationRoleMember = do localDomain <- viewFederationDomain c <- view tsCannon - alice <- randomUser - let qalice = Qualified alice localDomain - bob <- randomUser - chuck <- randomUser + (alice, qalice) <- randomUserTuple + (bob, qbob) <- randomUserTuple + (chuck, qchuck) <- randomUserTuple eve <- randomUser let qeve = Qualified eve localDomain jack <- randomUser @@ -136,7 +135,7 @@ handleConversationRoleMember = do let role = roleNameWireMember cid <- WS.bracketR3 c alice bob chuck $ \(wsA, wsB, wsC) -> do rsp <- postConvWithRole alice [bob, chuck] (Just "gossip") [] Nothing Nothing role - void $ assertConvWithRole rsp RegularConv alice qalice [bob, chuck] (Just "gossip") Nothing role + void $ assertConvWithRole rsp RegularConv alice qalice [qbob, qchuck] (Just "gossip") Nothing role let cid = decodeConvId rsp qcid = Qualified cid localDomain -- Make sure everyone gets the correct event diff --git a/services/galley/test/integration/API/Teams.hs b/services/galley/test/integration/API/Teams.hs index 72cb13c55b..63c416f644 100644 --- a/services/galley/test/integration/API/Teams.hs +++ b/services/galley/test/integration/API/Teams.hs @@ -794,7 +794,7 @@ testCreateTeamMLSConv = do Nothing Nothing Nothing - Right conv <- responseJsonError <$> getConv owner (tUnqualified lConvId) + Right conv <- responseJsonError <$> getConvQualified owner (tUntagged lConvId) liftIO $ do assertEqual "protocol mismatch" ProtocolMLSTag (protocolTag (cnvProtocol conv)) checkConvCreateEvent (tUnqualified lConvId) wsOwner diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index adfc6d1b29..11255a97e0 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -1033,12 +1033,43 @@ listConvs u req = do . zType "access" . json req -getConv :: (MonadIO m, MonadHttp m, HasGalley m, HasCallStack) => UserId -> ConvId -> m ResponseLBS +getConv :: + ( MonadIO m, + MonadHttp m, + MonadReader TestSetup m, + HasCallStack + ) => + UserId -> + ConvId -> + m ResponseLBS getConv u c = do - g <- viewGalley + g <- view tsUnversionedGalley + get $ + g + . paths ["v2", "conversations", toByteString' c] + . zUser u + . zConn "conn" + . zType "access" + +getConvQualifiedV2 :: + ( Monad m, + MonadReader TestSetup m, + MonadHttp m, + MonadIO m + ) => + UserId -> + Qualified ConvId -> + m ResponseLBS +getConvQualifiedV2 u qcnv = do + g <- view tsUnversionedGalley get $ g - . paths ["conversations", toByteString' c] + . paths + [ "v2", + "conversations", + toByteString' (qDomain qcnv), + toByteString' (qUnqualified qcnv) + ] . zUser u . zConn "conn" . zType "access" @@ -1531,61 +1562,13 @@ assertConv :: ConvType -> UserId -> Qualified UserId -> - [UserId] -> + [Qualified UserId] -> Maybe Text -> Maybe Milliseconds -> - TestM ConvId + TestM (Qualified ConvId) assertConv r t c s us n mt = assertConvWithRole r t c s us n mt roleNameWireAdmin assertConvWithRole :: - HasCallStack => - Response (Maybe Lazy.ByteString) -> - ConvType -> - UserId -> - Qualified UserId -> - [UserId] -> - Maybe Text -> - Maybe Milliseconds -> - RoleName -> - TestM ConvId -assertConvWithRole r t c s us n mt role = do - cId <- fromBS $ getHeader' "Location" r - let cnv = responseJsonMaybe @Conversation r - let _self = cmSelf . cnvMembers <$> cnv - let others = cmOthers . cnvMembers <$> cnv - liftIO $ do - assertEqual "id" (Just cId) (qUnqualified . cnvQualifiedId <$> cnv) - assertEqual "name" n (cnv >>= cnvName) - assertEqual "type" (Just t) (cnvType <$> cnv) - assertEqual "creator" (Just c) (cnvCreator <$> cnv) - assertEqual "message_timer" (Just mt) (cnvMessageTimer <$> cnv) - assertEqual "self" (Just s) (memId <$> _self) - assertEqual "others" (Just . Set.fromList $ us) (Set.fromList . map (qUnqualified . omQualifiedId) . toList <$> others) - assertEqual "creator is always and admin" (Just roleNameWireAdmin) (memConvRoleName <$> _self) - assertBool "others role" (all (== role) $ maybe (error "Cannot be null") (map omConvRoleName . toList) others) - assertBool "otr muted ref not empty" (isNothing (memOtrMutedRef =<< _self)) - assertBool "otr archived not false" (Just False == (memOtrArchived <$> _self)) - assertBool "otr archived ref not empty" (isNothing (memOtrArchivedRef =<< _self)) - case t of - SelfConv -> assertEqual "access" (Just privateAccess) (cnvAccess <$> cnv) - ConnectConv -> assertEqual "access" (Just privateAccess) (cnvAccess <$> cnv) - One2OneConv -> assertEqual "access" (Just privateAccess) (cnvAccess <$> cnv) - _ -> pure () - pure cId - -assertConvQualified :: - HasCallStack => - Response (Maybe Lazy.ByteString) -> - ConvType -> - UserId -> - Qualified UserId -> - [Qualified UserId] -> - Maybe Text -> - Maybe Milliseconds -> - TestM ConvId -assertConvQualified r t c s us n mt = assertConvQualifiedWithRole r t c s us n mt roleNameWireAdmin - -assertConvQualifiedWithRole :: HasCallStack => Response (Maybe Lazy.ByteString) -> ConvType -> @@ -1595,31 +1578,31 @@ assertConvQualifiedWithRole :: Maybe Text -> Maybe Milliseconds -> RoleName -> - TestM ConvId -assertConvQualifiedWithRole r t c s us n mt role = do + TestM (Qualified ConvId) +assertConvWithRole r t c s us n mt role = do cId <- fromBS $ getHeader' "Location" r - let cnv = responseJsonMaybe @Conversation r - let _self = cmSelf . cnvMembers <$> cnv - let others = cmOthers . cnvMembers <$> cnv + cnv <- responseJsonError r + let _self = cmSelf (cnvMembers cnv) + let others = cmOthers (cnvMembers cnv) liftIO $ do - assertEqual "id" (Just cId) (qUnqualified . cnvQualifiedId <$> cnv) - assertEqual "name" n (cnv >>= cnvName) - assertEqual "type" (Just t) (cnvType <$> cnv) - assertEqual "creator" (Just c) (cnvCreator <$> cnv) - assertEqual "message_timer" (Just mt) (cnvMessageTimer <$> cnv) - assertEqual "self" (Just s) (memId <$> _self) - assertEqual "others" (Just . Set.fromList $ us) (Set.fromList . map omQualifiedId . toList <$> others) - assertEqual "creator is always and admin" (Just roleNameWireAdmin) (memConvRoleName <$> _self) - assertBool "others role" (all (== role) $ maybe (error "Cannot be null") (map omConvRoleName . toList) others) - assertBool "otr muted ref not empty" (isNothing (memOtrMutedRef =<< _self)) - assertBool "otr archived not false" (Just False == (memOtrArchived <$> _self)) - assertBool "otr archived ref not empty" (isNothing (memOtrArchivedRef =<< _self)) + assertEqual "id" cId (qUnqualified (cnvQualifiedId cnv)) + assertEqual "name" n (cnvName cnv) + assertEqual "type" t (cnvType cnv) + assertEqual "creator" c (cnvCreator cnv) + assertEqual "message_timer" mt (cnvMessageTimer cnv) + assertEqual "self" s (memId _self) + assertEqual "others" (Set.fromList $ us) (Set.fromList . map omQualifiedId . toList $ others) + assertEqual "creator is always and admin" roleNameWireAdmin (memConvRoleName _self) + assertBool "others role" (all ((== role) . omConvRoleName) (toList others)) + assertBool "otr muted ref not empty" (isNothing (memOtrMutedRef _self)) + assertBool "otr archived not false" (not (memOtrArchived _self)) + assertBool "otr archived ref not empty" (isNothing (memOtrArchivedRef _self)) case t of - SelfConv -> assertEqual "access" (Just privateAccess) (cnvAccess <$> cnv) - ConnectConv -> assertEqual "access" (Just privateAccess) (cnvAccess <$> cnv) - One2OneConv -> assertEqual "access" (Just privateAccess) (cnvAccess <$> cnv) + SelfConv -> assertEqual "access" privateAccess (cnvAccess cnv) + ConnectConv -> assertEqual "access" privateAccess (cnvAccess cnv) + One2OneConv -> assertEqual "access" privateAccess (cnvAccess cnv) _ -> pure () - pure cId + pure (cnvQualifiedId cnv) wsAssertOtr :: HasCallStack => diff --git a/services/galley/test/integration/Main.hs b/services/galley/test/integration/Main.hs index 6907d1ab45..6410cc9fb8 100644 --- a/services/galley/test/integration/Main.hs +++ b/services/galley/test/integration/Main.hs @@ -88,7 +88,7 @@ main = withOpenSSL $ runTests go "galley" [ testCase "sitemap" $ assertEqual - "inconcistent sitemap" + "inconsistent sitemap" mempty (pathsConsistencyCheck . treeToPaths . compile $ Galley.API.sitemap), API.tests setup From 8b2e9afdf5bf79fd1c14492c1b439774e76f4dcb Mon Sep 17 00:00:00 2001 From: Sven Tennie Date: Wed, 18 Jan 2023 10:42:22 +0100 Subject: [PATCH 16/38] Update inbucket (fake smtp server) chart dependency (#2998) The prior version relied on an image that has been removed from docker hub. Thus, our own inbucket chart could not be deployed anymore. --- changelog.d/3-bug-fixes/update-inbucket-chart-dependency | 1 + charts/inbucket/requirements.yaml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/3-bug-fixes/update-inbucket-chart-dependency diff --git a/changelog.d/3-bug-fixes/update-inbucket-chart-dependency b/changelog.d/3-bug-fixes/update-inbucket-chart-dependency new file mode 100644 index 0000000000..63ae2c40a9 --- /dev/null +++ b/changelog.d/3-bug-fixes/update-inbucket-chart-dependency @@ -0,0 +1 @@ +Update `inbucket` (fake smtp server) chart dependency: The prior version relied on an image that has been removed from docker hub. Thus, our own `inbucket` chart could not be deployed anymore. diff --git a/charts/inbucket/requirements.yaml b/charts/inbucket/requirements.yaml index 2478ba341c..47adf720b7 100644 --- a/charts/inbucket/requirements.yaml +++ b/charts/inbucket/requirements.yaml @@ -1,4 +1,4 @@ dependencies: - name: inbucket - version: 2.0.1 + version: 2.1.0 repository: https://inbucket.github.io/inbucket-community From 58b761737a449e7a8b781d90434f1ac1a3f0b9d8 Mon Sep 17 00:00:00 2001 From: jschaul Date: Thu, 19 Jan 2023 15:10:45 +0100 Subject: [PATCH 17/38] Chart compatibility kubernetes 1.22 (#3002) * ingress compatibility k8s 1.22 and beyond See https://kubernetes.io/docs/reference/using-api/deprecation-guide/ * batch api compatibility for k8s >= 1.25 * fixup lost closing parenthesis * wording * go templating: don't munch the newline break after a comment --- .../0-release-notes/chart-compatibility | 1 + .../templates/_helpers.tpl | 10 --- charts/inbucket/templates/_helpers.tpl | 26 ++++++++ charts/inbucket/templates/ingress.yaml | 14 +++- .../ldap-scim-bridge/templates/_helpers.tpl | 13 ++++ .../ldap-scim-bridge/templates/cronjob.yaml | 2 +- charts/legalhold/templates/_helpers.tpl | 26 ++++++++ charts/legalhold/templates/ingress.yaml | 14 +++- .../templates/_helpers.tpl | 27 ++++++++ .../templates/ingress.yaml | 64 ++++++++++++++++++- .../templates/ingress_federator.yaml | 11 +++- charts/sftd/templates/_helpers.tpl | 27 ++++++++ charts/sftd/templates/ingress.yaml | 34 +++++++++- 13 files changed, 253 insertions(+), 16 deletions(-) create mode 100644 changelog.d/0-release-notes/chart-compatibility create mode 100644 charts/inbucket/templates/_helpers.tpl create mode 100644 charts/ldap-scim-bridge/templates/_helpers.tpl create mode 100644 charts/legalhold/templates/_helpers.tpl diff --git a/changelog.d/0-release-notes/chart-compatibility b/changelog.d/0-release-notes/chart-compatibility new file mode 100644 index 0000000000..54bf8bf8d0 --- /dev/null +++ b/changelog.d/0-release-notes/chart-compatibility @@ -0,0 +1 @@ +wire-server helm charts using Ingress resources are now compatible with kubernetes versions 1.22, 1.23 and 1.24 (but keep being compatible with older versions of kubernetes). diff --git a/charts/elasticsearch-ephemeral/templates/_helpers.tpl b/charts/elasticsearch-ephemeral/templates/_helpers.tpl index e49dfab7a6..2aa4295dc8 100644 --- a/charts/elasticsearch-ephemeral/templates/_helpers.tpl +++ b/charts/elasticsearch-ephemeral/templates/_helpers.tpl @@ -15,13 +15,3 @@ We truncate at 53 chars (63 - len("-discovery")) because some Kubernetes name fi {{- printf "%s" $name | trunc 53 | trimSuffix "-" -}} {{- end -}} -{{/* -Return the appropriate apiVersion for Curactor cron job. -*/}} -{{- define "curator.cronJob.apiVersion" -}} -{{- if ge .Capabilities.KubeVersion.Minor "8" -}} -"batch/v1beta1" -{{- else -}} -"batch/v2alpha1" -{{- end -}} -{{- end -}} diff --git a/charts/inbucket/templates/_helpers.tpl b/charts/inbucket/templates/_helpers.tpl new file mode 100644 index 0000000000..c0e9c95498 --- /dev/null +++ b/charts/inbucket/templates/_helpers.tpl @@ -0,0 +1,26 @@ +{{/* Allow KubeVersion to be overridden. */}} +{{- define "kubeVersion" -}} + {{- default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride -}} +{{- end -}} + +{{/* Get Ingress API Version */}} +{{- define "ingress.apiVersion" -}} + {{- if and (.Capabilities.APIVersions.Has "networking.k8s.io/v1") (semverCompare ">= 1.19-0" (include "kubeVersion" .)) -}} + {{- print "networking.k8s.io/v1" -}} + {{- else if .Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" -}} + {{- print "networking.k8s.io/v1beta1" -}} + {{- else -}} + {{- print "extensions/v1beta1" -}} + {{- end -}} +{{- end -}} + +{{/* Check Ingress stability */}} +{{- define "ingress.isStable" -}} + {{- eq (include "ingress.apiVersion" .) "networking.k8s.io/v1" -}} +{{- end -}} + +{{/* Check Ingress supports pathType */}} +{{/* pathType was added to networking.k8s.io/v1beta1 in Kubernetes 1.18 */}} +{{- define "ingress.supportsPathType" -}} + {{- or (eq (include "ingress.isStable" .) "true") (and (eq (include "ingress.apiVersion" .) "networking.k8s.io/v1beta1") (semverCompare ">= 1.18-0" (include "kubeVersion" .))) -}} +{{- end -}} diff --git a/charts/inbucket/templates/ingress.yaml b/charts/inbucket/templates/ingress.yaml index 7be2a320c2..c2803b4e00 100644 --- a/charts/inbucket/templates/ingress.yaml +++ b/charts/inbucket/templates/ingress.yaml @@ -1,4 +1,6 @@ -apiVersion: extensions/v1beta1 +{{- $apiIsStable := eq (include "ingress.isStable" .) "true" -}} +{{- $ingressSupportsPathType := eq (include "ingress.supportsPathType" .) "true" -}} +apiVersion: {{ include "ingress.apiVersion" . }} kind: Ingress metadata: name: "inbucket" @@ -14,6 +16,16 @@ spec: http: paths: - path: / + {{- if $ingressSupportsPathType }} + pathType: Prefix + {{- end }} backend: + {{- if $apiIsStable }} + service: + name: {{ include "inbucket.fullname" . }} + port: + name: http + {{- else }} serviceName: {{ include "inbucket.fullname" . }} servicePort: http + {{- end }} diff --git a/charts/ldap-scim-bridge/templates/_helpers.tpl b/charts/ldap-scim-bridge/templates/_helpers.tpl new file mode 100644 index 0000000000..c288d2067d --- /dev/null +++ b/charts/ldap-scim-bridge/templates/_helpers.tpl @@ -0,0 +1,13 @@ +{{/* Allow KubeVersion to be overridden. */}} +{{- define "kubeVersion" -}} + {{- default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride -}} +{{- end -}} + +{{/* Get Batch API Version */}} +{{- define "batch.apiVersion" -}} + {{- if and (.Capabilities.APIVersions.Has "batch/v1") (semverCompare ">= 1.21-0" (include "kubeVersion" .)) -}} + {{- print "batch/v1" -}} + {{- else -}} + {{- print "batch/v1beta1" -}} + {{- end -}} +{{- end -}} diff --git a/charts/ldap-scim-bridge/templates/cronjob.yaml b/charts/ldap-scim-bridge/templates/cronjob.yaml index 365fa67eca..3b41131f6b 100644 --- a/charts/ldap-scim-bridge/templates/cronjob.yaml +++ b/charts/ldap-scim-bridge/templates/cronjob.yaml @@ -1,4 +1,4 @@ -apiVersion: batch/v1beta1 +apiVersion: {{ include "batch.apiVersion" . }} kind: CronJob metadata: name: {{ .Release.Name }} diff --git a/charts/legalhold/templates/_helpers.tpl b/charts/legalhold/templates/_helpers.tpl new file mode 100644 index 0000000000..c0e9c95498 --- /dev/null +++ b/charts/legalhold/templates/_helpers.tpl @@ -0,0 +1,26 @@ +{{/* Allow KubeVersion to be overridden. */}} +{{- define "kubeVersion" -}} + {{- default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride -}} +{{- end -}} + +{{/* Get Ingress API Version */}} +{{- define "ingress.apiVersion" -}} + {{- if and (.Capabilities.APIVersions.Has "networking.k8s.io/v1") (semverCompare ">= 1.19-0" (include "kubeVersion" .)) -}} + {{- print "networking.k8s.io/v1" -}} + {{- else if .Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" -}} + {{- print "networking.k8s.io/v1beta1" -}} + {{- else -}} + {{- print "extensions/v1beta1" -}} + {{- end -}} +{{- end -}} + +{{/* Check Ingress stability */}} +{{- define "ingress.isStable" -}} + {{- eq (include "ingress.apiVersion" .) "networking.k8s.io/v1" -}} +{{- end -}} + +{{/* Check Ingress supports pathType */}} +{{/* pathType was added to networking.k8s.io/v1beta1 in Kubernetes 1.18 */}} +{{- define "ingress.supportsPathType" -}} + {{- or (eq (include "ingress.isStable" .) "true") (and (eq (include "ingress.apiVersion" .) "networking.k8s.io/v1beta1") (semverCompare ">= 1.18-0" (include "kubeVersion" .))) -}} +{{- end -}} diff --git a/charts/legalhold/templates/ingress.yaml b/charts/legalhold/templates/ingress.yaml index c6ae51758e..24cfcd9813 100644 --- a/charts/legalhold/templates/ingress.yaml +++ b/charts/legalhold/templates/ingress.yaml @@ -1,4 +1,6 @@ -apiVersion: extensions/v1beta1 +{{- $apiIsStable := eq (include "ingress.isStable" .) "true" -}} +{{- $ingressSupportsPathType := eq (include "ingress.supportsPathType" .) "true" -}} +apiVersion: {{ include "ingress.apiVersion" . }} kind: Ingress metadata: name: hold @@ -16,6 +18,16 @@ spec: http: paths: - path: / + {{- if $ingressSupportsPathType }} + pathType: Prefix + {{- end }} backend: + {{- if $apiIsStable }} + service: + name: "{{ .Release.Name }}-hold" + port: + number: 8080 + {{- else }} serviceName: "{{ .Release.Name }}-hold" servicePort: 8080 + {{- end }} diff --git a/charts/nginx-ingress-services/templates/_helpers.tpl b/charts/nginx-ingress-services/templates/_helpers.tpl index e6480a5834..32f4467617 100644 --- a/charts/nginx-ingress-services/templates/_helpers.tpl +++ b/charts/nginx-ingress-services/templates/_helpers.tpl @@ -59,3 +59,30 @@ Returns the Letsencrypt API server URL based on whether testMode is enabled or d {{- $hostnameParts = append $hostnameParts "v02" -}} {{- join "-" $hostnameParts | printf "https://%s.api.letsencrypt.org/directory" -}} {{- end -}} + +{{/* Allow KubeVersion to be overridden. */}} +{{- define "kubeVersion" -}} + {{- default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride -}} +{{- end -}} + +{{/* Get Ingress API Version */}} +{{- define "ingress.apiVersion" -}} + {{- if and (.Capabilities.APIVersions.Has "networking.k8s.io/v1") (semverCompare ">= 1.19-0" (include "kubeVersion" .)) -}} + {{- print "networking.k8s.io/v1" -}} + {{- else if .Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" -}} + {{- print "networking.k8s.io/v1beta1" -}} + {{- else -}} + {{- print "extensions/v1beta1" -}} + {{- end -}} +{{- end -}} + +{{/* Check Ingress stability */}} +{{- define "ingress.isStable" -}} + {{- eq (include "ingress.apiVersion" .) "networking.k8s.io/v1" -}} +{{- end -}} + +{{/* Check Ingress supports pathType */}} +{{/* pathType was added to networking.k8s.io/v1beta1 in Kubernetes 1.18 */}} +{{- define "ingress.supportsPathType" -}} + {{- or (eq (include "ingress.isStable" .) "true") (and (eq (include "ingress.apiVersion" .) "networking.k8s.io/v1beta1") (semverCompare ">= 1.18-0" (include "kubeVersion" .))) -}} +{{- end -}} diff --git a/charts/nginx-ingress-services/templates/ingress.yaml b/charts/nginx-ingress-services/templates/ingress.yaml index 6cc9d019e4..0314ee9513 100644 --- a/charts/nginx-ingress-services/templates/ingress.yaml +++ b/charts/nginx-ingress-services/templates/ingress.yaml @@ -1,4 +1,6 @@ -apiVersion: extensions/v1beta1 +{{- $apiIsStable := eq (include "ingress.isStable" .) "true" -}} +{{- $ingressSupportsPathType := eq (include "ingress.supportsPathType" .) "true" -}} +apiVersion: {{ include "ingress.apiVersion" . }} kind: Ingress metadata: name: nginx-ingress @@ -31,51 +33,111 @@ spec: http: paths: - path: / + {{- if $ingressSupportsPathType }} + pathType: Prefix + {{- end }} backend: + {{- if $apiIsStable }} + service: + name: nginz + port: + name: http + {{- else }} serviceName: nginz servicePort: http + {{- end }} {{- if .Values.websockets.enabled }} - host: {{ .Values.config.dns.ssl }} http: paths: - path: / + {{- if $ingressSupportsPathType }} + pathType: Prefix + {{- end }} backend: + {{- if $apiIsStable }} + service: + name: nginz + port: + name: ws + {{- else }} serviceName: nginz servicePort: ws + {{- end }} {{- end }} {{- if .Values.webapp.enabled }} - host: {{ .Values.config.dns.webapp }} http: paths: - path: / + {{- if $ingressSupportsPathType }} + pathType: Prefix + {{- end }} backend: + {{- if $apiIsStable }} + service: + name: webapp-http + port: + number: {{ .Values.service.webapp.externalPort }} + {{- else }} serviceName: webapp-http servicePort: {{ .Values.service.webapp.externalPort }} + {{- end }} {{- end }} {{- if .Values.fakeS3.enabled }} - host: {{ .Values.config.dns.fakeS3 }} http: paths: - path: / + {{- if $ingressSupportsPathType }} + pathType: Prefix + {{- end }} backend: + {{- if $apiIsStable }} + service: + name: {{ .Values.service.s3.serviceName }} + port: + number: {{ .Values.service.s3.externalPort }} + {{- else }} serviceName: {{ .Values.service.s3.serviceName }} servicePort: {{ .Values.service.s3.externalPort }} + {{- end }} {{- end }} {{- if .Values.teamSettings.enabled }} - host: {{ .Values.config.dns.teamSettings }} http: paths: - path: / + {{- if $ingressSupportsPathType }} + pathType: Prefix + {{- end }} backend: + {{- if $apiIsStable }} + service: + name: team-settings-http + port: + number: {{ .Values.service.teamSettings.externalPort }} + {{- else }} serviceName: team-settings-http servicePort: {{ .Values.service.teamSettings.externalPort }} + {{- end }} {{- end }} {{- if .Values.accountPages.enabled }} - host: {{ .Values.config.dns.accountPages }} http: paths: - path: / + {{- if $ingressSupportsPathType }} + pathType: Prefix + {{- end }} backend: + {{- if $apiIsStable }} + service: + name: account-pages-http + port: + number: {{ .Values.service.accountPages.externalPort }} + {{- else }} serviceName: account-pages-http servicePort: {{ .Values.service.accountPages.externalPort }} + {{- end }} {{- end }} diff --git a/charts/nginx-ingress-services/templates/ingress_federator.yaml b/charts/nginx-ingress-services/templates/ingress_federator.yaml index e756d1cee2..bd9a6aa0b1 100644 --- a/charts/nginx-ingress-services/templates/ingress_federator.yaml +++ b/charts/nginx-ingress-services/templates/ingress_federator.yaml @@ -1,7 +1,9 @@ +{{- $apiIsStable := eq (include "ingress.isStable" .) "true" -}} +{{- $ingressSupportsPathType := eq (include "ingress.supportsPathType" .) "true" -}} {{- if .Values.federator.enabled }} # We use a separate ingress for federator so that we can require client # certificates only for federation requests -apiVersion: extensions/v1beta1 +apiVersion: {{ include "ingress.apiVersion" . }} kind: Ingress metadata: name: federator-ingress @@ -24,6 +26,13 @@ spec: http: paths: - backend: + {{- if $apiIsStable }} + service: + name: federator + port: + name: federator-ext + {{- else }} serviceName: federator servicePort: federator-ext # name must be below 15 chars + {{- end }} {{- end }} diff --git a/charts/sftd/templates/_helpers.tpl b/charts/sftd/templates/_helpers.tpl index 5832980497..f6e1d33113 100644 --- a/charts/sftd/templates/_helpers.tpl +++ b/charts/sftd/templates/_helpers.tpl @@ -58,3 +58,30 @@ app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/name: join-call app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} + +{{/* Allow KubeVersion to be overridden. */}} +{{- define "kubeVersion" -}} + {{- default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride -}} +{{- end -}} + +{{/* Get Ingress API Version */}} +{{- define "ingress.apiVersion" -}} + {{- if and (.Capabilities.APIVersions.Has "networking.k8s.io/v1") (semverCompare ">= 1.19-0" (include "kubeVersion" .)) -}} + {{- print "networking.k8s.io/v1" -}} + {{- else if .Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" -}} + {{- print "networking.k8s.io/v1beta1" -}} + {{- else -}} + {{- print "extensions/v1beta1" -}} + {{- end -}} +{{- end -}} + +{{/* Check Ingress stability */}} +{{- define "ingress.isStable" -}} + {{- eq (include "ingress.apiVersion" .) "networking.k8s.io/v1" -}} +{{- end -}} + +{{/* Check Ingress supports pathType */}} +{{/* pathType was added to networking.k8s.io/v1beta1 in Kubernetes 1.18 */}} +{{- define "ingress.supportsPathType" -}} + {{- or (eq (include "ingress.isStable" .) "true") (and (eq (include "ingress.apiVersion" .) "networking.k8s.io/v1beta1") (semverCompare ">= 1.18-0" (include "kubeVersion" .))) -}} +{{- end -}} diff --git a/charts/sftd/templates/ingress.yaml b/charts/sftd/templates/ingress.yaml index 9bf7958fa0..780c3d5cbb 100644 --- a/charts/sftd/templates/ingress.yaml +++ b/charts/sftd/templates/ingress.yaml @@ -1,4 +1,6 @@ -apiVersion: extensions/v1beta1 +{{- $apiIsStable := eq (include "ingress.isStable" .) "true" -}} +{{- $ingressSupportsPathType := eq (include "ingress.supportsPathType" .) "true" -}} +apiVersion: {{ include "ingress.apiVersion" . }} kind: Ingress metadata: name: "{{ include "sftd.fullname" . }}" @@ -18,14 +20,44 @@ spec: http: paths: - path: /sft/ + {{- if $ingressSupportsPathType }} + pathType: Prefix + {{- end }} backend: + {{- if $apiIsStable }} + service: + name: {{ include "sftd.fullname" . }} + port: + name: sft + {{- else }} serviceName: "{{ include "sftd.fullname" . }}" servicePort: sft + {{- end }} - path: /sfts/ + {{- if $ingressSupportsPathType }} + pathType: Prefix + {{- end }} backend: + {{- if $apiIsStable }} + service: + name: "{{ include "sftd.fullname" . }}-join-call" + port: + name: http + {{- else }} serviceName: "{{ include "sftd.fullname" . }}-join-call" servicePort: http + {{- end }} - path: /sft_servers_all.json + {{- if $ingressSupportsPathType }} + pathType: Exact + {{- end }} backend: + {{- if $apiIsStable }} + service: + name: "{{ include "sftd.fullname" . }}-join-call" + port: + name: http + {{- else }} serviceName: "{{ include "sftd.fullname" . }}-join-call" servicePort: http + {{- end }} From 35b9ba8d45e33b2aa075cc81df7ec08da75d3c12 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Thu, 19 Jan 2023 17:15:46 +0100 Subject: [PATCH 18/38] Update docs: How to view swagger locally (#3005) --- docs/src/developer/developer/how-to.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/src/developer/developer/how-to.md b/docs/src/developer/developer/how-to.md index 14c0e278d9..b900fc4f1f 100644 --- a/docs/src/developer/developer/how-to.md +++ b/docs/src/developer/developer/how-to.md @@ -16,15 +16,14 @@ Terminal 1: Terminal 2: * Compile all services: `make c` - * Note that you have to [import the public signing keys for nginx](https://github.com/wireapp/wire-server/blob/develop/services/nginz/README.md#common-problems-while-compiling) to be able to build nginz -* Run services including nginz: `./services/start-services-only.sh`. If you don't want to run nginz set `INTEGRATION_USE_NGINZ=0`. +* Run services including nginz: `./services/start-services-only.sh`. Open your browser at: -- http://localhost:8080/api/swagger-ui for the swagger 2.0 endpoints (in development as of Feb 2021 - more endpoints will be added here as time goes on) -- http://localhost:8080/swagger-ui/ for the old swagger 1.2 API (old swagger, endpoints will disappear from here (and become available in the previous link) as time progresses). Run `make -C services/nginz integration-test/conf/nginz/zwagger-ui` once to get JS libraries needed (they are not included in the repo). +- [http://localhost:8080/api/swagger-ui](http://localhost:8080/api/swagger-ui) for the swagger 2.0 endpoints (in development as of Feb 2021 - more endpoints will be added here as time goes on) +- [http://localhost:8080/swagger-ui](http://localhost:8080/swagger-ui) for the old swagger 1.2 API (old swagger, endpoints will disappear from here (and become available in the previous link) as time progresses). Run `make -C services/nginz integration-test/conf/nginz/zwagger-ui` once to get JS libraries needed (they are not included in the repo). -Swagger json (for swagger 2.0 endpoints) is available under http://localhost:8080/api/swagger.json +Swagger json (for swagger 2.0 endpoints) is available under [http://localhost:8080/api/swagger.json](http://localhost:8080/api/swagger.json) ## How to run federation tests across two backends From 73d79d374527965331b42dcb2dd46a94a9670393 Mon Sep 17 00:00:00 2001 From: fisx Date: Fri, 20 Jan 2023 11:17:13 +0100 Subject: [PATCH 19/38] Hook fedcalls into api docs (#2988) * Hook federated API call docs into docs.wire.com (manually). * Update fedcalls README. * s/client api calls/api calls/ (Will also include fed calls from now on, and anyway an API is always both a server and a client API, or it wouldn't work at all.) * Dial down warning in link to swagger docs. (it's gotten better since 2020) --- .../4-docs/hook-fedcalls-into-api-docs | 1 + docs/src/configuration-options.md | 6 +- docs/src/index.md | 2 +- .../api-client-perspective/index.md | 4 - .../api-client-perspective/swagger.md | 16 +- docs/src/understand/federation/fedcalls.md | 18 ++ .../federation/img/wire-fedcalls.csv | 122 ++++++++++ .../federation/img/wire-fedcalls.dot | 219 ++++++++++++++++++ .../federation/img/wire-fedcalls.png | Bin 0 -> 728020 bytes tools/fedcalls/README.md | 11 +- 10 files changed, 379 insertions(+), 20 deletions(-) create mode 100644 changelog.d/4-docs/hook-fedcalls-into-api-docs create mode 100644 docs/src/understand/federation/fedcalls.md create mode 100644 docs/src/understand/federation/img/wire-fedcalls.csv create mode 100644 docs/src/understand/federation/img/wire-fedcalls.dot create mode 100644 docs/src/understand/federation/img/wire-fedcalls.png diff --git a/changelog.d/4-docs/hook-fedcalls-into-api-docs b/changelog.d/4-docs/hook-fedcalls-into-api-docs new file mode 100644 index 0000000000..2a6d02a8a3 --- /dev/null +++ b/changelog.d/4-docs/hook-fedcalls-into-api-docs @@ -0,0 +1 @@ +Hook federated API call documentation into docs.wire.com (manually). diff --git a/docs/src/configuration-options.md b/docs/src/configuration-options.md index 647ac5f0ee..1eeee72383 100644 --- a/docs/src/configuration-options.md +++ b/docs/src/configuration-options.md @@ -431,11 +431,11 @@ helm upgrade --install --namespace metallb-system metallb wire/metallb \ Install `nginx-ingress-[controller,services]`: :: -: helm upgrade --install --namespace demo demo-nginx-ingress-controller wire/nginx-ingress-controller +: helm upgrade --install --namespace demo demo-nginx-ingress-controller wire/nginx-ingress-controller : --wait - helm upgrade --install --namespace demo demo-nginx-ingress-services wire/nginx-ingress-services + helm upgrade --install --namespace demo demo-nginx-ingress-services wire/nginx-ingress-services : -f values/nginx-ingress-services/demo-values.yaml -f values/nginx-ingress-services/demo-secrets.yaml --wait @@ -633,7 +633,7 @@ account-pages: ## Configuring authentication cookie throttling -Authentication cookies and the related throttling mechanism is described in the *Client API documentation*: +Authentication cookies and the related throttling mechanism is described in the *API documentation*: {ref}`login-cookies` The maximum number of cookies per account and type is defined by the brig option diff --git a/docs/src/index.md b/docs/src/index.md index 135a0aa03f..da0c17e170 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -30,7 +30,7 @@ Connecting Wire Clients Optional Configuration Understanding wire-server components Single-Sign-On and user provisioning -Client API documentation +API documentation Security responses Notes for developers ``` diff --git a/docs/src/understand/api-client-perspective/index.md b/docs/src/understand/api-client-perspective/index.md index 8bf19e4290..e86c0de1f2 100644 --- a/docs/src/understand/api-client-perspective/index.md +++ b/docs/src/understand/api-client-perspective/index.md @@ -2,10 +2,6 @@ The following documentation provides information for, and takes the perspective of a Wire client developer. (wire-desktop, wire-android and wire-ios are examples of Wire Clients). This means only publicly accessible endpoints are mentioned. -```{warning} -This section of the documentation is very incomplete at the time of writing (summer 2020) - more pages on the client API will follow in the future. -``` - ```{toctree} :glob: true :maxdepth: 2 diff --git a/docs/src/understand/api-client-perspective/swagger.md b/docs/src/understand/api-client-perspective/swagger.md index 057d5beb2a..5722c95aed 100644 --- a/docs/src/understand/api-client-perspective/swagger.md +++ b/docs/src/understand/api-client-perspective/swagger.md @@ -3,24 +3,20 @@ Our staging system provides swagger documentation of our public rest API. -We are currently (as of 2021-09-29) migrating our documentation into a -new system that is automatically checked for correctness. The old -documentation still has some endpoints, but the new one is getting more and more complete. We will completely replace the old one eventually. +The swagger docs are correct by construction (compiled from the server +code), and the are complete up to bots/services and event notification +payloads (as of 2023-01-16). Please check the new docs first, and if you can't find what you're looking for, double-check the old. -## New docs - -These docs show swagger 2.0: +## New docs (swagger 2.0) [new staging swagger page](https://staging-nginz-https.zinfra.io/api/swagger-ui/) -## Old docs - -Some endpoints are only shown using swagger 1.2. +## Old docs (swagger 1.2) -At the time of writing, both swagger version 1.2 and version 2.0 are in use. If you are an employee of Wire, you can log in here and try out requests in the browser; if not, you can make use of the "List Operations" button on both 1.2 and 2.0 pages to see the possible API requests. +If you are an employee of Wire, you can log in here and try out requests in the browser; if not, you can make use of the "List Operations" button on both 1.2 and 2.0 pages to see the possible API requests. Browse to our [old staging swagger page](https://staging-nginz-https.zinfra.io/swagger-ui/) to see rendered swagger documentation for the remaining endpoints. diff --git a/docs/src/understand/federation/fedcalls.md b/docs/src/understand/federation/fedcalls.md new file mode 100644 index 0000000000..80fdb3c03e --- /dev/null +++ b/docs/src/understand/federation/fedcalls.md @@ -0,0 +1,18 @@ +# Federated API calls by client API end-point (generated) + +**Updated manually using using [the fedcalls tool](https://github.com/wireapp/wire-server/blob/8760b4978ccb039b229d458b7a08136a05e12ff9/tools/fedcalls/README.md); last change: 2023-01-16.** + +This is most likely only interesting for backend developers. + +This graph and csv file describe which public (client) API end-points trigger calls to which end-points at backends federating with the one that is called. The data is correct by construction (see [the fedcalls tool](https://github.com/wireapp/wire-server/blob/8760b4978ccb039b229d458b7a08136a05e12ff9/tools/fedcalls/README.md) for more details). + +The target can only be understood in the context of the [backend code base](https://github.com/wireapp/wire-server/). It is described by component (sub-directory in `/services`) and end-point name (use grep to find it). + +links: + +- [dot](img/wire-fedcalls.dot) +- [png](img/wire-fedcalls.png) +- [csv](img/wire-fedcalls.csv) + +```{image} img/wire-fedcalls.png +``` diff --git a/docs/src/understand/federation/img/wire-fedcalls.csv b/docs/src/understand/federation/img/wire-fedcalls.csv new file mode 100644 index 0000000000..bfc571a6d8 --- /dev/null +++ b/docs/src/understand/federation/img/wire-fedcalls.csv @@ -0,0 +1,122 @@ +source method,source path,target component,target name +get,/users/{uid_domain}/{uid},brig,get-users-by-ids +post,/list-users,brig,get-users-by-ids +put,/self,brig,on-user-deleted-connections +delete,/self,brig,on-user-deleted-connections +delete,/self/phone,brig,on-user-deleted-connections +delete,/self/email,brig,on-user-deleted-connections +put,/self/locale,brig,on-user-deleted-connections +put,/self/handle,brig,on-user-deleted-connections +post,/register,brig,on-user-deleted-connections +post,/delete,brig,on-user-deleted-connections +get,/activate,brig,on-user-deleted-connections +post,/activate,brig,on-user-deleted-connections +get,/users/{uid_domain}/{uid}/clients,brig,get-user-clients +get,/users/{uid_domain}/{uid}/clients/{client},brig,get-user-clients +post,/users/list-clients,brig,get-user-clients +get,/users/{uid_domain}/{uid}/prekeys/{client},brig,claim-prekey +get,/users/{uid_domain}/{uid}/prekeys,brig,claim-prekey-bundle +post,/users/list-prekeys,brig,claim-multi-prekey-bundle +post,/clients,brig,on-user-deleted-connections +put,/connections/{uid_domain}/{uid},brig,send-connection-action +post,/connections/{uid_domain}/{uid},brig,send-connection-action +get,/search/contacts,brig,get-users-by-ids +get,/search/contacts,brig,search-users +post,/mls/key-packages/claim/{user_domain}/{user},brig,claim-key-packages +post,/access,brig,on-user-deleted-connections +post,/login,brig,on-user-deleted-connections +get,/assets/{key_domain}/{key},cargohold,get-asset +get,/assets/{key_domain}/{key},cargohold,stream-asset +put,/conversations/{cnv},galley,on-conversation-updated +put,/conversations/{cnv},galley,on-mls-message-sent +put,/conversations/{cnv},galley,on-new-remote-conversation +get,/conversations/{cnv_domain}/{cnv},galley,get-conversations +get,/conversations/{cnv_domain}/{cnv}/groupinfo,galley,query-group-info +post,/conversations/list,galley,get-conversations +post,/conversations/join,galley,on-conversation-updated +post,/conversations/join,galley,on-new-remote-conversation +post,/conversations,galley,on-conversation-created +post,/conversations/one2one,galley,on-conversation-created +post,/conversations/{cnv_domain}/{cnv}/members,galley,on-conversation-updated +post,/conversations/{cnv_domain}/{cnv}/members,galley,on-mls-message-sent +post,/conversations/{cnv_domain}/{cnv}/members,galley,on-new-remote-conversation +post,/conversations/{cnv}/join,galley,on-conversation-updated +post,/conversations/{cnv}/join,galley,on-new-remote-conversation +post,/conversations/{cnv_domain}/{cnv}/typing,galley,on-typing-indicator-updated +put,/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr},galley,on-conversation-updated +put,/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr},galley,on-mls-message-sent +put,/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr},galley,on-new-remote-conversation +delete,/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr},galley,leave-conversation +delete,/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr},galley,on-conversation-updated +delete,/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr},galley,on-mls-message-sent +delete,/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr},galley,on-new-remote-conversation +put,/conversations/{cnv}/members/{usr},galley,on-conversation-updated +put,/conversations/{cnv}/members/{usr},galley,on-mls-message-sent +put,/conversations/{cnv}/members/{usr},galley,on-new-remote-conversation +put,/conversations/{cnv}/name,galley,on-conversation-updated +put,/conversations/{cnv}/name,galley,on-mls-message-sent +put,/conversations/{cnv}/name,galley,on-new-remote-conversation +put,/conversations/{cnv_domain}/{cnv}/name,galley,on-conversation-updated +put,/conversations/{cnv_domain}/{cnv}/name,galley,on-mls-message-sent +put,/conversations/{cnv_domain}/{cnv}/name,galley,on-new-remote-conversation +put,/conversations/{cnv}/message-timer,galley,on-conversation-updated +put,/conversations/{cnv}/message-timer,galley,on-mls-message-sent +put,/conversations/{cnv}/message-timer,galley,on-new-remote-conversation +put,/conversations/{cnv_domain}/{cnv}/message-timer,galley,on-conversation-updated +put,/conversations/{cnv_domain}/{cnv}/message-timer,galley,on-mls-message-sent +put,/conversations/{cnv_domain}/{cnv}/message-timer,galley,on-new-remote-conversation +put,/conversations/{cnv}/receipt-mode,galley,on-conversation-updated +put,/conversations/{cnv}/receipt-mode,galley,on-mls-message-sent +put,/conversations/{cnv}/receipt-mode,galley,on-new-remote-conversation +put,/conversations/{cnv}/receipt-mode,galley,update-conversation +put,/conversations/{cnv_domain}/{cnv}/receipt-mode,galley,on-conversation-updated +put,/conversations/{cnv_domain}/{cnv}/receipt-mode,galley,on-mls-message-sent +put,/conversations/{cnv_domain}/{cnv}/receipt-mode,galley,on-new-remote-conversation +put,/conversations/{cnv_domain}/{cnv}/receipt-mode,galley,update-conversation +put,/conversations/{cnv_domain}/{cnv}/access,galley,on-conversation-updated +put,/conversations/{cnv_domain}/{cnv}/access,galley,on-mls-message-sent +put,/conversations/{cnv_domain}/{cnv}/access,galley,on-new-remote-conversation +delete,/teams/{tid}/conversations/{cid},galley,on-conversation-updated +delete,/teams/{tid}/conversations/{cid},galley,on-mls-message-sent +delete,/teams/{tid}/conversations/{cid},galley,on-new-remote-conversation +post,/conversations/{cnv}/otr/messages,galley,on-message-sent +post,/conversations/{cnv}/otr/messages,brig,get-user-clients +post,/conversations/{cnv_domain}/{cnv}/proteus/messages,brig,get-user-clients +post,/conversations/{cnv_domain}/{cnv}/proteus/messages,galley,on-message-sent +post,/conversations/{cnv_domain}/{cnv}/proteus/messages,galley,send-message +post,/bot/messages,galley,on-message-sent +post,/bot/messages,brig,get-user-clients +put,/teams/{tid}/features/legalhold,galley,on-conversation-updated +put,/teams/{tid}/features/legalhold,galley,on-mls-message-sent +put,/teams/{tid}/features/legalhold,galley,on-new-remote-conversation +post,/mls/welcome,galley,mls-welcome +post,/mls/messages,galley,on-mls-message-sent +post,/mls/messages,galley,send-mls-message +post,/mls/messages,galley,on-conversation-updated +post,/mls/messages,galley,on-new-remote-conversation +post,/mls/messages,brig,get-mls-clients +post,/mls/commit-bundles,galley,on-mls-message-sent +post,/mls/commit-bundles,galley,mls-welcome +post,/mls/commit-bundles,galley,send-mls-commit-bundle +post,/mls/commit-bundles,galley,on-conversation-updated +post,/mls/commit-bundles,galley,on-new-remote-conversation +post,/mls/commit-bundles,brig,get-mls-clients +delete,/teams/{tid}/legalhold/settings,galley,on-conversation-updated +delete,/teams/{tid}/legalhold/settings,galley,on-mls-message-sent +delete,/teams/{tid}/legalhold/settings,galley,on-new-remote-conversation +post,/teams/{tid}/legalhold/{uid},galley,on-conversation-updated +post,/teams/{tid}/legalhold/{uid},galley,on-mls-message-sent +post,/teams/{tid}/legalhold/{uid},galley,on-new-remote-conversation +delete,/teams/{tid}/legalhold/{uid},galley,on-conversation-updated +delete,/teams/{tid}/legalhold/{uid},galley,on-mls-message-sent +delete,/teams/{tid}/legalhold/{uid},galley,on-new-remote-conversation +post,/teams/{tid}/legalhold/consent,galley,on-conversation-updated +post,/teams/{tid}/legalhold/consent,galley,on-mls-message-sent +post,/teams/{tid}/legalhold/consent,galley,on-new-remote-conversation +put,/teams/{tid}/legalhold/{uid}/approve,galley,on-conversation-updated +put,/teams/{tid}/legalhold/{uid}/approve,galley,on-mls-message-sent +put,/teams/{tid}/legalhold/{uid}/approve,galley,on-new-remote-conversation +post,/i/users,brig,on-user-deleted-connections +post,/i/users/spar,brig,on-user-deleted-connections +post,/i/legalhold-login,brig,on-user-deleted-connections +post,/i/sso-login,brig,on-user-deleted-connections \ No newline at end of file diff --git a/docs/src/understand/federation/img/wire-fedcalls.dot b/docs/src/understand/federation/img/wire-fedcalls.dot new file mode 100644 index 0000000000..77648a9d95 --- /dev/null +++ b/docs/src/understand/federation/img/wire-fedcalls.dot @@ -0,0 +1,219 @@ +strict digraph { + graph [rankdir=LR] + node [shape=rectangle] + edge [style=dashed] + subgraph { + "37: delete /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w + "3: delete /self":w + "5: delete /self/email":w + "4: delete /self/phone":w + "46: delete /teams/{tid}/conversations/{cid}":w + "54: delete /teams/{tid}/legalhold/settings":w + "56: delete /teams/{tid}/legalhold/{uid}":w + "10: get /activate":w + "25: get /assets/{key_domain}/{key}":w + "27: get /conversations/{cnv_domain}/{cnv}":w + "28: get /conversations/{cnv_domain}/{cnv}/groupinfo":w + "21: get /search/contacts":w + "0: get /users/{uid_domain}/{uid}":w + "12: get /users/{uid_domain}/{uid}/clients":w + "13: get /users/{uid_domain}/{uid}/clients/{client}":w + "16: get /users/{uid_domain}/{uid}/prekeys":w + "15: get /users/{uid_domain}/{uid}/prekeys/{client}":w + "23: post /access":w + "11: post /activate":w + "49: post /bot/messages":w + "18: post /clients":w + "20: post /connections/{uid_domain}/{uid}":w + "31: post /conversations":w + "30: post /conversations/join":w + "29: post /conversations/list":w + "32: post /conversations/one2one":w + "33: post /conversations/{cnv_domain}/{cnv}/members":w + "48: post /conversations/{cnv_domain}/{cnv}/proteus/messages":w + "35: post /conversations/{cnv_domain}/{cnv}/typing":w + "34: post /conversations/{cnv}/join":w + "47: post /conversations/{cnv}/otr/messages":w + "9: post /delete":w + "61: post /i/legalhold-login":w + "62: post /i/sso-login":w + "59: post /i/users":w + "60: post /i/users/spar":w + "1: post /list-users":w + "24: post /login":w + "53: post /mls/commit-bundles":w + "22: post /mls/key-packages/claim/{user_domain}/{user}":w + "52: post /mls/messages":w + "51: post /mls/welcome":w + "8: post /register":w + "57: post /teams/{tid}/legalhold/consent":w + "55: post /teams/{tid}/legalhold/{uid}":w + "14: post /users/list-clients":w + "17: post /users/list-prekeys":w + "19: put /connections/{uid_domain}/{uid}":w + "45: put /conversations/{cnv_domain}/{cnv}/access":w + "36: put /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w + "42: put /conversations/{cnv_domain}/{cnv}/message-timer":w + "40: put /conversations/{cnv_domain}/{cnv}/name":w + "44: put /conversations/{cnv_domain}/{cnv}/receipt-mode":w + "26: put /conversations/{cnv}":w + "38: put /conversations/{cnv}/members/{usr}":w + "41: put /conversations/{cnv}/message-timer":w + "39: put /conversations/{cnv}/name":w + "43: put /conversations/{cnv}/receipt-mode":w + "2: put /self":w + "7: put /self/handle":w + "6: put /self/locale":w + "50: put /teams/{tid}/features/legalhold":w + "58: put /teams/{tid}/legalhold/{uid}/approve":w + } + subgraph { + "71: [brig]:claim-key-packages":e + "68: [brig]:claim-multi-prekey-bundle":e + "66: [brig]:claim-prekey":e + "67: [brig]:claim-prekey-bundle":e + "87: [brig]:get-mls-clients":e + "65: [brig]:get-user-clients":e + "63: [brig]:get-users-by-ids":e + "64: [brig]:on-user-deleted-connections":e + "70: [brig]:search-users":e + "69: [brig]:send-connection-action":e + "72: [cargohold]:get-asset":e + "73: [cargohold]:stream-asset":e + "77: [galley]:get-conversations":e + "81: [galley]:leave-conversation":e + "85: [galley]:mls-welcome":e + "79: [galley]:on-conversation-created":e + "74: [galley]:on-conversation-updated":e + "83: [galley]:on-message-sent":e + "75: [galley]:on-mls-message-sent":e + "76: [galley]:on-new-remote-conversation":e + "80: [galley]:on-typing-indicator-updated":e + "78: [galley]:query-group-info":e + "84: [galley]:send-message":e + "88: [galley]:send-mls-commit-bundle":e + "86: [galley]:send-mls-message":e + "82: [galley]:update-conversation":e + } + "0: get /users/{uid_domain}/{uid}":w -> "63: [brig]:get-users-by-ids":e + "1: post /list-users":w -> "63: [brig]:get-users-by-ids":e + "2: put /self":w -> "64: [brig]:on-user-deleted-connections":e + "3: delete /self":w -> "64: [brig]:on-user-deleted-connections":e + "4: delete /self/phone":w -> "64: [brig]:on-user-deleted-connections":e + "5: delete /self/email":w -> "64: [brig]:on-user-deleted-connections":e + "6: put /self/locale":w -> "64: [brig]:on-user-deleted-connections":e + "7: put /self/handle":w -> "64: [brig]:on-user-deleted-connections":e + "8: post /register":w -> "64: [brig]:on-user-deleted-connections":e + "9: post /delete":w -> "64: [brig]:on-user-deleted-connections":e + "10: get /activate":w -> "64: [brig]:on-user-deleted-connections":e + "11: post /activate":w -> "64: [brig]:on-user-deleted-connections":e + "12: get /users/{uid_domain}/{uid}/clients":w -> "65: [brig]:get-user-clients":e + "13: get /users/{uid_domain}/{uid}/clients/{client}":w -> "65: [brig]:get-user-clients":e + "14: post /users/list-clients":w -> "65: [brig]:get-user-clients":e + "15: get /users/{uid_domain}/{uid}/prekeys/{client}":w -> "66: [brig]:claim-prekey":e + "16: get /users/{uid_domain}/{uid}/prekeys":w -> "67: [brig]:claim-prekey-bundle":e + "17: post /users/list-prekeys":w -> "68: [brig]:claim-multi-prekey-bundle":e + "18: post /clients":w -> "64: [brig]:on-user-deleted-connections":e + "19: put /connections/{uid_domain}/{uid}":w -> "69: [brig]:send-connection-action":e + "20: post /connections/{uid_domain}/{uid}":w -> "69: [brig]:send-connection-action":e + "21: get /search/contacts":w -> "63: [brig]:get-users-by-ids":e + "21: get /search/contacts":w -> "70: [brig]:search-users":e + "22: post /mls/key-packages/claim/{user_domain}/{user}":w -> "71: [brig]:claim-key-packages":e + "23: post /access":w -> "64: [brig]:on-user-deleted-connections":e + "24: post /login":w -> "64: [brig]:on-user-deleted-connections":e + "25: get /assets/{key_domain}/{key}":w -> "72: [cargohold]:get-asset":e + "25: get /assets/{key_domain}/{key}":w -> "73: [cargohold]:stream-asset":e + "26: put /conversations/{cnv}":w -> "74: [galley]:on-conversation-updated":e + "26: put /conversations/{cnv}":w -> "75: [galley]:on-mls-message-sent":e + "26: put /conversations/{cnv}":w -> "76: [galley]:on-new-remote-conversation":e + "27: get /conversations/{cnv_domain}/{cnv}":w -> "77: [galley]:get-conversations":e + "28: get /conversations/{cnv_domain}/{cnv}/groupinfo":w -> "78: [galley]:query-group-info":e + "29: post /conversations/list":w -> "77: [galley]:get-conversations":e + "30: post /conversations/join":w -> "74: [galley]:on-conversation-updated":e + "30: post /conversations/join":w -> "76: [galley]:on-new-remote-conversation":e + "31: post /conversations":w -> "79: [galley]:on-conversation-created":e + "32: post /conversations/one2one":w -> "79: [galley]:on-conversation-created":e + "33: post /conversations/{cnv_domain}/{cnv}/members":w -> "74: [galley]:on-conversation-updated":e + "33: post /conversations/{cnv_domain}/{cnv}/members":w -> "75: [galley]:on-mls-message-sent":e + "33: post /conversations/{cnv_domain}/{cnv}/members":w -> "76: [galley]:on-new-remote-conversation":e + "34: post /conversations/{cnv}/join":w -> "74: [galley]:on-conversation-updated":e + "34: post /conversations/{cnv}/join":w -> "76: [galley]:on-new-remote-conversation":e + "35: post /conversations/{cnv_domain}/{cnv}/typing":w -> "80: [galley]:on-typing-indicator-updated":e + "36: put /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w -> "74: [galley]:on-conversation-updated":e + "36: put /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w -> "75: [galley]:on-mls-message-sent":e + "36: put /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w -> "76: [galley]:on-new-remote-conversation":e + "37: delete /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w -> "81: [galley]:leave-conversation":e + "37: delete /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w -> "74: [galley]:on-conversation-updated":e + "37: delete /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w -> "75: [galley]:on-mls-message-sent":e + "37: delete /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w -> "76: [galley]:on-new-remote-conversation":e + "38: put /conversations/{cnv}/members/{usr}":w -> "74: [galley]:on-conversation-updated":e + "38: put /conversations/{cnv}/members/{usr}":w -> "75: [galley]:on-mls-message-sent":e + "38: put /conversations/{cnv}/members/{usr}":w -> "76: [galley]:on-new-remote-conversation":e + "39: put /conversations/{cnv}/name":w -> "74: [galley]:on-conversation-updated":e + "39: put /conversations/{cnv}/name":w -> "75: [galley]:on-mls-message-sent":e + "39: put /conversations/{cnv}/name":w -> "76: [galley]:on-new-remote-conversation":e + "40: put /conversations/{cnv_domain}/{cnv}/name":w -> "74: [galley]:on-conversation-updated":e + "40: put /conversations/{cnv_domain}/{cnv}/name":w -> "75: [galley]:on-mls-message-sent":e + "40: put /conversations/{cnv_domain}/{cnv}/name":w -> "76: [galley]:on-new-remote-conversation":e + "41: put /conversations/{cnv}/message-timer":w -> "74: [galley]:on-conversation-updated":e + "41: put /conversations/{cnv}/message-timer":w -> "75: [galley]:on-mls-message-sent":e + "41: put /conversations/{cnv}/message-timer":w -> "76: [galley]:on-new-remote-conversation":e + "42: put /conversations/{cnv_domain}/{cnv}/message-timer":w -> "74: [galley]:on-conversation-updated":e + "42: put /conversations/{cnv_domain}/{cnv}/message-timer":w -> "75: [galley]:on-mls-message-sent":e + "42: put /conversations/{cnv_domain}/{cnv}/message-timer":w -> "76: [galley]:on-new-remote-conversation":e + "43: put /conversations/{cnv}/receipt-mode":w -> "74: [galley]:on-conversation-updated":e + "43: put /conversations/{cnv}/receipt-mode":w -> "75: [galley]:on-mls-message-sent":e + "43: put /conversations/{cnv}/receipt-mode":w -> "76: [galley]:on-new-remote-conversation":e + "43: put /conversations/{cnv}/receipt-mode":w -> "82: [galley]:update-conversation":e + "44: put /conversations/{cnv_domain}/{cnv}/receipt-mode":w -> "74: [galley]:on-conversation-updated":e + "44: put /conversations/{cnv_domain}/{cnv}/receipt-mode":w -> "75: [galley]:on-mls-message-sent":e + "44: put /conversations/{cnv_domain}/{cnv}/receipt-mode":w -> "76: [galley]:on-new-remote-conversation":e + "44: put /conversations/{cnv_domain}/{cnv}/receipt-mode":w -> "82: [galley]:update-conversation":e + "45: put /conversations/{cnv_domain}/{cnv}/access":w -> "74: [galley]:on-conversation-updated":e + "45: put /conversations/{cnv_domain}/{cnv}/access":w -> "75: [galley]:on-mls-message-sent":e + "45: put /conversations/{cnv_domain}/{cnv}/access":w -> "76: [galley]:on-new-remote-conversation":e + "46: delete /teams/{tid}/conversations/{cid}":w -> "74: [galley]:on-conversation-updated":e + "46: delete /teams/{tid}/conversations/{cid}":w -> "75: [galley]:on-mls-message-sent":e + "46: delete /teams/{tid}/conversations/{cid}":w -> "76: [galley]:on-new-remote-conversation":e + "47: post /conversations/{cnv}/otr/messages":w -> "83: [galley]:on-message-sent":e + "47: post /conversations/{cnv}/otr/messages":w -> "65: [brig]:get-user-clients":e + "48: post /conversations/{cnv_domain}/{cnv}/proteus/messages":w -> "65: [brig]:get-user-clients":e + "48: post /conversations/{cnv_domain}/{cnv}/proteus/messages":w -> "83: [galley]:on-message-sent":e + "48: post /conversations/{cnv_domain}/{cnv}/proteus/messages":w -> "84: [galley]:send-message":e + "49: post /bot/messages":w -> "83: [galley]:on-message-sent":e + "49: post /bot/messages":w -> "65: [brig]:get-user-clients":e + "50: put /teams/{tid}/features/legalhold":w -> "74: [galley]:on-conversation-updated":e + "50: put /teams/{tid}/features/legalhold":w -> "75: [galley]:on-mls-message-sent":e + "50: put /teams/{tid}/features/legalhold":w -> "76: [galley]:on-new-remote-conversation":e + "51: post /mls/welcome":w -> "85: [galley]:mls-welcome":e + "52: post /mls/messages":w -> "75: [galley]:on-mls-message-sent":e + "52: post /mls/messages":w -> "86: [galley]:send-mls-message":e + "52: post /mls/messages":w -> "74: [galley]:on-conversation-updated":e + "52: post /mls/messages":w -> "76: [galley]:on-new-remote-conversation":e + "52: post /mls/messages":w -> "87: [brig]:get-mls-clients":e + "53: post /mls/commit-bundles":w -> "75: [galley]:on-mls-message-sent":e + "53: post /mls/commit-bundles":w -> "85: [galley]:mls-welcome":e + "53: post /mls/commit-bundles":w -> "88: [galley]:send-mls-commit-bundle":e + "53: post /mls/commit-bundles":w -> "74: [galley]:on-conversation-updated":e + "53: post /mls/commit-bundles":w -> "76: [galley]:on-new-remote-conversation":e + "53: post /mls/commit-bundles":w -> "87: [brig]:get-mls-clients":e + "54: delete /teams/{tid}/legalhold/settings":w -> "74: [galley]:on-conversation-updated":e + "54: delete /teams/{tid}/legalhold/settings":w -> "75: [galley]:on-mls-message-sent":e + "54: delete /teams/{tid}/legalhold/settings":w -> "76: [galley]:on-new-remote-conversation":e + "55: post /teams/{tid}/legalhold/{uid}":w -> "74: [galley]:on-conversation-updated":e + "55: post /teams/{tid}/legalhold/{uid}":w -> "75: [galley]:on-mls-message-sent":e + "55: post /teams/{tid}/legalhold/{uid}":w -> "76: [galley]:on-new-remote-conversation":e + "56: delete /teams/{tid}/legalhold/{uid}":w -> "74: [galley]:on-conversation-updated":e + "56: delete /teams/{tid}/legalhold/{uid}":w -> "75: [galley]:on-mls-message-sent":e + "56: delete /teams/{tid}/legalhold/{uid}":w -> "76: [galley]:on-new-remote-conversation":e + "57: post /teams/{tid}/legalhold/consent":w -> "74: [galley]:on-conversation-updated":e + "57: post /teams/{tid}/legalhold/consent":w -> "75: [galley]:on-mls-message-sent":e + "57: post /teams/{tid}/legalhold/consent":w -> "76: [galley]:on-new-remote-conversation":e + "58: put /teams/{tid}/legalhold/{uid}/approve":w -> "74: [galley]:on-conversation-updated":e + "58: put /teams/{tid}/legalhold/{uid}/approve":w -> "75: [galley]:on-mls-message-sent":e + "58: put /teams/{tid}/legalhold/{uid}/approve":w -> "76: [galley]:on-new-remote-conversation":e + "59: post /i/users":w -> "64: [brig]:on-user-deleted-connections":e + "60: post /i/users/spar":w -> "64: [brig]:on-user-deleted-connections":e + "61: post /i/legalhold-login":w -> "64: [brig]:on-user-deleted-connections":e + "62: post /i/sso-login":w -> "64: [brig]:on-user-deleted-connections":e +} \ No newline at end of file diff --git a/docs/src/understand/federation/img/wire-fedcalls.png b/docs/src/understand/federation/img/wire-fedcalls.png new file mode 100644 index 0000000000000000000000000000000000000000..35fb0ed9d2a6b2b09c69bcea79b3d6d6b527d72c GIT binary patch literal 728020 zcmce;bySz_+AaFC5kV0}N<|bA=@1c6KtxmoL0VEu8l^i_K(P=JDG`+h3F$^b0cns9 z5$W!(Gk?DC`u6_L-ea8e$JysuW31&;c;b%hn%A83x_#tsTp=f=BP9?BSY0CG4s75Q4!x?lOfl|#g1*!sugwd^quT<6|oOo?OYWR*iKeLfma}2e0Ow4i0?_f z6bU(O|NJRPxG(9SKi!^Sm(kpUKgZti^76WT`SSAe^2w7YJFQNgI+deQ<0~sIEiEe> z6JA+SVP$PSY{p}p3XXRoKLyVSq2u^}QN!o{H7wxW1;KzMKRGH)wE>_T8bK$LtO2_v40XQ#_W$@Aa+ zrD2$4H9yj{x;S~@2Br9p9XkqEMiSPR!reEP=Z1fJQe>zl)*H3X*mt|y*S){Ew#0AR zyf*h!*7N1tw}&m3AMDsuTvD<;Jv&gXo?{hNGwia|^Js0xYY%mjW?u8sQif@ZGONh( z;~(Xx^Xw;^sgwDr6U|%aPcK*4I+{d~0tHahvGKx;t1Cwmd!8d0&y0 z>go4R)||Y&yzK0$-aUKvoTS`cbeQnIefQK`9gMiv85R92Q&Lh=Gqbb189CI_jT$0% zi;D2_&UEHjd%k?|;K83iy5d*0w6u(k;|lerZAY8U+fu#uvhtEeD6$$aH$+{JkqIsJ z-5iSWZT@ttgiAZar0EKS>o+d#bffxcnb30g1KY&QW5V5pbc(mTt-aw;cUzxVnd$!K zF;si<#Svk<=|7jnA4@Z*rLS%Jm*2mCUo+RHr%XG~j)RBCPdn-E;h7))r>3W;)dEhf zSnoTV71LTTo;Z+_F7Nc_z5^Q@+XuPw&193CUMo|*SRWF{{c3a?J`pc=%#zYXPfva9 z%@c0tg_9~TsECsocNe-oJ;bpzJ(fc${#Y_X$562+hcYqq8H1?X?=Sb)h%+$Rk>%?) z@J{O%K459OcX>L^uy$ji1Q)nxeBZH@vnK%x7rPnsUGTW80QKuEHhVsvw@VrERGJU*Mp^-Bd+*nns?0frlqEm?%88*W@bbqN+6i*wNfDv(q{jBe;OOh;&lFs z?+$BgYfNCS-FSP9EW_W0g5N8BA(nb=7>kUWUGNld#1l{i*XXmLDGj0A7Tg8`pRNe%gCJE)G{eQ+A~bQYa4-kr}O?R7cbtI zW{%KHQM2gErJ|-T+w1i6={XWcSA1G~2Ak+c$K9d}PID~PO~V^+Y77ttCJPpiin@(7 z#&w$J@7Q|3px{DSO}1t4(sZ8Pc&^ht>$1K3)hnS#4j*<}7;DS5HFa~rBfPwJ*aZ-T z80~iuVo8M#`Gtli)Em8)I$6zE@b_B&`e+}+uOfGMW8>5G*L+`Mypv`(vJkSkG;?3O zMc<0TtX*Xa5p~~)yylPfx^6a?D&%CQuc!C(!=+teWs)`%V>_G)gxirzOH1FrNu2#| zP!=`+obPUHO3zeJiCK_*96KAELY(4?Ro69tX7i5B=WEWg_a8poc40m1UQcn0wtE-@ z11&8gngZQ5E{+d|yYXUe&M{j?$ccfOiII_!nVDHYAnQHxI(F*RBe$Whyv3J{mzJi9 zk?_^Y>r4INt_!rG)>i9Fy}m_jGfx8o0&tV4sHm{8BL%#PD#=7a(=nzicC5~c zm(><)FWhbAw19w0oMOCUd}pV|%H%EvmlUUY6SuXRFku1nc8OJ$3CDiSgxmVcGVlWI_e^i?y9)`*3D_iuH@!Y|?c9N9D?Y^i$67z12t9#BFCehEzS!OUS#Ou< z22v09mac+aj>e5-u9<~}=H)^uB0dP5nVD&=jBDNVN&M0!D$*UI4#>Y{z6^tJu13jU zxl)Efyf(;Rg{7lI<9#Q`A?D@~_dUVvJErLc+7A z*FQCt_x%0tx7guEEQ+j3CMG75#B1m3*UC!Mnzw>$zst+ZcOPIrW71gO)YSCjhfh~a zmU+in7M9orwtaYt+%2gn-V-M-vDH`q_^~`yvhTu$3(M1C!uBs-ys$$w#B#x9X=&UT z>z9@`J5(Q`?bsiHH#E99K`JME@ZdovX683<-XKiA*(}1xjCB`@uEs<}oamz7wR5N2 z#`0i>stg`P;Il>a8mtYUt)15{bji7uc4uwLadsf!^=snGRsX$8N-lkn`)#Sfzbl7L zEbR5GSE)$#)LdFjKw^jkCr|eG^-+i;JUflI^Sim&*)7bEwzSPYKJFLWoIpnWoMa@? z4J2z|uG6PaW67t>u?8R?{H>YA__j zX1c?8;)Gsjwk5Kb%+;$boSe~K7)8KA!dCf(q@3afA7$ck)B>}e}Dh@co&uz zU@HSdH~`RDi_YwY1e3i$ppCB)lO(QOxpMjP_ix|y4Gi{+e@aYL&oo~`RqrEW!oA&lr58;|%dD5SxvEMq_93*^WzZ!%NEy*GD|U==l2jCMR=UkR2Ev z_6emW-s8QAyu3UwFK>H>Y1w8n*2>zX`=)72QdoGnHXujn8Dsg?rUYX^5>^gAzS)%` zKu^b&IpeV8C?YT?#2&Gq{H2+Fujk>eeSdm;cQab$=$d3lpq2 z-d(g=_u*1YvbOum+-b&0EHnJjaAVw0tlw!G8{*Blcd-VHY^(!bcNgK`v5vNF-Fnun z_4bIA(mqUPsTv*xNS|jj+>m2E$f=ooPEas7SCaU<1SLzLFwB*TwyVx|Cax^>Ipupsd#8ugH|e*pvaSvg+4TK;(diJf~717yi)#*zO1%tP>`DCVxhIG&OT z=^Ge0>o}vYpt6hji|==icjnCYlsp2Ot}ZWk7){K28y0ru#bF}O!?F#^HF47Eq@v6H z^XE^2^J3Diw1N7FD`7&mbUeD>Lhlx~eY(Y`(6Z<6tC0`TKEG&D73 zG*5KB%o8v9&Rqp0pQro%$=KK!Zc!V|cV%!}+24_-dw}RKEG(?hBF@#9RQJAMeWBy% z(W7a1s!ckxKgW#{%R2(WiP7H6o8{E0D<9;7at{&azaqMgRIB$AMxG;mx*?xLC+#3K z?mECM%^j{XrW%sFmB9RkTUtk_2fg1kY-?D!0)!^ zpwP0FFp`O(p%gE~cOx1Tp&K*9{8wSCzpqUY%Th{EUS3%##8)x9y8;CoiB(iq%4iaE z3Be;5158*e>#PIc326pE={`71T zn;<3sPOa_oK|T&8jLyiDzmIF=QHZ%Q-kvcE5G$j)3&VNrTUpr^2?_RFsowyh8*Tr& zo!<7XUsDY#Z-k4u_7v|>Xxfs)c80{X`IDukr6US28O{BF-%cnFTMv8x0I64WJmHt$ zws~y}iwV>)*wf6EKGK@h|NIy;2ZzjYzt*-k^Umyg&!#WgY>n<%2iJ{_hk+ZV|Giu8 zDPG=&l9FD3S&?fpGW3=sk*zue!JtdS#MNgSW~ZbUJ0 zadBzQzbn*3==8~xQR(Tg-8eZo3}^-#;}nt$7SwNiq-WcaE&1{N1Y-AMMOyxoM{0?d!tPV>nE313=h7ehhGk^kF`2(@?Qr9C;9P@N> zT6}%5!%QaEc2wEx^qH4o)?Fsi*Gx>?FQ>b2x(V5vnvSZ)$~rj-70EGC+78wA0MJnS zM79scBAyT~VY_Tpkb^JYg2T|qkY0cLV)={8>x6m6UV zy|_5Ip@Iq-h2_qDN6q4Mm8#1a+^g>HA&Bu;)YhV&oFrEMNt&VW-VqD6*7o+jM}+1O zXw0gdCkq5D|9t!X`!`@rS!wADI)0PVXS8?orzNTGWI4{>@zz1AylOl4CqDBolDA+9 zH(3dZS8;rGmI;5d^@W>JA;vS!!I5QW%mvn))$j6<9XNPUanJmZ%1XeoM%IHq5`~3@ z?(6f^7wDCQk#T5Jjp{$5`s>VhRABZWeoA}RjAmAwhlfY^^SSfq^Uo%jkg2P8NS*xn zXzvlaGe$3Kwixp1zt1Eqc^yc+=tVOsIN7jMjV3yC7ALxdE&oVN{wg?0=@Sz(h_3|N zq@O~T!c^E{SQC;9O#ol~{eR{=&aTXl+Jmb=-V90^h?H8jI}op)!7paGsd`s!mrzt> zq*1_QiI0ak)ZPl)*L?l@;jF2=z}{=E4Gtrp(k4`_W_k`~j;mSD9w!=FVFWJd23X<7nC5 zn9#d%-SJ%tOZz?Jt%;IKcl{!YTw7lGOZ^xD|?2(a?B-^*H++w0Ueeem&K&x8E zCb~X64miaXQ3!}QK0^?r4v3M4Y+UN@<$QPjIlTZl8hzeiMOLKYe3xZj0bVvXBL1un z;YZDlEkZ(0j(y3#S_Dvw`!3AS2S`N$*j(}^`%rdrj2+Ammv*7_pi)o37ZjzSkCGD; z#}N;QR9bsO%ge8-%~Y@@)9~4|0%HwS1t{}!I3X;&ieD83$VH)ufD@mBd-QqynLlh z%!0s8|5;ohwLUM3}kMtcUgr4N`POnvJs@Lb~M0Nu~SAB%sWAoZg14hRff zU0QNk?6Ozu2c3qA#%=khG~M0Z>FGl!y7IJhtWI)qO(JR{HDZZ?V>(QJ_1d-RnHeif zOH_N0rJ?|RF}vA71DWRZMf zzIE%?ns*mM!^7QCSpd)Y`Q6mXTgsSUTvTB>*xBCTLw)Fw@@i`Qz3;R}Av{mLIu1RT z#9DZZLfF92kUr=3C2?_fc6MDoy&T(#Uj?gRgN*t<*2Rz(od9m#zkfeUZz}J4P5*?1 z(I6f@#CBv3yUAbX0O!5EU@>$2z%lTx_BE(zITB-yntyys=xWa+hVr6XlRq3yc|n&~8TLGXv9_aU-Nyagybcgyht;G(KM5=49==Iyx7_!y=j}GBeG7fWO%U z37DUskBv(qj#~E-IlbNLf_a7-zULwdu~?kvXblX$;dMc{C|IZ{D%sY>wxF!BC6+Xc zxW>$vQglj&iUl2KA|;LwqfDZIpW)b@XP*r0hV9$bo}Zx{%u`wI0X|`BYD!jQS%Qi3 zmYNzWlcnX^!BUsc-b8?roSmJG-_ZT+fUqNQ_g7VIZE{ZMJaQ#(uymrbX~3@S+s~al zNApXjZm_tk&^44J$A0oCrv+?dFlq!iC~-o)DftlAl;a4OStBEy$KG)XC#wBviS zxmS)UKW74|%G>+Vv}vlXnSFhIh}~5o{MRGuX+*t14nC#ewH;AB`y1m;>h&=)@>}`k z5BrFvr-A;2)MVP!%nU2=&Yt1NKLLk^!cm>|^zjdIOwl*#IZIAn% z832i$zk$b}NPt#}O2&NR|Fph3Yu-*FUK1`_`^Lf1F~h9wmA3naRqU9J^sBt);iEsr zBFOHRlny4%kt-cMG8PqMOrsfUFE9T}mHq77Ji4?_viCV`>w@QmguqnM&L)OD1Tywa zO-%)!c64%TurA%T_i*foPJSjPeK0pl2}~DgD)FMzB`}mG9{qm zEG>C@dn@aZf`yZhdmXllHQA~nL6gQZ2Lj`E=CdkIq?;S=8=b|_K`6yhCW>!%7B~wq z>H`6F1_3F(72ax0wk;?s6Rn>kcybaBg}EjvS7!*( zaj>Z*F?mjLas3%g0tI1qt;lVigb@{v*)^2N2E4TEpOR0-kJ-eARBDbI_!_^zNo7GC zaGNs^RA6PlT@sk>*8p z>U&sF{*rItV;>(u0JzHYQ9pkD(n^TD86oNUa&B%8vPpoHfw_6&eCAQ7i1P(c^-wuD zFN_hZYB{-=VPRb$p@^&@t763a_rC$ZZ5T|>Z?ldG;Ns#!!0BAQHk7C=`|fIJs3$=>}W+j~o&3)_yomXAB{FmiiXB0Tg1B~DlL;LKFQ!Rt*YhD zLW4Ek&*|ys0W^T`01J@p+w58C1ig6lN)al}P@rdLI}jyy415auNj~3y1~-Qii(j+pIQ6%5eL|tj5Zb22Lf|Y z5RK6GEW2?v@yE)Ox0PAP+HB($H_KIGoTXh^JG~P!V>8}()KrJA>hp(nI|Xm!ZeAGg z0H32CN7_OGCZ*|wfPg(#KXK^=`y8aAvece;+uT@3y#_e2!ktA_0Vu4-T6xaC&(2-~ zZm0C|{FSDE_pYqA-Z4raV4y1u>SfOn>Bf@M3@YD%IcU1|{L-Vnj?T`e7k|v9`krB(X{ih(P zK+k=A;=~h;!kyQaEG=|U+6)=IRja6|;M6W08yOL@9*{OPGz{q)c0T@jIX1qXv7wsG`~n_+<9HC+0U{UQ6bDf-#^J^1P6& zDUYP6p(*>V@c!x11aq&&Jmtz%-9ZC~fAG`ggdjud)c<$^ zI8cQj@R8`swPj;v<+UOuE78~2zsf{;UyK{_UoUmCPIfod7c*MNBDNB4s6H`KQT>SH zh$;Y42!wjjqrh)Hc(9f3CD820%X|E;&W&ev0}dM8xnt2?sL2|@dXRKMOJtz`9iJI~ z8fq#gPRWkCVl%n5RTbRl!o^RZ0vWh(i}^Fl#IL>80kH|$aA05nBp`UwKVFaYMd)J*o56%a;_1z0W;72pbnZdIvuA`x%#F zI4Iy>-z`eF{3%+fOK-$f(W@hc=V?gy6kyJQnux^ovy)hZ2=jHPC}ptz`LkB7)xIGGJXNT`-9m?HUDuz(O$mWKwY17C2ce{r?%wV9L!Z)z z3G)ls(HT@#RRsdZ+0oHQ;&_h6QA$cH1pNi~NdPg&nLZRR_0+a@5Yn+zJ0I;rszk`B zM365jAtni-gp1u&rS4UqcV)hnA&URtd`Zn{vE6qE!H79v8 zXwt~((_)v|{FWyTnotgY{HY^wcV*=*)>cDk?QTYpqjgyo&&)Dz47|xBrQs_iWq9wc zZ)hkiC>VC$QGT@NKy(uTEM^lrNKhsnME;J?dRNrc=v;Sa1oJ&=5Bc#Vq3$1fZVqTg zv1NPpto?R^=SvRtOm@{2Xr^V2RLky2%16MQ4OEAUF8zL}_AVni8E{|6#U)=k2ipl8 z2{e@qwIta~&9|Maa@kN!badpUs(7~??wlkgY+4T0eE`60_W2gjn9&}hEFtj?Qah2y z^Pqbj5;8J01gAj7oAd4)=Q%lFXw~hx%`_K8R51u^S8Qy~6r?8Hh*i%>bcHGoBB%xG z{_*3-v3(_995x=I+amVxtU<-W`(p3sU-^zew;-<3Z{NOwRzTAArQ@%tsj;a4c-ia= zj4eZ^;)t3iNpy09@5??N0G|}}3(O?=QzU2qSFi8~!*JI-BXsv~j(mFpV(~G&KCIs#PK;cyTPf9J7wo4$yhv|#JnvDPc%?&vh#E(XiTUSR%=<{13y3`M7aBG;> z>4|52Sx>5?=n=W#5%{QkzegKy$*Sg-_ngJ+|JsKL0l>olFqKtDfb{=f-1bC!1@ZdQ zNfhD#Ue=HPKmS2PV_2Jls(?~B*zc|EBoIazqvTH$#fCkE*bWFnsPbhrpKjvGbjLxg zfiDRm680BE&zehv^{zWaL-W{TFCqSN+0{3=m>3;}+j~r~! zfBtr0?|+Ed|Jg!?srdWP|0jMh?Tjr(a3rXqLEi7`H!kP}mDSZz;d@9)@qxpJpH)A} zu`Ep^OkcZpt&=i9N=ZwLGeAl`!{oa!7YmC61af7an>YQC5?Ycpm#0lKRv=Og1Xk+v zhE0Rr0$uPYQ25-r?Ynl(RwB*1Z?4Qs!tFNcQ=;Q;~j@N^hck$QdMOAH;^)2 z7TOHgmLS2_h6?zqLyLPA9uDPVwmMW`wl^FasMDO0n&Wx!Qn6e&^DGjthh zGNM_gEkN1UYfIR{_9(=(e*u32R7_U_6RX56UGY7t#3~{rw9=%a%`YNy_N?-tQzJFh zMp4iYZjK21kQJ)AWUpPjm2!JaQD<$ftl>mRKtKlAaW_X`YFi6Y{v*kXtnSPG)WFjL zQaSwyVST#}h{K~$(^+`5j96T#iE8QQ&PP$<2Wh<-J=0BFZs0brUw`WsL8*hM*lut_ zq>Q#+ovi!tux6=ZqmXrV)pcn~=lO&?k+_6$Wu>jXUG2DET`h`2498%ZD89V>CF7bE z)OU!BGnD{YWzT4VEA$KuzH_u>iNc!HhLo3caxRicG9$AsPX}C}hLj9bR?#iBjlpac z0V1kH5H;i+n-cXy1KgBon!q-djmn>SAla!Tib3gaSO);<-`|9FI4#Y`uI4m zj%N`iQy399C*_Rblar9xF8VG!k5U{p)Lefq2x(xVCL!+QE54cyKYzl&@MGi0S&8oK z>gz8`kPY^1-}-S=^^xdaJslm!z{>gO2}AUcEbkGo-+_9EcmD3D0=Rz^b}L}rbvili z{hVgt%{I&Sz?_-~;~H1{lS5zr&_@RC5B%BrYFzTxid8*8rAe6SdshRf7KN)5!fq~# zij81)miwU>AY=6hn$E{4B28g=z?AdDw+M>`J9Viap1n_s`S-2KVVU|6o)dUB-T3DK zD66%l*w|PI(w5Hh;*V9?Eq+End>0Ds~8#maMJM#7m(}U0Z5xZO!`S^fdHrT{7yCB(;nu z4B@X|&p|DmvY$&zN&+4oC^OZZEw#G~Q5$sE+7c)#OviK#m;VnJ7nh}JwKA=>EiBL{ zcRubAwW>Wwf3Agyw-O(YJ{Zj_qS^x$Dn8R1Qx+%3Y_qpsA>AUAGr_M*PPe^x82e- zD6pg8ZPu%fYU~#WzmsOsCFJHJFW=xzPETSu7BbMVQj?f6=+t_NcV)|<$tXi#(!2t! zPc=+1SzVTtj8|lxnLI}nM(~$~ojwD1Yy!))^x`0wW?8syZn(OwVe{dYmio=x(;cUN zZz%$nuz>2;d0~%CIFv`_WUXpn+3VNU;mll~bQJllIyNz}F%Wuprq`JBiYWYkpH-zr zJ6cXdKu0;h?PBRdvxU>1Vt(;6x-JZ=jC81XmNDC2oBap%XbP4KI|Zm)|@qI zj?jZ{0DM~Q^Os$k27iY&nwp*-dHPIsEZl}rMr!)E!PJt__912kfg$7}1+VTm61RJ( zw_pu8L-$-hF4CT=q1z4{c4#T)4HhvGpFMcU#f8f5V?eNc?GqpQz*}QK>DFIC<+cWAM7Q(gg9^&Wj-Q5p1Rpd6i1Kol)2Hhl-8D5d-qsk74CQI_K&n}u=|ApQ{_EGT zhK5(`j`((5H0PvwlobQr|nS)rFecp z0mA6gG{_b|KR@2!XBHWs)IRYNpHFCna`B1iS~lfqOW*uc=LvI`__dd7*Rahvry@*1Rd}&Q9v+u{H{ zSa20^ivU?ei)s^CY1VT%Lh|<`#kvr#Eiod9CP&WQ`_gC|M^0m(c*8*$!Gt&nbnfn( z$m!5)f^v^%Z1GXHr9+j3q4-y%Cm6BNUHv2VGEX@I&hQOL>j=)#N&nv6^?}lD-hXfPUMHAG&l^Sp&=!!`Nf5VCy8-B+)-R>Mi+BI~ z+`1GdCMHx4`HR1tVOdF3%UFRcTv{_CzszCl{H|D$lJq@1q5c%o=$XC zz@ckaoU8O-Jq5P%ckbMAbj(rKfvH6@VtF_w9MKI{BK=oSnzn235glu=Y@Mp#<88~z z#id=~Wb5j5fV5Oo<}}QRFy~`TA)Ub!aQW#Wz0zkn&WjcXdLv0#vb(oGKqaiylZSu- z8^hN7U%I*qOlBd1-V2DiU88m9N028^=>6a`Qie)!G>a_v`J=Q^RabxH?Hv+pqLc*( zLarHXS~8k}@zDPlzv+Gs3~c#yEA7dXCu9=Mi2+6~d4wtj*bK$?ZQLD)2OXaAH*b^< z-)hUV=MuJDIY_B6e`L=g_S>cniRxLb(qE;WF7SPl=8qQ`ZwSmI-TnX@s5+Q0=FO-= z8cpn$|43jmLEK*(8mz$ez@GCF{>pD$ zeBJRfU)P#G*wGfzDrVj^$;gFc3$?V9Py70#ns^KJ0#M8#mjaV3wmdV@2DJ^v!j?nJ zB%ps_KvF`2f@lT0zD>v0;v`)gXBetVu%@OTl?jp-nwY+3dy*CWdWz2H&z~Vk8^e-@ zOOEA}T7cV9&o1$DM5K%g?dVG`Kini(4kE{Am~D?2u4tsP&DA{9i*))(j-_$M z6%zx2J2dw})4tv$<^{N~YD4%do0^nZ0~SV_M$v=7%EpFgY8mKNc_t>BM{!<1hr09q z#fR}O-We6r{MvPK1IoKfxUsm_LdD{x4V8vTeu07;A0JOJl_YPj`~=;LXc&uAblO;R z{Le=b()huZB}UYOuKlcf_~_A^Qg*fUSfFxfW~fD&Ke1|OES}(xw`e*Pk8SisIp&Uz z4v+Of6-a|lMWuMbb9Mqz;~iP10ZXf^(8;p1&w1-$VfpdcD&hiUfZMG}Y+#R2)6fWz z9MX6bKfkL|bwvdwsn>vAzS5O~HP*DOtlEl-hQ%UV`;&AQ)E;dn|BGRw;tl+^myY{2 zS~NYAFIc}Slc1jAHS#JX1PaG$j2z2bUV{o)W~5b-CM{b6qw0+?{3@EYld!RHv9p_X zWG3E^Yzzt#>Vd}(h7l&F$oo~)Hu0AdE@>0jy0kn)-L+#BMrY_;H`VP`KK9sc2}*>p z4juoo`bXsiNGuni&RKkLWKJU3ii3lIS9BewrLyPrS#Il2Kb7MKj@i53f=d)+t{Rx9 zMb9e%>xg@v6eV|`@-s{BS-dlcx^MfA9Y6GW;}q#=XdISj3@r4Om7BqdAadS8>Gu0K z1sxq^np+F*FtCBV85+W5Wq+VV_Mjce6Iv13=! zYv2hOSoKyg4Ja50x2C2BgXH9rp)$uPz?Bn4+iJB0Ynr6tH?7NX`d_=Q<=9WvOYL|6WR>}i|8Cas zPAE&#fR?XS;52-G_0H|Pa{VWlc@xIKl0peh&C0Te+W`Ttsr1muPpR#(5q1-u!^6Xf zN0Vyr?jn0)6B)&nj$4zjk-=|~l%b$|N=awah{<|lp6tss<)V6qb-uosuI_P}OP=CA zMEo|^${5?*tM1)6P@OT~1Xn%&9_(ow<%#g7$dhp9e*bO&hlbMQ-DG4oHa7WA^YQm5 z(bfWpX*2n29p*&fE5K!lL}38|O|VE9iD7TSqxAG?rQeAw@MPl~QJ3u4`V|&wqc7il z4)j|sL$4<#C6!lD5ZzeP6E6o(+ja{jLjFR#Y3Z4>sMw2(!HzxPAo`*h^_Ql4HrHll zwX}XAZIUn|8gepJr>{&@f*4Dz`?pmm-_txSXyr*UgF+tO z21s+C01zQNB0%4X5mR}C(gL4`E;uMIz`{}_xAN=~dB)@Ktw1UKoRtN~#Hkx^-;?j# zyBFT^0g;-RVlRcZrkin+v_DA)y2s;FQsfdMQ`PC*D|J9RUxyJ_qkc_W0vP5W#|GC& z(m?Kxw;moI#JXs35RF(eEC=uC>koa}7nNZJzM-YaeG{17O2xv<`C&ZAkHA|77?X&i zKv*KAm|lSn9W@KXf+5lh8!BO1K0DhD2=AJ<^T(K|s~{r-%c5~EhD+>&+>o=fg4ZLV zo$s2M0xP#|+lJPcNaciOS5Cbt!`KW3-e`Ay;|mGb5_S)Cj!ySD{O~NhzEbz>p~$IQ zEC*qskdLTWTuKxo&oP}>EpjW6V>x{8-fwsgVUS9AnTPzJdAFOmjdx}WZsXaqWy{{Z zdpjR8ia_}iFGsX(dxbK0zxdkv`fxairBj|`X#65)VPz#6Z6%I>P-y^zKR;CeP*?`O z?fCwrIgLB;!m5ieCc-45cO7NeB@#i9uIhjK`56|O%!JTooF4Gh-YRyB8sS! z$KI57TdB%~oI&qgPavOUp3@gW!6*pjN}9JBM{Y2T}0CU{JCr%}s1V&75et zOEZ)elI#X7mFiEzeukOa31kU5Q&QW+fXoH zA!&nCpoAMG2cxIwqelR6N{0N;YBdqzKk}Plj{bKZfMDf_?7VqW)ADexFmXl2*RY_# zRjQo-kI!KN>KD=Hu;%QwvxNJvYvBwZpTBweOn>F@AnfYaT-HHn&FO>{N=6^t?XL*% zq{uA!nUnhLu8P;sn0<-$Lis~zXR+7T=qZjH8{tm9S(^2rzWb?)-SytPf{->03|!H} z=lK$r#Nkh}UdFyXm-VX#u7s9YxRuuBSV2-xS5E3F*rL#MGVc45Qt3bySqZUu)bl0c z?98bpbT5!>e*m|RXLwulaDdT!&7H96geYVrvN17vj+5H*qEaXqn=mmq*VNLg;b6m+@z@q?t~qRUNf(fA#7-H4*uG_H`>f0 zUXDOx)CyPz{+Y;JiC+akLfmDR36a=LfT-5l-GLR-=0mSa?MV?Jl>x<0R8*8$PX9!f zXkbpE>P==p(SeK#s~)aq^ZN)50cz(0)Q(7=tyof~QI6w~8GOSB3lmu69eWN{0X4$; zDA|1L>HW+4eFIn8T3cxZueh=WTDW!IqW?vo!kWRPeJ7xPA{dsO!-XD6@W@552^i=- z^9u<0zP1=FKNr~jKbs>gn!(+i@T}X_wrBeT%idCKG^senqcCpj1i^@$d@IeH{OnBg z6vh^`%Se@`_NyMck*oaOC^;fd~B%^8`%mb&?O-^;xOU%mR6S2L8|o zTY`9%aQ7Vd}&#R#;8YCxX%lnv{N+c6v$*h>o5&Xy%v*-+9Q- zFI3xLSUm^KLuU}_;!Y)nJqc#e2Z?@YEk)EXt$-2@qbuIOx>GV1Edl> zI|k`$gEs1yt_7PX$LxC%C~cLmA0&1RMZrrf3!bm@1S^#T>Z$ra0ON*D>9v?kui|Yx z8T!D``1|{>udjD?c9Jk64hu2%p$su2cIu#tMUS;>lxIPKD{Q`uaMnSs@D(X~LC+N+ z)%GwSkPPGoq7`ve(HaDB5@@zJ{O@r8FTRND*RFlo#ts;B#eU zWfA!1LBpIgZD!YtH@U6(Iv_x^(50=Z$rcM12z2h5NAtGnp$Z|lwR2f1;5Fvdq)Ntl z@2mjJcv4K*ET2NJ#6RZ(Fj}G2Kw6qiAo`zNA|0Vxn*K3&sKW|vDhLAgP#wU9DGgHN zv;VvAgW8WLRGVr_#R6Fuk|XX=aFMF9hIJJ^eFgdXL)Ki`{kEy22Wkfx zIaq%JQ;(v+V6#zZBkrKy%9RuUP6Z;ASMSM^?OV6llN4<9h7&vj4g1nU06XLE7Np1? zo6_AITnfN6eXz9X}qspzmE2h;H<1gBvY(2fPGZ!SgpCkqUh*5?uRf9Z|ktyLOZJlH5#Q( zdb%L;gPjC%4eRRk+#HkV+w+dEcGeNgWIMRxVWk0ea#|r8Mj|w+(Emi5XP~Da9U3YZ z5tzg-*tV05*qgt&@tl5Tw zlpITTt@ekJuTqV`8a-~v`vG0KXkQo|7*Ot+Ej1YuECG2BYihpTcu0*%AkPwC;|doh z?+q*wXS1|q8UBf1De(nukvA?)sGWG$`D!OQouZDGi8NUWH&!g~fM5yy0w`gCl2BA1 z_uIaGyGE{!550eIFr39^Fu}ov8CVH#FWPddHzvWMGRwZ}j8|e?S&75`AvQFxPCW#R ziJArB*ulZUtUdjjA^%V)Q_eowJTzu!X@WZHdw2VUrwhnAl!1M3h;45Xixt*I2H7ax zV0HHu_JV!Iq)8u5>|`Z%Ee~L9dMa_;&~Ok#ZY{i9*}>u5@}$D5)*VgMcK;h^Lb~#p z!f-ZG5dGK2z>}(3DlIA`B!nzhQ*#5{!%%%M^ed<(^-jKzd%P6lnsl;n3aMoEC>wRL z@!qKliZ?f2iLW5tySLdsGD?vZ2!06O7r(10VI4t-5)tER)Sul`$K&tqHsyb$IH19l zXEj`T2Lcn_!GjW~cyYGEMs8g@y`uRwtJKy1RAz_7?lA|-wibwMB$dt88=A<0UL6GhH$jH(&G63oTAeMzs zs#!>HUN!DdT4N57MrZK+W)F@_n^6^fx3sT5V4Ez$)xbURu6`{MZAygWX#OW}Ikqm|UuLc+pix*KYg{>ceq z;|aqXpMRB1Q|67*KnvZxOSx70|yQO zdL(HViI_C92n*+dAylgTCX+#{c+pEKii;iY1xG{#b0tz6xj01xiqjk%{YB!@H2z0NBE)D8KCIxAcbhJN3VUl~B zc#Z0pkt$b@!|r%5kGMn?aefwv1CWz1-nj9C*e*yQSGjjDB`Yhduq9wiKAD*&fw_gi zxA)oqL0iKAYexjpzc9k_U%m{2QTBg~8GB;03elzsDTCJ&?G?h?>|18*me_QKIGX{f7fjzoUWy>uExCn*Y7H?ctmI z=a2vYn^OLV%_S@<4o!S=r;udc1O(LA5;bV?vbZluH867|ihJvHc6EK%s23sQD;Erk>C()-foN=#O;iT41~YCy zu{9bPo!DK8!x2_)MaiQPF+HT9}Dw^sA2_1y< z1C(Nw6Ivip5D#lvdq&F#Kk!mN{aLenmX>krOAz}`ojmEKU1(T)R6sxgjt{&IwwPH? zTr;((7R$l*wl)|Rs@mFu88+Jt!vXxJD(B5>)jCA4cTa*1=a}2|(EyEJFnp}d0KEY& z%v5$ejdDQHa$Cl67QummWnBBn$$|U&1Bp%Y(8h53jQi!qyJO-1*h?t>^z<-N`YcU% zx{?GIu5&=zgvV$kjX02cerTS7+kaB^{)J2;MrFRd^5QZik}P z{{FoOM~l6g05k=(Lu~Yf6zX+D3bnA1q`yw=sreKWb4s)j$0s1*4+eJU+9m)ft<3<> zmVN&|T?tzRj-V;!LVRwdWuT@W93QXdVnFjWe6Y`b!+G>14gOA&tSRc%WIlUMtX}AG z0P7hW#O6C8>lR2w&}r&w=UBqSxI}{}tRNIdMlxCWTxCkd*PmKb1WhMO4QWFyE~9N` zYN~EoxM5k(%wrmAIqMbm-DWewjnsDm)8=QT>Z+>u?B!M# z1|e0BZ9F+iL4uBnWH}a_W>IwG6%EbJU%?G<#f0N5iB>Du^)7f0mj|~JeH3A)w`(=6 z>@y|Y-H)J2)_6I*#BJsB+dUq+Q8hNp1m=rn)i{Et?F4Ix&G0CF>ue`95Qs?WDUp%- zD0Xk$xUuFi;pjTS_@c1z<+y3ve%28TJFLEI4z4-PsEk5j_~E)s^un339+7- zuj^__#f#C@oWog_%qz)kJzA%VSy@>_Ak`I)N@!t=c1Mj?UT*7D8fBWAvOZd%jOZ+0t*YrLCgZ=+PKS* zK&y+0^D5NP#fTBvm_ua?Ug(W#aQt8 zN6)jZ>*6s(-__JwO*JxdclF&sAii^TREXJieI86-j?Nf0#O}CRhu#n(ciLO7(VMW}WMA`$cH(>~QBZG1C`RCSJP&5_p*Y@8Eh>;qgDstv%& zEQ*7+yNDip!M)u$&u4+oZDM#H31#H`QT5Nd=!DSseT`PJ9b325yC?MlRe}aP`l|k$ z)+mz*&F&mFBN(q)+1dLBE^D9-%^E$)7c#E-v*arLgac=^WfJuya0pxmon597gAmJ> zry;GP4$ven9~Fe|k z4r_EefZ9&t;EdC00xbK*AqWq`A?Ld|H`nM`?Jh3<;L)R9MJ}i}h_fO>H*?nlHnKqW zTOX}I{eg^*eFlzY?kt1Q;o>5*#G~NPK3FA*H9R2>d)K?hnRCt7(RN520p`o;}};Lo(pYU<{BNZew0}3hBJg@z)EH zO-a)_;sGm!_b;A+ZLX<_)~laW4LoYQkCso->&BDE} zws`L*j2l9e7emik>RMPN!Mh2WRvLV#KG<~R4k(oMjxvSIczeUwZ{9@5{Jju9^!0HR zo9bX@2TEjN`-ywkjY=KhP7cc%LU;npgz3Z0@ZYc!9?=_X&q%1Tyh?mU0yhJwh=_XR zOd%ub$ra@UqEop+aE|Wi(IgysBxQj7d!)ajx*95GaB#3t^7I5$608X5GN@bW`AtBH z4xm5>l7=}xtoIku?2?13_Zr?xL*wvbH3d#10zro2xVMa$+9D%8-w7{^E=-(H@`-Ri zyu?HyzLjw|NzWm<{jy^DULPNLs3@r=h&*YJQLR-|%r^;dS2j&lNtpip{bf9D1Jnb3@#nrmn)qPsrw00uG{$>klW$Yp^bO#>E z{{2?$LR0M9rGsh^cl9v_jXGkIf6C1 zFolEehy`5qi>*Pb1_n{hZKiPbdfxN?I3I=M$HZHD3)ObNn>2|Y86Bl1D+O&N{nvl- zvo+r_E$Pa`2P&i&DUjDI1Gy&K(qiKDu=HKy6B4L7)DFTUNkc<(_^{br|D_3&LU>Zp zJ7qpXv}XjBA}vWD*~)zaH2``9@puAZ4z?LpoC}A86~KhdBlt%!SU%i!mgsaMUfeH0 zf2}SruMdiDeh@j_%5uz}pUYvw?4N%n3_ZpZP_%{k`x6~py_~dGDtu@T7*0t^@%G-S zul^*)S;<{3%`Lj!|h$B-M=7bi>(VXTOU5gFDTwzs!eDErTI6Zvs= z6ZRav<^6lrDYurOq2tUzY-8OKvA_4i-ISOpeKa@)R*0{(2SMe zGSO}Ek7bPG(9`GHs^yw8$&rzL$PDP)J!5<9-#gW*>3+`6&2>UZgIxr`8)vvk-eT`d zijGla1$Y<-JrFANpHn1_5&Y%jtDOLjMlhsiYE;6e2|;8A2#iiwGr&l1eEwOGJ`FgUFDi36+W@^}Ih>-`}(M@3r@S zJ^PR6wfFspW##T3KA-D4uk$?4<2X*-$KdpQ#Zr*9S^V~sM|J*Rttv6MZcTBWF!2Y+ z(amDyXtjj672zh;;p+~JDtP?9lbgD_nwo$mk;S=e+64Cv+=qyBJtE>o7!Z95id;RUdtw!+St5j{Y@LXDEk9~*45cho=8YaK7IUHgZ$J$fvol&7aPaT(@HBgR_DFS zf_4ESOgDPrFz>TV#`P!E(4;><&!R{};~k$CI3CpNuYpH|Hm6_L!pmUF(p~!w9B5aq zBjxZwZn%a^*!An{3$87lzV$Pb^K*d#cGH$bO1!N!_+`}ju8vdM@2%_CuXl56e&^YK z0g1wz$NxYws`Z`v_F~~rum(_p$7S_xELRj)WD-EyLZs`K^QHLaK{>Jgx+^c&8SUJu zIa3O%sQY{$VPAEfDbJZi2(3 za{_(sJmVbAx!1a-PuHqXkN@khD!OTR zj#@;W@{Ao60q*BNZ#LB z@0JXW7&ooko-Y#ht|lj!+`G5;VOhMV{xFl89!7Ffc!t!t7d)AJZB6=+3q7^xi)6-W zYmbo651HG?Avh>#gl;7(rx{MX69Tqoh}WZ)Uf#TMgC?7W(0*I8o1N=AyG63+pqQ68!L={?{=iXO!FcajEwy}{5%AHy`Ij0at!OQhqZEY_T zm@Fz~FJxxw`Q2|Azgk%a$ErkY^@bA;%j!hQd!dDyPcX%}Ena!lsD0@+h#-5kZ-?IN zWr(sW+_f%9@b1|$M_lBVwbaB!Sw^CD`jH2;;D0q0t-#8orsG2_U5c+wN+NOmR5pxH zB7!?d7Rf0x;Y;<^L^3?CSi;Raci~9|2_YbUQ?+eRQr(`tdX?3?Sx(x9wp@0LCYOvh zcpRA+^{sC3CTcc%CZkqh+c(0a5z@*C|LpkNw~=&q>K zKH$NTb>vglmE!fcI&9RM*$i0`Uz)mZg|p=R^Z@MUKL`}dhazQ$M@xVC(WmBzzp#v4 zkv6>J^gW&Y^V2yuBTi?*Fyz}enp!v@`W!u)ioQSP6pAP))!FIko!Xa~nqIlOcGGg! z!Eih+uroNALB^Vk4mHbhor~+R_&L?vn+sj$_?3k@x<~{&cJ-a6<1Rp{=ck>>t$@ry z-g^1UmFh!=o<%c|hduu1FU{e(>FJnHULgxqQgVVn$xmQ|pkwATf1KRZXOf3h@1dhI zT<@HCE}LAqc#nL};MmQMIpdDkDo*mBzUlp4%v8{}wI1V8$+1`_YDD4)T$KR38(T#~ z3kwS)qZsOHB;z|9pUs6-c}Mc;di75;xUI9qsBPo54k)~90$3|a+nlu&l zWVIoRA#tHiy>4fv3~JeEoxRmBK>dE>jH$ko%hGnu*mWq`;0GkLdt+#>UDCVcDb>5` zl4(?I;Dx1KmOCw(GpF0e6AxDXt)XF^w(HG<2cZeQo*flkF*PE{IZ!iV<|a%EUo;-~x|4F-}FrNWqWlU2pDORT&?^kTP*D(@w2++T7~u z>d?Bhw1&&OV8#*K6G{+$jKQ&w(FH?f8$H_a9Gi^O^PlYURWUL@vcPfH>DzRj`9526 ztihSxMU#MOGD-!i!gZ+u10L8;#wI0frNrRLAEDng|G)}@ICW(9CDu3hl8k+vnp*y= zR~lGmg`CRE%Y*K;)Xc1%#t>pf{IFC`8YlWr!S}w>I1Hn=Ds<8RtJ~km8ZVR zN22Pm!b79gE@8M^-LagMV%aVxyJojw<~dNTD|x-Y(u$~S*W7_lY0~)n4?34q=$&^w z>Iepb)JoV$Y8_U?k^-+r$P{KvmjY?Xd7YlHOtlsTK#MOF=7GWw9Jxz$t87b8lYSr zFwxrbN#bn4*6RE!3~Pc@HNcO(@;Kb|CQ@VTi3xqe3G1Bw-}5?^K?wn2+3UXbc6u~0 z_uad9IXMd#&+~hC>%h)x6|x|Tu_}wrAv;$)G(BH`-Qt~acD0;r@@$H8o0?T8f2B5I z-4`i$MUn%`uKbed$(WO}kyvUkj)bN*_FW&CH4*k_Sf(JkoP--S_XD_v2j8vh5~jTlE<_?XHb7 zz)bi;;D`W=vE#>!JLLyR_%yG5{SSr{h3RwGX63v<8!e>^=T%OTXyNYeURYFAroA>N zV87I*T@U*Y8g#SxGcL8UU2nq`xgMj@v2Hd)-^w>z`{Ydes+h2c(Oul;j#t&e?^CeFy1QlEc^@ zg4YCDD@(igU`wm%J>1>*jmJ14x5DvDkA@N9cHN?zjO$oZFU~;{jEIOBwIHD6PTz^L z-CBNKqv*;mM?ebTg-m%v)3OK?{?~3YqQKWoy7NFmLfUQMGc@eYE;JY_Q^v(MtuT!5(u)H zncd}W$yD~1oKnLkLLu!VRwZ6i7B}q{-aPHor~kVCiHV6Lz1V(Yd+$yy#)Q|Q+s=R= zXs^bIt!iuiDHhB!FG=-C3J%`$)~ht32Euaq!TPu8nR&X|+BXu9YU@2V^ z^sx&%cj=N~xpCRD!9SHixXkNM z9sY^3pKRg#EFH03{g7HTvzOYBwDnaRJ^oU7r{`UA^-Y#8oip(>G#u9j8{CTKjh2q> z{MPt-RFuL3I27A80|RX;_eD5X=y*CE7i3vB$_QE(zVn~le!{`DE>WwS(4MnDx>L7% z;m1s0`)A|qJyx(C!SnMbbpI4ZH8Sy%l}QthaB`2=^mU7FB=7|n&Re{At{BT@@bHs` zCGl&V+*$@kXO$EjcY3X$-E>Cp>$TvfcV}p~&hAeRc;8i{By)bzVVQ`DzQSfb*INC| z=KaQ#rL(TbRM<5?s@ABz*0-mtRPWE121duW9b24jF}C!}Qj(C@BR7u`>-?1paO8$Hzb@e7LbL6so{_LyUGFIaLpbL&34mWl+UR{G*X6YQ5V`mNQ@E=i8`72d*`GkR^ zydY1z0??2U@{s}K!E))UMkT1odC~ZfvNW^rYyKZDK!Wa~N!KFQ{f=JJWt36H9TEUt zF{9dyW%PU&F3e%M_SL0YgWo*bsdIWH8Ld{(;uObns1UkhrHZlo&2rsNwFM%JCuuQa zUwi4wTTj^K<%AW(%-?H1N9B!(%&fFwyKUXH$vobn0HxSKCF}JzHZBu~%gunhxpe6% z>Y~gULCql)mOE9(){^m?i8oDpDQb6 z1`L>UdYgC0T}9nsbdFANHFB&7qq(`63)}CxMutcISgJUpk7)NK?ca_4bjN9Gp11H0 zdaSQ?Y4GCdmWGBpk^9z`$kiU&Fu2RRy2y47)wi8|<>YCaJyWTOGfy=i{pS`c`&AM5MVd@drE6QHPcDm$fZ;whr zkKl2xX~luvB?ZZX3*e9`pGhK}a&%v=pYr^>&6sPHxcRM9N9u+pC!d!SbLjcj6f4M> z;`LfVh%Un$^5ncmOZSAeT0B_(zKt?23@{mR8KPIPSZX$OS8%aMk9|B9#=KC%-SO6~ z(YK%=7wBKqJn$uv$V;>RJpsn7kWTaQv>XrIMgCi6h=SHY#yd5jyIe_15k; zM)oZ(LH$tK^Kzi?@?NG=Rl8QizrQFiIuI5WRoEo`Q{ny!J+6Cb>f!eL%QhYv+gupDIMG-yZ2uZ3 zZW)x?<5YT1D4+!JL*sUFfyOJb3sp_l(M{5UCN?!<21AZ4ID19Gv;$U1!26 z@BVwsfm_S4ZvW}M#Ve1L46hk7t@=ZjI}^T9B|j2}L`yU%-88LPeqiT4qQ%aSR>kP~ z%I+V#DWtH%IpO14K~D*&`9e06q6FnoSde&0TxQ5!0%85B@e8}#$t_rrO`)F#l1R-C z8Ta`##R0>Hy~)eFsA61#uW4yjRd|K`h%(;MNcC9%Lr|iRnBQVM_BtX2EONB(qKGyeC!m$rlA?S!){ZvAexxNq-|YAsx0 zDq)a*_m>rRftfbSf~bZ{KXeIVTzquJy`aSzE?n3<>L%hEuRIW57}(V32+MHRKWd?o zs~nWur&ljk+rX8WiZ3dH8mwixC$|Dk&f7ErVS%~~jY>v74jDr1A%+rY1+{a9ladw2OOBezGyzVN?Tju5+4^w8#&87d&NP@p3-qP8i zI??NewJVA{*_t7;vh!1(Jvg3T;5y65FJ(_cLW0rN`nwvBh1)!1$Z@v5WkzUK;?9Gn-bzcu5`^67x}RB@mnW%*^r$crS`n#@%(T)9an^kGe*UASBB z3>ECrborip?@)ro$2UP~hLwmE*#EY>y}R1mxUK=17tLt-B|e%oa>D|N%6 zD_TuwO8HvO6*Dsy+>CMXSZTf^Pd^zLeLJ@K(I#pon?} zc|WLVAkAImIdBd(C-MyvNeKxvaMYeW`Q*d0&C||Pg@DyLT;R+@h=U{4z$5$ymo|Kh zE`(Gk>MH7%ySd#^eAUsmyJ$!4!%H# zuC%xnOO8j1m2bCKTo~vwtm*i8-AdBacFvseF;rR`oHteNds*^UPV8Pz&O|hxAvZ#N zOm6NiO6vrlRHIVGgL4Ap!4v=k5|DVUo!)pi?;f10WKJc$^C#+k z?J4IrtXWe)M5LW(v^FOl0!=hQ=QWwet5*VDzih{OWgCAT$FK8c>JAj8J;?COG1Okx zC>^`l#pCsos-O9*!FN0!_}*#IX_8+5FCaJTyS3WZ1|g=G$m?}K-+++})hb>PLnX&f zo7PM<`SHOq>UcHVKjpTw--r>jzI(guDQOx|5U~BgYsue+Ve=-eNG+lIJ>DZ|_xGZY zmB4YQ({19`>g$Y_?oH#S<9lcCEvg3l&cC|bFuo!)l9JCD+gQqof}$ehph^4A zw(G|$xIM=$hNXVla2(FvXx<X_+_w)7G3DQ9~Aq?Dx>3 zxJ2K2{gmnB$6tFRTX1vm{T~-ncXHf6SQUe3cuQLQ?`W8&-4-Zk>936JGd#nu0G~-V z0+~=!qxgoD%}LJXj~Q`N_fJ>EpR_{Q?A_axP-rTvCfk|&7bchbhGs2J*Z%vVDb9ynYHG^m2Bg2zclhwHczV`sH+x@z2W;v4_s3CI3?II+ zn#@)w=+;~7boGpX@qE#fVOXU7`}qIG-(l-?5oyc|T{6kv@lOxx!2Hl}_4O*Q{qI`( z44HZ~%gb0&wA&0@U^@rkLQwC?uKoE22H~vME0Ueg1Y&;uSz090I1Vc^{@KaFU1!EU zyYz2kMkET^edIr#IMJEu|8dBn*Y*F|-xnBTy>CD9tN{Y4a_8;PTV~CgwRGtKUHxD~ z30bvA479S=#zZzTF~C~O?uiUzm)w8iHB_$zJ_Hoiyp7>XQetNernUVs`FYi4pPZQS zLy@T29~Zu;ix+jqpE6y#^soNnR7C#SIJQD{0GFjlpOS(xLuEOKA}zHIX4+`pLEjs{ zLH<3jUG{Nc`^Sc#v{qc4oi7!oxz%5q9|}KaO^i8j%bV*4AI~WDo;~uB@1M)ty~3)K z{aCzhfMvQ}Y(6RP)=JpUi`4$kl-(>ondgQvgjK6pcVu%cbkSK`Gk3O}0?-&S&Zjn2A6@ny^ z+)DKd##%08_#(xG@R%oI6Hp>e;^ZE zSM9Ci2#vwa)RZ!XCBY?DqQ!w4Fr+X$@b96tP!w)jJ{J!0z^M?P3IkOCGyrG3-(@DU z5ZY8!irm9_X)ABRxz@ncSou0L84_>`-@9LI$ z{MLbf-$ZfUQ(c3s7cJ^))7HebEPctClgiG*O}Ir)31LU@x1jk{o80gel;ur?nUZmj zZW;%0h@*$?_t(a9?SJO4L5h~Li~632-f4!XPzwkIy0-c*WtTopX|{7}O=9K1CKE2X zfiMW`dUQ1UKrwkB@~xlE-97f};!Uubi9<%z)qtdnqE}d3x25+OaePDRJj`m*zr&-s zRHXFRUuU&E67`hD+^PL*Rn^bd zpIh92!@0J<(Av5ta`E>*fx#Ch6cq$vCXArAd7I?HS#Mv~e*EadM{xhms-um4+;g&w z#2%g9QmC5gA6_nszIN>u6j)vFhh-tk ztN+P|vfBm<%L&$ctMxn+L^cG7{!*7El(^Q^rKJU z(+d_+5!x&k{M74fo73MnZ|l*cM`V-wA}7$<*6(>OaZ{3?K-~-W9N`@v9*)NRSaiH@XRG|HCxUp;a&7N_oqj+`|#kaaL~ z!oV^O>fuRVo$gAcZ#NSKloCtG>xDC`a$oMk(hbvp`lZ{v|E<}Z2Es?*96RG5$0)4u zaxjR7{?7=8`4jPS9e?U@ZUs!pBi3nm?wr}RR2OVWfAn|;a}^;<&u_6A4JS0R2~IYo z1Bqy*fk&{*)@#dJ6;o&V0#+ZKp2imZkwC9UwU6F%Qz3VHu;;dLU*F!_cby~(oTzwM z_f>tc%=Xk1^GDqto4c=5USIMlzlWBOR?l!dGW+*=q0nR0{< z5%INm+r0ex(5_u)Vug(jLSBuwM+@FqQTfu$%d)%4F+*E^LG&@Dd-s0qCNuozrr8=^ zOHm+r^J!W3lhmof4_Qa`GshY=qDAzDE*D3^Psx7tsF|7t7ZnifnWt+W$A={hZmBqt zW}xsQCr29^(#alo>M>!5XPoSD zsLpkS=5Qx;tBoxnfP|^cPyFK1=Zqx1?>N# zCfhcar&Cj~_?)K={TBXd(?WH(Q~&k3>D&`UuSj+ktrqXo=SO{g`k-GNi}t<#m_mZ& z(MiA$y*d07&qJ2P@@f<`%Y*3zcqB5`!JV4Q+nO>$TT9DYPHNM%p&a$FbAt`-5o3J# zAT2BVN+F`FPSQTy|2l%a{c?;ZEuyDB3uU)758ts<&uo&vV*kVJTr2<6>{+0z&&8^t zOO{z#`L0dFQ(2G&eJu$t%6;<0=g^__K5jQd@B?=*^POsO>!>JBT{w*bEF$eDpY0&~ zh(yuChoA?B0;4LLA`Q!i!S36wtS%I`te>)j?!vSOh>UQ5L32c%O3&J2OA+SJ^j9(M z;?Zlr?k`VTX-jJ?Ek$wNgg3ZHdlU&0hti|L?H%7lqibZpCV4OR62QA{vol;)66mEI zdmMf|3=MaC_7oR?9*{&e8^jTC^;4p5a2_)ZUca8sII;bTU97j?P?FrO^@kutzJ2_T z!035@s=TTy!7fKOqlQK+o#YT4Jvo-hy)))HVUz$QDc!GMRTHLra2gm`HBS1_P!5t| z!;phxd{dvMDytA=(Wl+98Ziacor6I8c`o| zYaXGVy@6aFp)7=z!ZT)^#=>|4vPd#53kxg&TQ#jE58ea{pHeBx!{xr4&Q*+sQp3XR z*?YnQ=5g1SbNKR-b&$c!a2c`dUx$w!L`EOgN7V} z*uGs?a@6}RX|;9oI$k)|7p4$=CmIu_J2t{jSh+ITKq2D#bxjxAPpM4{3IjQ$MT;>tPj@JYX)kIQofy38)!oa2|3Aa>5x4+UG z*bVPbon$H*D+D)1&Gt-! zVz7O?>z7vrZ{7@xvfVgp)7t@}FHGJ9n0Eg5ao)d_HXCN_{%f6_qK#f)N=jTKb()0-B?%duiNFasr8db#fsQhCiDp1Gn8~ayG5IOy zzLb@zLKFbcz1`%V!aSvJI@3d!{G{=WqwlNyJ;FK=RjSe!qP{J?9v>6acaX)hWhwAb zfdv{{&`90Bd2k=S&|-60_qG&>_Z1^7qOa(Q!7Z^&aaWg?EGg? zF9DZ&z_-&*rj(OAE@Y-MEe>ka)=Qlj!BK5 zH0dc% zRq2iaYvx?3**KGew6X6{ojt8+@bK`U9-8XW;?ijpmNm7_wjYTpMee*Ki?HR_?D{?k3%l2OTl`7uC{iFK85PQQNTn|nqcsU|fT(xaEp zvFvgjP z*tq;a2X!Q=UtTp~c?TVku*%qHq3rziLwKeTU^WM?#_bvUU=lV{s4HM$V6LGKI|E+h zf&Kea)VA0UmJF;VU6N_uS>)s6qvg>8j~s@dlcc52h$E zFOVfR*DQysjpA|?>wyO=n45zT(n-$M;Bo@B4u+$^TE@aLRVegxNppLt6;Z==+YcZBr@Ervt+6`n?s zP4R&~-7fl)5@R~G8shR$$+hUngei0S`udwTDSIrAO-^p*HAq-Ew?_xxiud0VDY6&J zNXjb4{@il@+c}|?e1ZHRkqYfubnu(lHN9ZzDa!zG{5SR-Z-1$90%mTBF6%JI;Yj^s zHH4F)Uh1_a;aV+~qsT7-*PNV$^MIB8;o!M41#)etH;MaSKX+~DUPaG`Ud-qb+ZIj9 zvAfAoIyMX45KFw9ZEbL@UE4YeP8z*bI;ZLBD}`EC(Cn@;d_SvcjynZ~;wabTbA59EK2lQ(dZ-M5+q}Tf|-*$#^v2*Rug9 zDr=sawCu7JK>n<}iOiV%Zw>iMEqTq{9{pJ3v7JUP_F)&##Y|Zm5k`-YZ8>YqwN@ z^2*NENlX)xJgABaCKl6|{(f2PmEZ#^SX1;HyXgHpPE$r;O^`kW*@t9JAaUj888RV| z-k-0j?yOm?A5$x<--6k&xjC|r>e{mm-1f@jav|NJPlK0Ly6t3!Nrj)Ntkx)51uC;= zwXMEz=Mp)?1S`HQ2azbxh>HO6=&WWFnvE$to9i&1VzHgkP!stuiRr!2eV8%$IgLD) zuLGSY#%j_NL}?KJJHt_!-gof@Cmb`uxLI^}nG6Uh7;r90x+~*E1oksglh3%brRu6h zO=Xr9$>e_B;KB|4dgf-5fGj#q$4Vssv9|VD`}r9=rs!4@oo%~?E~!ExR=SDy>r5%S zB=mVgC%q^ty1gc$hE9TvgdRpM@sur#jcXWIP+Ah7b=TWd`W6=^xKFQv<1!?BA0FBw{s1Uqw$feDtoI1dp+e!=J3aX+aqT@Swa zciL`bIA4Yi7hP%MhGW`~yVj<=yE};qPWcW)Kh)k4(x(wI=9(B|P{Mf~$MCP0vzc^i zFKsA_Z6^au3Z$BoBlLnfK6P;s@Rk$Xx%{Z*q^qVK>`D(v?pD75**ZQ2+&T7cv276ls$(UP7r z%yg2K=JnT3omxt7F??2+yPuf~vmb_X?YnlSi%-WU0q2_qTe|l6Dty-y`Yu@8#h9ND z2yllpTR6diKgEKlxCdTk%=kFwW$KDC@)@PkA7W5_oQvj+3+5B_#4uH?;UHv5A@(55 z{@D?hz3x%jp&u1r6oWf$KwZZPxmJOn528= zWn#2%bT4$p%`*yZz10Xujo`wexUIo_yHSh50 zTMfpa3ecD|ihL;CnE3O89Y!tOUoGpr-xG#m^ssZ`6K$jfZIotiJ~D^Qr$ z(L?s=96xblk5I&*IlFiB)smVm2_=b%8m&UI^W)3wtLi?`+nSr38(}w1NMA@TVI0w| zd-o4VtjJly7_cCw zkn^IVf(=*Mv|RiD%o%yQGU56Nu(flyyoq^S;|W%&691sOD}ul5{C+bAC5sgGlBAgJ|Hv0dChl>E%LQHy}N0!GQQ?Lm%s{OMG5G--iO0M zH!kgIG3dWn4$ig2*|jgRKVc*CL|RQ5rObUp_PnG9R|}d*SgPDKpP&_l2jx!|)$!=J z{}(KZ@ag@q$H5JT5(}CdN#t``Jxo2Gf1f?j;RO{Vc1P%;Cy%~CEH>UjdaHgvE&2%i zenPzb&gj2fCC5tda=CWwSRce0&~&`L~i<`Yint5IVL zt?m8C{bXJwEh-zH&)GI4LXiz96y2h@x2#u6hwuCjH5;(*`$)f(6k*n?u)K7R(88Dg09jfUV4*?|0;ri2kWAL_l!jkgn&-@!on}f=>%ZYp zIhdj1YS?mjl7i^&v`LnClxd9WTY>|P8RefXC_^wdYE@NG=&n6=c?Msk?qh{$_fcIr zDb7iJLp-^5Y~2~~_9%7(CqB?jw_GR^l|6d$M3X_s;UScDHbW63H!v;}9Hn_@8+Nax zB<_r$B&GdUe@#|ybGM75=-Qxr6Esw4ji{p`Afl2t81~O1Vg2kuZ^mg=lIEF)nL@ga z;AD5@C_7kZ?vfQNjs=D5J+x6KQKV+}KXaxTs5Zfp6ef*YP)<`b`NF=gK(Zu{^m5)n z(TX(vQB%ab=jQia*Td{br@TFcG#-Djkzsm9M$f2{nWgcqxqfx4s-~u8P01dln>@aZ zkQd!GVg2L&{T%`frQ2DJfnS7s&BN^{V*{o##W&mpCn6+wRA+Pb^e7w|Zz<$vyD@{f zA){*N6CRS+^AGB=+kYlNCSR5mj6~CL*|K?QD$u&o>?!yWwRyN;F6(!{r>Jrqd8HbL zjmC59+X2jlx6CtwwIK*d$>W8w0PgOI^Fu#0{G>_Ib!L8HA&x7_-2W>_`-<}FP0Y-c zEaJu~D%KoPO2ucuhwgRwC|$9CKZzt1^k&A%GhG!3ongNhv^{s(*w}z*oO#lI&PR3R z$j((Oty{@7`&vsWl?`rdSGMe$~2t zdn*2zz?=!m&(9}JAD(?l@b>F?eJh@IJ0=E8yzqz!)eWPCypPA36ZD_iV?U}3ur?fx zn?0s?5vGxoT@B$s3_6(c_s-@Uy20jX{ML>7&j*?yTq(;tc{Hs(+kdT~%4I=dpLdMb z;@vOTMWPq(Y(Pp7rabK0D`y~s>ACP`BD+1=<)6yS1+o}@5n#~YElsMfv#Gi~C5LO0 zFhQd-km>SY&rY|&s%l;RhX?4Agl{3z6{BB-6Wb{{Z|qqigHhqX_Da92`CnXxflBDTQHCzPRR_=HpZjz znvH-MijlRjvk|ANy2=f^OOLSUUa?-i z9KUMMA1xhOE<}HPW+TcRhX4B!Ear?qB@FCdk+y5%f(ipcn0Y^qR2)Q|MD)ElMzt7@ z<6H8U%Ma=RCZ2}04~$C5{07XJfGKWu)gait>6VNeEv6eu!`Z>lD}Q5L)Q+TQ8fGs9 zyDMqwSK7i&dqkn;x+ zH7PMMwLWtQzGZA*n$lVfHUj2YILx8}^H(LtC|WF`bVryx$^6Fd3F&4M@aByo7$7I- zh1c`zF$SXDSrqq1?evAfQ1*uJ{Woe{+zugz4}=^@|>A~hElrl)HUW6u-P_;bYMuu*AKA8ICNhzn1YHc z*l-#sD9gUoA4{CYlcxMP6bjy;`lgumQ(nJ$1Ag2pkTm1tBc1Z=hUXO=l#P5pR+s+D zdu{4c+%_n}z=|H0JEPjBJohA&(Q9f=YdI*hICZI&l?Ohn$vUtOd`V-p=T3@M*H6D+2{@6|rFCi^nWv`uF#es)k{(Oc=_tW5+L&yBdpTXrs@xx2T_h zmqEAh+_~4~+5QpNuF-Mq55W&hM7x_=8k`wo!M%q};j9IeVI1(jEZYw~&+4P_rf_zj zk6`J)a2>B9;&?mVy0vJ*f{X!RlvKcr7D>8-OGz-O`cCFyb8~a(X8J{kse-_EQ7D3L zoHaOo_Uzzc!*Xntp&~7A%l7KoPfC}uf%*#;FuxQ@ACM-XLt^4l%j-0QR;^-8`k*5Z zK=WsbdHgK*&vZ}&Aad+OV7PZzYxMG-y?du%)9UKlgy@eM07lFbBx+VPD6L4fG&V*a z2Z+>H7r}2v`^MDv3sCBu zAe&IbY71{yU!5sIbI&j9(Mg3ZMT~Bh_X;~?zj?FDxg1Gx@q3FiP43e4--;-kD41!~SSH5`8m))lob>=Ehn6{8gtPP*0|%7Lk_+ap(@J&mzXcj`Qz~Q)kmozXOaRgw>MpP z^Se;^!PUt*tFez6c-Fa`jiz7S$>+ROi$yD#%=Y%zvm1odN00RV z^0D)k^{|H!phl>3|NZHEE=Rj3lkwbt#;(c6O>?%OAYcml*mi~y?bI4kHC(XKMn`|< z!K!td;?B=6&tt$SX_?9ILRTH!w*O|&DTKTjUgb9>3 z$}oOkab8ng^_Ee-NU-`cEU`+!vuK6N22BG~H}X=&Z3 z70iUDo!-l$75@+^-TaAXm<7k2Jdlp8lMv6>X?7*MZ0F^Kaaasv*dye+fmyao{L4On zE}?oQybSE$A2tgvblkKG)}_$V-nXxdu044dvsSGAf=qn}JTzYJ3k7__%A3U$FEn_6 zL#&|Uo_D$r%H>ut|1R=`70%zQ{9{wqi-SAYu^Tvh?jH%&av3Dwh#|jhgpn76h|O(m zF6#B6V{&3p_>|&f|>uOy=HI(6yJ(`@bRPDabF@%AkkFtO5ehT>b> ztMP7c&RI_Wol(wCsHr)3>Qw037}-tTYd(Jb=fsJXlJ6|ZgX)%SXLu~)@u$(;XNyQS zM^r>ahS{OK8mHpeax^PC|4fLXV-g&^QXn|#`CKgWgwY~QPjUZ=sA$BnVGEU30ORp@ zC%50!?`K<*cL0eBFrFc8fWi{JktaACpKVdqeK=}h2fExR#nSPEuhh36KZ@Lrj*y(^ zxOSu$k|w4staVQ)ydUmS%|tr)jY`so;tHKMwpUiKwc2)1fAmR;mJ^;IrjFIcMt{B| zjlEhjlM6eyoTO_Q|0#+LErF1eQi7VvLj^5nsgpWA%XTdo zdwXo-*6?56mr1nCdVR0_0u*SlSd;ViPFJUBm@Gqv4__3mO}L8eceqma9g=O}FI+yj zQORmkqo*ujng}Nj20413haQ&kIl;(*(Gp>c%b~HTB+Iwvc)x3q54s^V)7bDE`frs&V50%4*o8^z*Yu&GWCksX* zi=RHV-;p3CS!wCXq|nX_Ube_z>0bQw_u|l)4+mTIZVxE3&m;;t;WUG?e1hrLrvrcKQ_Xj6qhj27u{_kALCog4S&bGiWLZKcvH!4)z+K#d^f+`4t;^p*t6 z$R=v+&6-5jcb@yBCp2MlX3_4qSZi6FeP4kZeYixN8IM+F>U_D#@@OcKTZI84z&L7` zC#UMosxi{fK()E^P^@n7#nfn+1z`mkuC_y$WhAx-^aU&;Rw>JH9SYzjyQIX&O&KjE zMwH#KVdC#ieb&(v#85MxYR&Arc?_^J$LTuTX9v{oS2V1d3|>7zJvUv|%4o6ikTo%S zPA>E#34aPZY}h0pJNN!P%ZjdzLB|7}0wMG5^~KWQ*t`-z)5weiFOUD+WAMDar9CEV zkz;YKD5Malj19bA#602h)NfJ^M;qmi&~EV)w; z+rpxTM*oNYMj5o{;|kxQ2BlyStec4G+i;|!aw|nR&q$a+qFa3G?2+kLtuefNG{l5r zBH1BeM_L3{qV;YB-{3T_=#WVhCR{d!Fec1_zp4A&J*@_+(0-{Fh>(w@cjgY?zQB6c ztS)|8fWBASis-vC7`9)bpMby(N7R%M<2W@FyI_m(ZCOlt+79*L$ z!_2}G6#>OgYWix z+7ib#+ijZjs@5c;fCBY5Bk5e0>d%|Eohqb##Eu`sit?MK;-1FGFr|O>3 zKYl#?-L%WePpb3Cp3U3aOBK)m`uMgYaENm5u!_8f#apYMuOai}402Gx+qAdL+B28J zs32Jd;1CTzTj~6_f_`#jRy6!H&;e;*zaCSs2y(ZOkTP}^L<{})9$#u}b9agROU2S1 z$r*TMF*3`Vs;YGqJa8K5a7X@DC1uoo1cGh-`e;}|RKRp=bMtAm3fU8M=uhbPXSHCf zp_J%Sb<%F*=hOJfytHqTnj0xUZQs5`Oj_ ztWGpWMAniw!qMdwUyQzD2QWg>ih;ewhEDtl_Rk zTyekD2ZmkJW)WO5yu9+K44fmtyCx?66u`3Usc7#CjB; zC8UQ`XVL1_t17#StVys6AXOFj)4t&=tB&2QG4r$jXj_q9K=Wz`eIiRbH%(Azox#Tc zV8+UKc_zp_h79?I`V-uQrX;M}SFc{V)A?F>wt#TUSOz0&Yb?*9f9m#e4zVLm9g3#6 zG%V*;H_^iG@rr6@%rrB{`A$b?xA_Fgo;?p8Ir2ibkaLi9hw*naH%L@zQkqxRTCM0n{T*VAVkiwJq`ZO$47uiqpX>bw?+_v9NFW7Lx zve?m@nma*X1n8BXvWp&{iOc&uc*F9N-y)DVFo2L=zb$H1~^u#3!yizf$;p z8n0W!tkPd12%2^E&kwru3qBUKw+Pn+-`*Fv+dO2X9n6%i;UJQml8Pha%1h|@hu!ik zuPZ29Q_nFzHys#Q;y=G36P2|0#HDe9cO%!>{#|NH@RklRP1GgB)l7K_fHMizbb?oK zzdt|E==eG1#%JyT44MD>`K&MEnMjZ(FrSY4?!clBy0XZp^Vh-MSDP3a)stjOkXGg$ z;v=c82Hj9mN#8FnvX?hZ+N*-;DVc{~$Mvv}f*VENB5|RcNX?0(eTs{C``%ZA8{);# z7%bQ{x5o-kv^Tip`{zwWKEoW$?Dvd2OzW^M*R!pxy!=7AGy4zL90OOz9=>$u476!1 zu0aflX~T~du|5#CT@4K#to%g}mfro+O?0blDrf5)QP9$u_$x$`w~Lm{kIL(eV>it* zdPVK5Hd^3#Q4!NXiqC6%YV}hEJQ%EM2N?1h=n^Uu>ygFP^y3E;&b>RSI!$0d-vq`K z6H|5>sdo*~kh4kn`i~0FHCM)cKBmDtC2JTa>1GpEw!XjH33vc}iv}Lw9bJP5N2hbb z`9o)!fCy{l{Q2`ZwPAxuFNk6B_z6gy*}B0?cV)`_3sl)4`~hc6_iX)QHP?ul!ceoi z&kL_Qwn()lOc0CZcVDp~xLY*=2TOva2asE_QBqf5s^GUd!BY1T+cy6#T#$!l^~1Gx z>KpYxn6WO=Wyo@O_ZGrG3E#@f**?I71%#5`u~k>JH6}Lyxm5ivw{=?`2eS$+Vaf+_ zh}1Bzs5}&;4>=qWCi-5xR!#eUOZya0Tx}=$59!;N*V5^=dYZ<>PyfC|ew2+5$ReD? zGQxaX^EKMX46}hATmA849GTs)6F}gjy(Uo9x1U0p5s-nk#d**N8XTBU=9B*BR`lr6 zgCqFnD5xSFLD={#Cr1Gn#Ft8_$(JFu6tK&f)pNCiu;d)VAZL6i@$_8}iD%^Bf1iD8 zdf`Wa(7k2g%DDd5HUwms--3Eb}GZeS}ujo z9$9*0C#(T4sr|a?aXv%;K54yl=FD`#;`YrO{9!m201n`+soy?%;zWJnawkBVXWFO( zsw)S(FQ+KwEM%bJhk7@%9rCTUU%N&{*1TK}2d!mjh(^+-r)B09&(_uHxo(R#wa~8a zo+WI7i^Io>%8Uu)z(1^PZu{09_V$&;!NwK2tksf3T3JasCpP!$CQy~r8U zroBX-!(GvMTT@Yi*CTD-3nx!5v9{iY$so%RCO7nnyiSy7G`_b@@ z%bNqgN^2Q*FItC;`U^u!bss^Vt4x`W581JYHN`i__EX;UUWea*+E z4&&Q>NjyNfId~{qhV}1nyKddtlP53TItAhjGY02KOC}BF=f@$k-Ij|Upr5Ykb#o(n z!G?ysO?O_Fx;aKQJhG;8A{yMP?LLiFhc%9c6oxeH9a3<0M{(S){N5*fF2^C&d_q*m zOkV3Xgkk+RsUENCK)!g<02IKXI?vTreQEovQ0oqo$7yS z9J)NxuNy^G7@pmD<;rTtTtHFrNJ#6a~4z{>sY9iG<_bR!mwY3nl_x~Qpes#+;eGbPl4?0bsWei=JvhodyLlRhS6Gb5|jVDrMG)w z6jp1lb`iPH!fW-Ks;mQ!9&_)v_EOJy#!-m!d*&wXt%s#i67OgR%ZoYoG=y9CIucDr zdJTo$^#)Tp@TPL6ia8cdGB$qEJ=$`@z=(Lb7*e{l46#Tx86S^VN^I<^9@8{5q;*lx z^pq#1V`Rz?A~dv3SW~d5+@RZczl?@>JM&-@Gv&PUZ*_c2m6xk6p-M{WzU=ozU?z~6 zqaNuZFF3RdqG9G>?NCI7G+H`l#L<*pznSm_CGB%0_KfkPU4QtPjJHa3VKDnKWP~Xe z?&9z8>Q8Bjh)L4jcFTSE1@{dm?OzM(jhsFh@P$RPBw{jX*XA%?hZTfdXmB>K?fA_0{Ek5|gL- zzz`CyKWa#lvfy#!2^ce;#|D5 zeZ%+Ov5aC=v#RRqjq-NA=>MhIy_wN(z<_uJ1Z2ztoe42|Y$ubiS-mic9MTl%b&sFHL zhkfziJLB4oz%mIsO0bGOG6zqZIB`w)%>s>mn4I(>*k+KEY$q?Xwtfq)FD8!|zT3@1 zkXT_!5bv28%6JqADrTy^>F87BQOa!{m9h>|q`(tCeVQKqdN3Kz4Ze8GXuWlsgXSCM z)8U)lHeB8pM?%tf)3l~G zpw5|o<;IOo*hp~-a_=FY@G>eYCzb^S1kiGL9d10<-8ygTTesEp*BTezi2HA)X5_lj zW5%5D%zbnzLAW{)07)lT1l?zq-2ZwS7O8Q2s*(mSKPmDp%Z$IvERt~lYRD)N4oMo1 z6hq48p|4Ey7ll=QJ2Xb$Okg3#fKGFZ*h?= zY1c^ffBq}vZP&AvyE{_o>ifZMzXx=^dj>kmcDyRLzP=ia3N-%4jXG+;0}BO_3kle2 zDz^rmQ8Imo4ts-pT{7H$+)8GR6?Ro-uiT{0aZ+F#*bFJpA64cRBBYjed+X+iU))d8 zuIwUis5lgde9`VfUNupo-nZSG93&6=5$s{zm2Sc!tKY1(9l3kn726esuKg+*ApU`Jm;2%njNc6w{ z1=iY8p$m_;Ig|X^x`p}q&~B0ME?>56U1lrwEtp;8wQIQG;lgJ!|MtQC`x$@YmMGvlz+bfi`ds26};LDO0+=ryPX9d0?jydkfp75MWR=@jx~DRj4jA|a=l z!%C3#jy&NlHBw4Ups+4n$k9bB>n&MzPBSLR*q3kIIH|w5x_{?nal;OVlum|Ge{Qnc zYf^W#Y*2z|$uTAsg(xC8@LB=VSuu-7R@cBCc<4QtBKs`T0Pp@eFsQBf} zJSB_U8zbYls;m5L|J6I^$rEbC)ea8x+r#3O6}+H5{KZGQ`KF*?sbeyzIEE6x1FROP zzFxY8*vib2#KTu=mL!_Ki(PZ1@Rci7F+GYaE103D6{I93hI18!V>HE)gWFL>k!G1p z7zpW_f5cLlh&&FH6^jOy>($i`hh~!$TQ6Jomwz@T)%?)O>gp&6U%VPp=WwsJgQ*og z>8ydNkr8Gf@EG89^FZ>C^62W|1R8|YOAAIzCyK+dIF(%v4Tb?yU|I+4S_y}7t~I6{ zps0VKMUl!#iD8rlF8Q~8o?K2d)hQPR=${O0h1KQgS$LjmOq-_G7C3b(KnVJEOfZVm@4R=csmLY)+&8_3yV~@SJbTB9(Y0g zj6X@Rx$lJ%f`@p#bFWgWWHLX0tgQtk|!f-CNv zF!IDB^c@hFl-Bsa-yaiE(qXQ=1a(xT|4{+ICpzC;-n=t4GV%-1SKM3Ui40ls>uYg) z2Ph`kA8@j%iAml>urmZjypS)bk`Hzaf;k~rX=vz}V-G`W?*Ne~c#)tvWUjTdv%a%w z=ExIVYDO*Ca==tnFp>iQEjQ4;fO-z9p^{f`IeyNw>_~h?%6eRDv{zKv1_O`|BX1r! zcrYNZO-?gIkOifW?JkxN{WBgIjXwM5FJrYbO4k2D+j+rqis3Y#7RloBISN2S5+ z+E1SrnEV-`;o`)XcYf|(?RS6PT-KAO(WTu5Mvwj(Fw>U;7*gXfmncFLet-_Y{``sR z!|R`(LYc=gqP3drX>N$v1y4Lgy9!H13M11un3sXtZ1BqBS7`A-@jwf?mtXl@nXZlt zj~17?%_ZFxJ$jr-GZT7qfpugz(Ewr|UcK7X+?*+#$wXm)##G1fIc~X6pZ;K3=DrTL z!9uBpR|-cqKd4b`{X`$g0w3!WbgQS=3il*rn{?a_%mrv-j*^e0V#Gw{{loTm;yyC4 zm&iwe93M|xfNYFQ5boxRR6Am?DafiM2wjyi4^r6tm@IGAGWMvh9Kq_o-(Jf+^p>AG zb%^&fRE}&XCu|Z?>DT-^2rD_`xhNa4#`zB`>L@nvk#1V>i3q# zNDuV!%a<6b>uV-q5KBEj@wmCQ_3#lRuB&neHU`&y_noPzNS&3l?TUOgrJ=PiB=7UG z4@vz*oGyWpDzBCQeFz^He!(cI2kdWQOuU=kyRfA~FgZR>72zAE%ZZJBB%+Y5yZF*8 zQtk093dUxRJp505>W*N7Z2LjDb|z1o*0I%`_w946%qANmnXiP9i0QxGoK7#v=i!65 z^Cz1*V~`0{=3hG_I=Y~H4=Oje!M&eRm@v24X!EN^4`kBRTy9)T!3{t$tS%y0qI4!6 z`&&nroOu3ba0C5Pm;G9CAz2m5rk`UmuUm$UHyk4oIpkr+*z;KxoTM#>3fl1a61vf3 z!{PI1B0wgYP&dq7V4Yw+D+|*X)HUrxKcZT;b`sKp3Ne|cZM{7uoR4?wcew1shj6L> zA~yyfi%@yFa^}q2kUkmjVOVd5_M!2+Xp_UW>IP32+h>84G zMb!eaLluBd^7Qk0qT?pMI<@38y)U7l=yT7WKQBND^su~nu6yO(jVW^{ot`v){AcJr z&NsKF%9F%7l99nKI6OJl3yfKOdH4!p7AJQLKSF8!qK*FnxCh9d?Sss;$zRic@qE8a zT-V*Z%v-YLSjWdj4vp8o%IWUO^OnnM{+JW%hBn3ej663ze?YS4WvS&R6=s(gSm3#d zE}UKU&xW}Bu^*ZL1mSLAYGpN1WpOcIzzDRc*^Bjjdq2|7<#DO~RNJ!^0(E>>oC zWL2~i2WZe3N~_ekdDC4+cUaz!iC7gS;3u9rp(HTr$Xb8gDJNnu=~zVNz*BDP<1b!h zrpTfiabH@`mC|dU!3m|Nnq#%6jXr4ZLKX_DcH-!aj(=QcB$9|dh&2VrYOdlst@Y=; zxceBXk*o6e1ewxBJzuAmiCp+R66S4qCnx`=Vwbi_$QGfyY*;zi0bq)L$b9Qon8iOB zHhARxFjtI1DSvLzUn|$}H>yNi$R4T88BkRSI8$)wsO*G`^8+Gysc|eq#PxjA185i zfkDz3X+d>aBrB*}Sy)(%&-&ZFZ~DGOzNQza3)pzvAVe79(%9=9YUDTG~R zP0ck2g_{9_GatARMT(rdh*D^l;JZeK-NKt$|3r)ohd+N@7+Sw*gS3jcwZ79t8Z6LW zS}|fN=$pXnjT!Dqa0fy>R7sL*b3B}xQcD!i<|T$Lqa3suasCYSPNj@>0w{xs`5XP$ zWq_>DT=uvXu9>^wwLlbmxya7KLVx}G$RYw^i9}8^oW|@GW#tQ@x8jJ)=zWNas!-Sy zeA5wGt8zapx^A51_xy?P@)gsSf_%=K2y5u^;|R83up%-w_Sxi#6IUiL9ZRUb+fc0x z9-*KK)DLfx|117XLVuB|A==>_=GGj&Y;rR2))*NfVO7WbCP0GOQK~1AQVFFqyOuA8 zonQlwSukj`uXlvJ;R>taEG9R2kiRLL{rzdV_EN-x0ORVn^aApD$4rxPT}3PXQo`?E zzpfj;0@`lk=_R1F88<1BC>GAA$m^M?U#_u^U4L)u%b^*+_`bp4e!P9FLZyc8HRBie zCew6)`V+pre)|?JZFI*m3NsXbZA*)WHKkp@!=@G%g;Z8v_TBEeG;J8tBHDMj-}79x z9~-T$tGN;uKhIX)m|f~r<_jlpmCAwB`t9ZP8SQdIy>~hc4(nyez!8x1NvBZ(U@<;K z0P~?kRwEw|6C@fy92D9#oZp|-PwOWp_VxSs5LKIXK>WkUt4H>=*3W)P^~tm-SB&Af zoPo-z{LnyQep&1+CkPcXw~+5%TG=Eg|-i9hj4~^XB2w zd^NcdB9bdW;)TR!a-l#5=2vW~g+I7UyY=zomi=1U5P=@_p zdv521e$%`7c1f6AxfO9DH|humsKPt_ZmK$%CX{dZL)8zQL86dm215|6xGKt?JA_l- zzL%cnIV@x}G(T&LE=aY1N*feiwv+^62C7&mR($@f7p%hYfgF9Gs+_CzAtOhQ6i|EN zoN2}3gbtT7Z_Ac%x2E^MwS8P+OE!5rRUd>j^`He~xh=VF2OeE8?Mz8=am zL4#X`v2dk;;!`q_ z8?<hn|rJp<S3F|C`HbPg(DPA?I(|R_mvg$q_xDXiXtV%HR%^V5i0ri<}CO4 z)JCVd(YtTIE-JF}rBrFh#E_=B+xeusx&GDY$4%e9(OAFIbPr!4IbcAz`&b}B6dpLRSxUN0Kv(@g6-AD<0e6;5I;NWAM zO^kaOIv!FJ1Z?~r?lOVV4V%b@1~sxC`Kz}`*ik`8j$sp}pAO*#=eMX14$`F!+RJ!J zsydEHU}=&}A~{6^%kNi(Kkqh7B4lN3u1$PbH(szs@ z;vvP8d;SzZIF!Ufi@yaR#P});jo*#ee%;uUwagmNuJV{hmjx@skLL-XcaD&&Yp%A$ ze1hqJK!`6NH@;tCPY3O~-qoCyWCs1FL$ha=Kqo^UuY$po&HzqVXUUSWp51!%K$w}H zy?CBFqx|?$eecVHrSbX6a$v|J`~$<*q}6W+^ersVddtN)?er4vlteiUCo*iMqb@v60<$>FUD7a>T3!f@M{Bs#kh6-LXw*U1Tz z^cOW`?Ijltcm5?Gkamz}hf!H>ElpA3t8DkINPhg_x=}LU+)h+_os@l868JcyvjPeh zlEEyC{Hp&+d;*MUu;C$M_JZr9PpN-LC< zcTbr7XE}!mXCa^55WsSNMF%W@ZpC2G1$SZC0c!)yn|p4i{i01etVQ};P$=|)+gP?7 z=H)t(bihCvkDZ>iJXx>zRg=qtL4IuE#7^;ni9#WOE$Kb{I!&5QY}$*V&!-D0r1{a9gfWjvux!~lE${{828 zYEgGUr;;i-e?s>~R}!lxC@OEgAGYUEJDGPxcAe>j_$+5&e`z396i5VwG-H*M(;dzj zGIsFMzOEyoA7@YM+6}6a-#)mL#xPKzzNZK;=W|3?ZjM8B9&3WhmE|PfAACGyxK~6Z z3r9fSs6g?E3Rrsyky;1|ghV6S?%^}$n;z`MK(*e|Hk5t*NFrrRvCWOXBW8W2qGxv~ z+po%wfE_0>0JR3p6_wrswCC0NHT1|hrilp~c60%bUW5D24z^kUq#WpMY(@TL(U7^g zx|YOSDQ_t3Gt zACe3MrRWe^S%#wK(JOIWz~rlb8H&B1 ze_d=CF{X8Q50PxxVX1)wccE;8;TNoSWR!ybcrEK~kOHazaf%3?xs4M&CE)YOh&M`c znN42@AH<2EV1HndN0;o0)21z(GPF=?2Etlpxsp(spO4suKLFFsGK& zog_;V4S~~;RDvI|e}#v~o+0fSSDm+ykIIXJggwF_^uF2=K>iZJm%IkagO`fAyyKqkdIOvQS0W@$NRBDfXszs`Tlo8g-4ou@-ct`L* z;34&)l-fv-yS)xQMgfU!8hz9go7wN;9^Hhjxm*A@{$DMMdQK4y$4*X11UDo{tEJ}U zF+wyJ`{NuSElSg(yu~xm!@Z!RxUoL=G2gL0s|uV><;7*2nor0(s3!>$?QuQVBRRLp z*u~=6mYtw(wo82*MXEBTM2rUvUP$X4g2s)tiWVdyk~37m&Jj*|E$gdgLQ9$U;zc0v zrPde)`)NH6*2H{mXYN8HA9Ci$gPUn?DU>J!Hk(&!uH$vT6 z7*OXGe&%Z8vUQVA_eiT4g7`?#SO51-jAYI-n=*kzgj)_y8DQm(WzJYCllc|h!){=1 z2b6IDFQNDI%6ztogS;J>rG&!@Qd8RI(0^nyfmY0cn&264_cnOsqUA^TP~NEPGj_<@ z*)rh&md#x2`y26~3YQe$y#e9{OkM<$31W^_Nrow0&d9QG6#_sZ=mEmXF}YQx$A2O_ zVM1=n)y8G-`FDprJ&8O^&?AV0SAdSFV*)SrN#;`qEj(_1E_k&3MlpRCIV3q>j}2X4 zP4FVUSAjA0qzI%oFm2B*2Go)M=jJC;~~x!yKfR$cW>*KQ&=#`HVf$ZZb^ zC?ut&2P$%&d1Z=)m6p2xe9N~kj5~(;<^cks9t~Gk>ef}mrcbH>=2IYYXVflIe{Q|X zzaxlTa?qeBcH;f}Pw+&o67=@h`$t{1b*PVn0+`OrmrQ+Gwf;e9XsFhhN>FdK`r!14 zqDk{ZO9UmQ)f24M$;k;6JN5bVrI%iDy;oLRk|GMDpf(1PkL*C!@?D6UnKK=!E<8`5 zj!`~K=s72FzJ&!XG&e2q^6RrO&5R_Uj=(u1Lpc9r%;2pdZCB>ea*N3j^LF9q7Z@c@ zZ;G#4-vTLr+9xn*E&#q&zD%SsWWK(5^-5{{_=TZ!9hyY$S6#h2Nj=#hbX(2b5!NB; z%!hqZ(U>r5c!m+cG$EC_v2#GDB)p%IqQNycjY%#b%OKNl-oE`lV%y@-+cob@sp{yK zCr_XL4gglh48uQfCK&}UbK2B<3$<9_?QFkCxDg_d_=SaS;*aIz~RivhR zO^l6?*!~R)s7LNDewp?W#-z3iqUl~TfJuD@jqy*TMSWJ*fJ$WIF=F;;d;0GSfoW3YHvAwJG5axuAR0c=CD~FPxI&^vs^{Q^VAorKyn$cYvq9|NRGM`wT=3T%M z7)6$wf44C&r>k|sf(x}PLk`c6_Ai1#LmhdkD8^w}^%P(5|Ak_k-M^>(^AAH*P{y>} zGio0-hb;0zx%jC%wAz{REHK2l` ztqpUThsekh;}ju(QfGnKL<9^yh4HNtf;Q(^1Ot|qi3OpKhTl+GeF>5uqe1MA)mCpi zq$iQ?Dc6;^iZ5JAc(RNq322H_9a5Q_Kr6iGt#$`$*Gq;{>YDQMC4gBge?(?J2`@;C zr;4DY=N@kTu~FSIJoD9&QLB)?`i#UPd%>daYF5ej`dCD7avmDr=ZCJ0=-#ax0r6Ej zNRU!wJa0Zo&0m*X-F8zEK>-5Bp;HYmZYUaFC;IB-Zd*z)l|D^lPI6SS>>&y7?5;Qt z-)O%|M6a3?2eJ;U;Q6CR(-=!ys)gKY#(&Cndg7sO~X^uabF6u4zh?b>?kmk!Xn#(Fh*K2n4(#ww9~OH`3POA`YTr(rJUK2te$75h z|29k2e@bfdO=<4ZZFneL-dHx`1A~KaX$}PVpsfJ|fof>_K;x_-bCHWNGyQw>%>mjh z(XSX$1ojdVkj~Pq*tc0EKR{VT#SxSOg>D9slik?OKYqj`OJ112W#sC8$}nNwKvKh#n07g5Ebj#6rcmJO8nMrRZIPP-lW z$KahTx-t1$DB^d9M+r(sm6g(xU$h5@{KM5lJ65tTsEaZXwv17-N z8z=9XI_P?Z`+gyB>FU)jDG7z0`Y_ergozLJh7XS~og@XVGiQ0QhI;n;eHVZ4h;&f0 zr9k4#L3+tDK#HD!*MJfp5_=DqW6u3KT!d&0nd0}>*wFBLmT_g{k261|jvPB?kmAzJ zWk{<2^3c6Tx9vvCET}8|v`}ay@syUL&t5QgHg|5WU3$;R#B-t#1Bp!pb+thl&k(G2 zykbHs7zIsfP~iJ(Q$&IkO&4ivKSrp@pv(NsgSQoa9Xh-y4r2@*Np_=|WnZ!0y%{W^ zVP7e+dl?R1@*=X(BP%K0N;&IKwDk?+t!}WkT-=Eqn?~FHBoz-x?E3h$mz>tLGXyJ= zSMS`ff9!f)zkS>6>=5TH^RxWtI4+g++ts9S@j?r2h6^^?rl#6gm-^&tZ>Ox=Ka#p+ zX3M&CwLvyh*ktIfY->Mbxe|=itGy~S{VU&g^}Y4i-TsHoC*)!uWFSI5Z6Qpul@VSP zW!KH24EzF-Tm3eO`sj$n<7vC_EyLY3y&2$I+NPw@l^r{Kc2QcI6^okn*R;2%xn)i2 zH*hC9^X#rRu_gbmMoH;o5llj2z~l&sXIon#Wr;W@E~lCATvpeFj1hS_bkr!Jt4E7P zw<@J}=1kPY?JK;>b+RgqNOxRWV&S8{r>@dS)=0Z8XxEfQ<~=si79Pkh9z6}Y5m_{NWamYy!? z+Jmi_#6Vrf3XPMl5@Ibeg58UcZ{lCje>&RR1M}rgP+U?n|0rztfaagC*Epo22TZUn zDag%LmXU#7AI#L{XU`g%UCejLz1>?~DLS%De5RA1{fK_ilm^3x3kFVy9HwAvi8W)Y zqbL&~4q=@4vu6u`mYs5MZ@mp`qwG_MFkaAcc{eAFe|R~oM7qF~HrYB^YVhEvZq;*S zWj!TsS?6B`?B;ji;LYhD1AVmUxiv?FA{yYp580B7+&tpTuS+AzQ5Z~E5QQzCAqQG) zfD%j`Va;4#i}mze@hj6JnpGMa_s)x1(No<b*#62UKSl$sZG#k_x{`anSzwy0>_+rt+Umzjg&<*ysQSoZJ0KuHknQGUZDBzDlt zQ@1!L3u94jll@PgRCM{ZgPZ~9#6Ws=<)86FKwhGXWXrw##_Fjl4?HBk;lU*_=7KH_S_s6z*aT2an%Pf)xA)wJ_#D7>^2 zHirh6;(%~tWEl5td9$&ly~CbwCuHWcr*{0cVbBhy>wNvXVzRUE9}%mJ4YzOJR6v|r zZ1;Ch59f)~r$fTFu<^;KbB34zsFBb{U^26%s+Wk96p4_WhQd5Bq2ai_IziH@bV%3- zDm^tI>A^A6*M@|=A(b1}G4}X-!C4JN0M4IakgPa5taw)skIS*7;Kr_JXA4tC>4DJU zpxw9?87X#X^24QDt*nyR(&Lu~OaW5_USm0UWk%+_pEFbINtLE&f0gwiA#+KBM`?j` zurSo++tH8^qg=D^S5Gdv1f0U(56(@O6eYj*?WAlwM_J38i}a zy#|qNm01AdyGxm7CZykk@WJ%VpO7!M|r~FG>p7FuGC1q>4mx<_UcC-+^JFDTjcBC zXg58I&*8MC+8?~I!-P7T+m}j)gu9kszo@6A)aHm}f8oPorrGJ}s7ZSC?g#Vx{uCF) zi1(@?QwbAuqC@ua5r-WWT#1s zi@Y73!b9VQ1CN@qI5-vn@zXu5@uok0+7acu%}ylyj{Gr%9Dqr*wWY$K>0GNZ*tqz) zF?QOSeYAp{EI%r9%CN~XUxvt?MAo3avB=j|(6nHAg7*)^ffC1S*%iD{0zMkA!fX=i zTtVzj(+BOw^@HNc0A~dKk(`Vfn*pE9m zyQ4?d)a1^C0x>oKt|qn=9`% zbvoD{enm1?(C4bWp=j8Ew9mVaMZjPZ@MKG#psOp4EhluUD4PV|C`oo#<#!% z0qPmcAgkeAM4s&ppHDB)H>96P{0^*PdofruGJy@b2MHXxL*07zlvYr51*ul!e+sx> z_6A&cA=E^YYTcd23r~SfSwfGudGiMf`m=?rEpWD`nJHzY@@*d`T5`@DVZ@1|q9V$- zszJFut=XJsI(|A`db7!&yF;_i{YsVtrEpIUi$d)7A#3f85ESCoKY09RHL0%3z)VsG2OC{{(;%M_2x`X~u^laZ&F0JbCF`SUW5%R|(d~6NgLOHJ( zJcPQaa)Ef%=I41%lCtj6S;+&nX={a)GnK7!eLF?nVL9{{{1R7CN_Z+^x*!LU8CZ`X z)I*W-`+E&ADw6pE~w0F$nDF z-nxXs7n(z+TnG!JOya&w{YRD=AxcWCWCt>t?fv`rXe93nBSX^fEa7SqW(bFbjBG7q zQwA=q?eh#_FKy}-eXiykRA0cTIh(&!Ry+*IYN&|dQL@`0E{l9Q8x z)A64(!i7@WyjU1wrS2TReoA6ooHU_C#S3T4ZTv4hy4(&hf!)hBJ{3v~v=77=_!yOT zUcmo>{V;lkYKBu|okq&Z| z=70IFTK=c#_2{MCy6@cE-&$Ji&OadBZyp9nY)5il6Yj?Svn5st301aV=~%An5g#hYPy>L&E5Dcc4 z9szo(_6@RPOYG}s&pspZ=N3hu&YYaM+qVhK44xPi9Gt>5;e1i)SR3K@UJE>+5tK>Zlb(bqh@Sd-wEn+$l)~sY6=ob*oO9JHu`(O z@aQ5XkUVtc=l+LnQW%FQhI4XcTpmVF4GrT-r_IgG7X0-WH5_$9b!ec5qQei5FLNgz z8`z2SzL}R`S`0iO$8jlfi(w%EzPt@C3dq6ivcsz0f24##7ncON3M)P<*{3JAZI5=! zOx8$~4ImC0>%xBh`cdfwHvrjgS{D4u#IUju`~{t2fsb%M14GeX{ysNa<|1cN?cocO zAQ2^G?3xQ^RAH`C(fJc6ZlY<+_`#RT>f8_~WPAsY8-$U_7X;PD#kH{gV*}uAllUMy z;Q}9vpMITsZIi-+P}UuwC#910g{{<7^J{cY^EA+rFTfjZ5loW;b1S1lE_p!RV zIDCFjK?L=OA~<#E5kdtD14O>M~Q{mX_T6MB)P{a!O;QH>{b5rx$5c;6-kyk z5$JFQq|HgnLYrgO=ovb?y0kr7TepsHJ?BcGP^;cpH8qR^*h3({&D%0>mGK^u2xQCn1r(5Z zUVUTS{ls7GZ9gBxnP)_pFxn2SL5l-1B?ZObd1z$1DbAdEWkQXqkcDJm6U|FQ)>yV} zjM=kr|$M)z%%)7|XD-5c*m-8BsvNFIEiBl8mEo zDG}ofVxVf_BxB8g3nK3Cje9BhNjJ4j6yzzKsKIqKc=^LMZ3?b~&aaD8=YXrJSpC;M z^EMX^O-2rpmJ!?l5%3ako-wr#XU4WJ%2sW#_r!rc2<&$+D~I%kvH#pSvuinB)2 zZ`QEV)BQTdu@bLf{OF0SQyJ{o#1Y(NvuCkMNz#NLNt0%%XN|Nx-_2?Zdd8%xSp2`| z7ahtw?{02h4&i~G`u;dhDr!m$-FR2Wj@4GCD2o8)f+4R(ydY>+4}L#3NUWRZ33`?v zBQ&|9=|R)zVk?XDU%diJZiKFGhSjP_4?;CKY~)Cx0wH7tP_x_!4VUGP;XGLuwOZo^ zKfmQ@J6Hi6X_Y-~tWX|Y-MyBkTh{ZYrU=amErZ$RKRNSkhhb|LqEUpTCRlmYKq{)L z882RJ3K%te7d)z``jBJ5T%0a~5{g4M&f+@zcS2)uTSi7^=H)&EGq!C^u%^~{_U9v2 zQOWb?hc~o@w*L{5Kd$_hHJtfbb4rBe($vS<%FIkQ?#Ro}$P|g~s-W9tf4`cb=(?r4 zRuq`3{@G6*o0ul#x{af1Ld(KS?r zFzo|&AJUT@m|&fMOak9&Lisz+I-xvl&OsL4R-Qv>W^@CD>(>5{WdIc^MzT3XrDD{;%oA(`xEvI_~IbMPil*jBE14 zsm6wuiP+RkrCuvXyZ$I*f{B_;tM;bTMXYgMM6@x06a-vo4 zBAa`~6`^vcm%taqsFs4{k{$e3`^uUUebJ2FW1F^`MIRb3Bg-j8d7kd4I}t0j^&H2W zv}omjl{YL=^SG78d1Xij|>vlzu{6X{ebo1{H&eN zXFu1q#YbbC7FQDXP*}tl{2pn%wW<5!gw6ACh6_YmZKyhxR1 z4n0?j-{Kmn6Fj<`Hu`(SUGE;#h%(ri$WqX)6Jl`_9xQFT%qfyk8oH`vRg2#asFdW` z#HUS1qvCp!0^#r}>H%;k*n6^)D~Syty%MKcTeEoLTAlG$DPWJ?aSr*ta^E7q4o<3 ze!eW#5}P@W5`@(LQC}%PxM{JP_7NJ$f?t4@KW6S)Wco}9d`@rJ+TJU=&hIsQQxig{ zCB;5kbj20q_;gm~pFZ7`W(wuvy3x|Z)i17~vb}Ke(cG+vfcdWN?QoVXJa!a2z4pKf z>)V#(iwet-ygsGWq%`)A;TnQ%r8zo!VB^b*q&ttqzlR`6aD&FF`=%kl00W_uNZ|q*(XWEN6{2j>&**dY7YoxD zp)|OPgrT~&`OSbU3g5dA!2!E)i3$nJbmZ}YBEf+aL5)3^g!cK1k8`)Cuj+So zM~dOK8;gqM8@^{8e)(A`Yt#HEs1Jlfm41Umzxig4<71e*b8Up?Fp~8H%d5$Dp63aU zU}}C}subhr`iH^((OH*oKK5Vtd+hYP)mGqcL=*@ZE=@{R0P68_CG-17o_&1T{=77# zhsw^F_9qVpjgjdkGiB_UumipAjtsTUKM*Q@EF zY~C6qd~Z4unP7cw)XwH1jvM>PuZzF+V%how-M+c+)U-HnCD&_^m~7?G#z{%LGd=|e znVH9I_>$zDz}|=8Lr($h18>H&4YT--FiDVzvuFwQF+aiff$9-9$J{;!c8Ty$wUhlF zh=2dYEsRV*E#-I^$O+e;AmDCoq7F}Ay9~5Ov;9vToY-Dq7Ng=743%{# z@2KZONNP-}Qmq@DpO;4nep*I`uj~1PNf!HC?*{~Fp4F9OK1tuvenl#-KUawKh|I4B zgabYa1-Indp+gJ>C!mMn(7K{5Osb$cr;{Yzq#|tIL12v`z_18S0s~(^4Fa}QSv~i+ zRDq=m2J8zwdsaJWK8^-%dC+Kl4LD5ab*+5%gsur!g?`kU_Mh(Ug(xT3F64gva2oz9 z>!z+8)1U7m@?m`BemAe6;~R_OHJrb`-|z1~D0g=~O)SS2?!mLpP0Hx#$&n&NC3;m9 z@E4-_C1M*;f3&Kge*;jVzUPLX0sxHeT}Ous7W>hR6F=y%b;OePHeSx$1}*dR$dMxu zNZ+a~a22%jfk8AW?}Lz@T)PGlb-BF$elgdF(*0syD#U$@%a-bMUA-FfELBhl!*&Vd z<6N3=QDRc%+Pfu0UDoMy|A{5v6&xuYry!M3C$2m_$o!<{I29GVzflfe8>WyL)B_#A zu0@Lhz572S)(`N0hIE3`V)%@0lDSW2(QO6}Z7*JXAqR=gY>FJFJ1vOdWW;)ZYNBBj zk*(rv5se;kkfjL8a(ic6syur1h;3Fo;i=_^f|&Ha@gP_L*EJx-#0tv}T+A1-YnPJm zX2iW zB2>XQb){!5RIAv`|D`(SG=?C)c1=)(qFtyOL;hbtsOE*F!k8|4_njy7d=K$l4gZIA z5tM?blu7-|puj)BcG7JHR!oV*Qp|wPEQmcqW}1z?`-AfO2yF|!=3AqrqLGY*0t@1B zQN8+nmOm7`DQ7K9VqG#ToHCgvtN3ZNNFaH|fixrBV;Lbj>L1+nEe)waYf#zh$ifjr ztO)5h^R=~+uw1xNKNjBV!k1w}B0yv)?J;Etv$mA__t}e3fe|SbG3jR|MJhduhYTll z>kpU`zb?0E`F7SgC!mXqOZD4WK~H_}oaOcs&da@SH_}*Y#Lc^XOPhcVTBx1`aBTbb zEVpj**Ifz2X~+6zS&acsN+ zLVcV35M;-Oe+R_!W^V`5HbF(@RMi(TktrhxeJkb>0RYoY{f11DbbCFK*9K=< zj4O1U5bX=3cjZQouc*j99l7P_|3GL3&gsM7$nHboH3RhjJGghj1=)t>WrcDI5Qke zkPUPP|An%7x`JbeOk3pZE#l_xMyESb?mw30u zN_P|>xACO~c6>t1-XOZa=si0A)**A(J-s{lgojHrfRy-o1Kge``m=LljEKi2-mR}N z(GY!mLBZYS!bHq%#qm)zhxESTD`4EeQWEQ(+dLmBWuI;bKyJ^@IXNsSGYD%_9L^!m z0i63nxZ4kDuu&3uyLH*m+`!!ab~18$!Lmc)1nyg81mnvUUto~gu1PPq5C^1AY7VM+LWH&=rI2Jeu9Rc5&ep5%d z>h6R4(R3mBWlZDDor$NQz*io!gcsm8{T{N`?znxXsPCfHr21{#XcKVCKGV!O02nco zGyL^{V8>m->*vd@n|6lNb;+ey5Xh2Zagp7hDtEpK<8yEAZp!nCa1ZzB!c^XN32myq zz@dPoL=uDU&Qa;D^VjCA4ph$JH@DIp^)$ z5Q)FfoinVjw^hQpvn7^p{PCNk_g*wn2?Y<2`_woPB+oj*;Q#oIY>aTU>H?canKS(R zo+{eC0o%m9Bu<#AFu3yh>Q!sd5I_LHdg>&=2G(Kj)xGiNw>G;BjZaBKjxmx8mi8shjK`Zg zO8tZKtgP7#!kEp=CK-OItrb)bJV1&)a7%~|N5`MEdt_yqpR-PA0c)g?q|EWxkwugs zU`a8FpO;N(A3bn+##T`dvxj}OyN11_@nrGgD0%Hmo6Lt<Rkk&gnYS`vh3>rME;H%x32imBS^(aOYeT_Y71u$FomAtpE3Mh11FZ6iCr z`IFVAe%UvO>ofDavVXP8)_(@$-3DF58O4i27!ANLOk%t0owG5Qc5pL`?ejt_vj;W^ zfm?hX&PEEG$j$PW>j{(2vn`lz8aX{EW?~er>gcPaBqc3PO&d}Em6PCMZk_^$LI3J` z0ztJLd5#PZI~kQ?-&;&Peyjk7!NB&+q%0z)BCl)Lf*FQ0m$<_0R=u>Jo#9q72L8gJm zy`~Epf!aEP<4j-r2s;^2 z$V}7lDdi;kcXXpdG<|itzox%#4gtHA6LT3nOk@9Q=^+M&P-162waGBMNW=*p5o+x#xR=<3?Ze8GeIWx5~#UR&g^J}P=BA)7yzZ+U%lbNZp@YqYeqn0f;z zZER8O;ulpfN%d!vEuzyaEKmiNvXHvd6nV&b~+6=Lgw=H`hBp4ks`o0aniL!@ry z`lQ7dXM)h}+sqkOnEF*dI!Kq16)>E$iQ*;Aixy|wi#0bVFqEC(AM3aAQTg8M=l7Ut zeY~ioMEM4M54RU%!VUOnuQiD!AOItPpg1Noym}@K+Yx;C3qyh$n_~xPqe{6Afdr8b z=?~lxXm$Hj_lwgzuG24qMVBC2Ev<)G1}rB$^gXuc5zL5btp{uxea>6$YwOH=>dOyi zTHm>m4_%L4*BbM_(MZs@av1$6ezql+{w!)u?J|4G7L7|GYhtq$W}a9-WzUWsHxm;J z_C|f;pruQ12P$y<8M>}f= zMx~`CvU)ppCouh~UX1r8;3c*|=i;h~ar$)?6}Q{M5U3+?j`LNkwCiHl7CwOMP_id0C zhAWf7j*O6N1vz8o&YqWTY*sfTGRGZZ_1vI#&r)NpmR&ulm&^MlOb+y2bRkH^>J`mA zYg!Njaab}Kjq>JDhiL(Q0Xug1sMFfvzxXz}DTR<(E(#J%jxLBm;HE4JrU73j_+R-4 z9UP{}JB1@szWpqz=ZLI|l&>T1DY|uK6#zhGrKLgLq8oAJ4CL~$G7@s*_thvYF|-@3f0 zL9Qt_ykqps^QODx&-9m~u2h?IBWCx!nzMosalp7muS-gDuRO!II+sp2RTtpHWVpD- zL21?JuZIng&=!Kn@R{#<32A#h#}X8Ci>c}Td-qUe7#bN-_DGH%y}TsmqE zz@Qj(KO^K>pP0-b-O(<$$P0qFzr3XS5(0u-o1b_g`TJsOK4p)n)C(zI$ykV_k=n@h ztu;oNKn^&Mm+3*<_vyPWy6J6(BLCqzp)CFe9;0wzYbn~yY^^)*$YSSTG4QkFXT4gd3J*nG)xQ?v&X%|H zj{`U${{Q_&8v%;Z-MKS%%_LRFp$uv#7UgqUnW+F)h&+_ozvzOZW54We7q46)eWXa} z%L93!Ce-Jh07oSj`_6YaWu!2Vy5nc*e*-3`{5N3orhTWdKkng$+OD;i+wEudpd8dKf8$IQ|wm(Vh_JNU3&tXUx|-Z zRY~x8b}sPCyuYM6iZnr_a=Vp7n)}ya^~D@ZHT%jPU*FHO`y)W{&NWl*lDT3CqDZPk zCkM#zIgt}VSbT4OAQ&Q_Jg}q`X9LM%yGjHxs)WQt+xmj&65v&b;}2GrVF;Q!sf{hd z=^r2Iu z4Mrp23}2zsbVCyls~=uhTY}u3@ekaoLp}gTFX#E<^Ddj3cK58+?^;6l)p||4rRL|# zImaX3*_Fpb*xf@FoJ?*A{2A%@?rOd@j<}}nXM=1X6!hxp`n&fV*8zYX*miF0+4937 zKJ~`-cRCifoCWOSVIjW{aOs%V_fgVKnBE53#9TcqRkCZK52~*lA&KCs9Dn9zLUHS# zKlYhfoS9TlO51~S=TMwU>pn2BNKSEzU0+*P_QWnWF)`rfXL=_!(sB0UD7kH9au4&1 zqe*}S@}UUjQf>#5XWs0A>knz*X$zi>J9W5eEbcMt{O9_vGM@Y7Zn`yqa^7Ja*li;~ zI>qzJ7j!3&jRAm%*W6K}H)MNI<6qZSMz5>+{UERYCmHr@SFU_y=eT1>wp6{q-=8l! zTyl;vDyftc)~zN?A6`3YEsM}0mHKp7yC2d$y#5yVbmelJ-H}WcN^Lhay^x?0o*T3M zK4-!AuOH^y@v+GFzuv#7@d{=OwZc#;49@sBd${dF5^9im+^N~EtwZ68fa*@2aO2$` z*7C7t=M>$V*P&}B5g*uce5%s(v1C-eZd~#1hkng^k>+>ZyR;GR1JoH`=wtaq=?5CDT*BToyTOOiV@rWv!WUM<4@jn>6MEPB- zcqb`|`+8@YFwmIL1??AfTR^&2rfAv_j`tg_k?w0T+R2kL?%1Mz zdddGzwQZ-8F?MftXbGPELQjmG!&bE4Ne#SGFrx2u7YwKU{PClWsZ)0v_yUk}^m5H8 zeM6im3YxRhHyUT%)s?f!iGW1tHZ!FYGjB?`aA90-OnvYo^Nn~xUY5SgchnmLXs&Zd z->YV0`|caFuM|c@VbgByEFd}K`!_Nz66z3ZcD%nofPojgzS?!FpP;cp^l?<$ zor(q8Xh#WY4i_6!ZcwL?ijlp0gD!`{h1<7(k|)%8DDWZY&yrq>VSy88R~c&6zy?h5 z<@Wxdw3!CWF0CTaD|Gfh(jY7 zX0+6N+DkV`m&MsoW#jt%7N}|#QP7vQHlVu0|aDgh3X7;59Mw8 z#=Cbp_8XWmV6rc5R6Awbl)=4&4!YRC{qX72Q+0P8F{Xmt!8N&i)$zU9M86x&=p|gf zLl3zANREgA-#IAFbT8-fbgAOT?z!C;u?pDtg8szr4~@pqDeJYCpUZY~?(5FJrKo-A zEBHB0h>)|}hY}4p!E;-DQS0wJ)6pd`?M~kYFpI2Iy8n>pT^`p>R)6$bWF)P2i}Rvx zrr~=M=zteCP4Y%d)u-)vW&H%EB^%Ebs%V8x9`GWTs{{)$L@$JgM=ywOkOaXuL7!Tl z`FWv-M#_|vi!RW`pF=~N*=8Ur@_y?IV`IU;XXBvW`>PU2)KIZ*0s{$y(>K99W4QJU zG!(8p-7g0(JX#-VG*zD*gD(Smia1$8seQ~FbT6JLu}ot#(cm-qcX<+UVtZ+UP=U^x zp06cva2-Eiw*Zd6b7|BAE*2P`me)FwxL5)X>=__N1w3U%m5dWc;auLqai7Gk{&5*X%Z%qEBS^n=#q=(v6(~UyL3cI!8 zX~)O#7`BSL7U5A+oaK)d$-x_#8iSIBp5{L!naVNtI8T16;T?qu6P~|GQZbD*%BV%1 z!HUGD@t@y5d?DaDIT6r@v_rU1{yu-+?u$?G_`Dc%A<`CFn)K(OIRiSr8_U7m1ng$W zh3IwljBDo*#!r<58K2Z>Z7#eMGqXlLr zj|vKAV0epv=Tw<3b61Ny^Q_yBK8lPbp_~C6KxIo_>6ll$hOX$kdqxt$9=S+f)uq3C z|5C``a_hv=j}NYkhPiQ`@#rNbCFRe&WUWGZr`_-3zuadtQkYG9A}nW)FE>aN=BxxY z!MKhxy?G+vH23DYqpm+{rEcdS7-LtnX-??K>V>X~yD0i{^@4J4AMf7cFo{qm9i1_rq+dMN3S2lf%vYfUIj;b z{YkI(t+-YG@xfFPr$>x?LGbzY=#grxH z5cm1k+g}97?YEJIRVXyx7wz`M+9&JHl$zzM70Y@|>l9?O z#B+|erPx#7AY0@f;(+VA83@9lj~kIldI7R$u`*&9ZU4m~%G{CaIhVFs*_$t2KAJKJ zcaP*8?f=qxSH-7p__dMZ%4WbKv zMs1xI*7EsJm$>w2W&^#>sEXtXLr>v(#Z9uFP3RF8%TyDFBDWNS=@8O|v#+*n;zQS`!~Kv1FV9TBa!{e< zJcn6M0Joij5aWykR>oCr9j3yx}y*J$tao z3K_AK{4}oIlz_4YZ%6*W$-!yrAD2x@{>)$IuLDBw;6KN*(R%#%j5DA-7JKvuv7&KK z>`F46bh@9U!h62RH!gP`Y~J$qXV$YXrx%Qo$nKijl8U4ty`M3ZtIUGvb;?tl3Y7fY z{p8ClAb2>1_%`88R&Usl=9_o>g;R`FQ)|DP*j-&k{1LJAkb$!jq=S%LV^2oH>hX1x zoT4)6@&^!JvhbGtJn0k@#N}w^G;Yf#NAC%3*Zz8SGb4WG@2&>MK79>C;GwE6KkASd zedd(GkLJ>&o2}lyeN!!e*OOzqb!)=5BBed1>Ma#OV+z^&g6&42)!<};gVo&rIBCB? z2lMbaq4XY@a-hJQe5tU0QwJf9qO&E(N}w1~@OXRoN$wfUPQeUy=kDF}vvwXxMWcas zfsY?_lo(M#3?4d>(Ue1aAA()&;ni1MoRPWar9^}-3||4ac$b{62@@e93MyEzX{r>=UZ}f-gS9ET-~g#9%OEN_M)&SXd}{Hv zlUv1454Q@zLFJCndcW39)93x7uR}=F7H|o2$ZBZoAeUhBJ2)|}m%1Qx9!>ffXg<#q zd~OD{m4E*1N;aQ%_zvD+54ixVqsNcKr0oKxLGcMQh@i=SS;+t7FeA_5E$bwOE8j5R zwTepsuaJA&y>%LI-YJB>(@dV+C&554R?spjy6WW3_5#ceUm+wy{a7Q6i-%AQzad@b zzQ8?(I)S$X?o3NSK|IcL+nYNl=d7&V@N|k&n@x`=51y#$+w}-Y(5SrD%h+JWNopC+NW?aN>NAK`_ zeg6Vk$zG|MIfnvXGXrSsd-r|)wtL%W+=o!)M=j&iZ$s0Ih|B$ zNbY*SS4Wb^{0{;(bk@$XGM``%b-kBdA{pZJg0Yl!n=KrKD3Qz1jGiZGhZLrkmX->w zO9`ulGsYC zi?Y&uCa&5*_qu=FGdBHlTwn1|#=~0$7^3gRI}t3^)&+sU4^8j$QXB6%DYi@Y^&2VE zHDR)hr%}^46J;BxZ42%sZxoL%UTZVZ$4GsKnX+wOo5!*Y*RB>K(MDAt2}4-I0GHho z3L**D7S3{So9o(pvWywiI(?0j*#Q9(w*d9q%B~n)r2E`cHu=WA$HqY&sMZVP=Q)zv zFL8^`sADLi(AV8z`p3_{&9Df3toeqNczeGZ!z8Ko z1ua;o&kW5fR+W?Uidp;Gml{US^Il8!wLhl66jmhD{ZMq@lHMB=r21#ZSOdQGCs{9| zo>#Bz)1INy-nyS|X9Qpi^G!6i1eSiRudlDH)Ujye_UI*pM$*B2+02Iw z$sX%jS$n}9!w9yv^~`eg_I}AG3lhk|MW1!WK4{2z9XG70zy4jiy#4p||3%w*$8+7k z?f=t~(r`7Usi9I@R*NL5L?J7sos6WChODmC)sjleNN8ChAxUUykWm@Y9uiWS*?!Nr zuIu{Uzx(l9f84jn_i@kg@gC>vJdfizk3&vQ8u?C0NC~@byU96S|45Q>V4_bb4}WM7 zcg~*KS=KtHjw0`8;k#__IT!PL%y#{&@#~6wk)9nwlQF_tItDamQzuXMbk*tfZ13d6 zPb#-9zI=>LN?WFC?AfzWr~2LSuSM|_l6p1ZO745jB6(lLGWdO3T$aA-dv|2_EPe!s zDVGcxoAtNJOzR_WOAOOEWI7SVQ_edaC$BeItNah8t?SygE7N^|$pd_3-40qszOEH| zdD0c~S0S8sYHgm8beVPE7UL0;$A-^oh1aKfrESTxRAeHO!`lB z^oo*xR?iYf?A|&3?xw9Vru;n672|~NX!P7ARa8{eR$p3%YcoD+ykE4UOFWl?C>DF1*Z7n=ZfvS6zr%rwL z?wu?2HtmX*$6EHW=|}~p6{GS^fzHZn?{%)5boNQxf<~99W~Hr}cZ^HYb>CeVb921- zrqoJxhF4U7-(f2o9w_Qj3XLTKl@taBzL9UeIBjR`%g+8*_x)>Lfn;qelzMm(sPgBFx#@5fQRQSLPQO%s#^YwOCJS&Apce&$_mn znyIrb0t)+R@GJueWC~c0(^Lpc`0|$FC6(M&nmit%HN9oe&$5kf2f_dFaiSClZfeo->l`ADcMFoj^ww z;II)Rg0jAM-_zO(hJ$@1u85qqOv$J$?|%DPIcUG>L}4Qmterf*ShkQXFsS|Z{$c&_ zXMyGTUKDz;zZ_IhQ}yhGQl}LG6I7ftq(x=6q;(11)sld9zg{eE8p6BNxy?rm0Q}-W znPtrdPggqaOxQimy7r~wKX-uRODk>vs@I)*tl24h`<>us)PB~yW(OAwNFpq_^AGA> z4x$uc0sX-ar(F;RQ1$F5A9U`dMP~uHj4~QVWO;j5>WLF4MtEx%wsc17VH&I_$m>{- zruOlDpIl6^?mc>RG42S)O(-^(w);mBWI`Qcx1spMhr1~adck`1f3H)VS6nur#0#=} z$?LRBt)<)SK8M;rZM?n_eOFmh|HQ2EAFo`BA0ePjf3FkVe&?P2{+r7|@-KW{4pM`+ ztkD9Tw$!y)xKO<5tv3K!9}QN-{Lh{h$_O(Q)1F!RuBabc(mOMR6@}Kf0i96p?SB*2 zY2ycp$D!w}?9TAwJM6swxTR-xLyOIuCP9}+IxuHYgrVH|^~nDjX)wAUA<0U$O>vG? z+o(o+2mEE|gYmNF2*iHjJ9<1{-!S_NH)BIxY{b6*xR&MZpS95z#7H5MqJklJA+qL$hMQfLgcg6KZ+T#m?VTNZQ(hEyHzr9KN z1)KBo%~+|0KTrmU^(vQ=icQ%pF#3(!qwX(zkkbEzPy$!i}BN?+>=8F0B5w_%K%3;YwD#JKz`v)^R_(Y%t8)@nWx_kWhQtrS%^z z!2hC_aF`^0Z-%1P`(H4BbL-qtZsbZ24!AY5?R_r0b5GvQ^1UQo>iVEHkriyuK?<^} zs^-$XKg#UJsD(_q}2>m z*s*_oUp`G9`Y8SJ;~*0sALHK&2D*M-wJ#6#HVJW^Fo$&xojpQ;1KBcr@nTs7Cy`#V z{f(`(`s|`kwU%7GN=G)>Zh0rsHKpS319Io7DsNq52JsTjk$~0?GKhY%8MQc5_Kk#u z)wg%YJV|A{(PP+~%>&Awo>A+uo>r}>G}MxuAFZBSVvar;-D9GS$Z{30D?Cfw`L*lK9KLVCV*15jH+kw6S4rQod@t5Z z=jqcD=eYwjZZG-9`OQ4?H_uyRqA#Ps*fFfGyBqV z|0!$#WI*7kBC6x6aYXslgI89npN^SPep}_qg`6!PxK=RGJ6ZgFSbyZoY8}~UC*Fl@ z+#(_JbL+B7;q``9H{L`Jbm{b$%|YG8K~~`*${k#hj?l9)3wFa(aMvVM6~kT*xU<;i zKj>0A2L)q0OQEh&?ce9v0h|A@hwX2iDrcy(u7av-5XK~2Qo2GK! zezPRp4kDhas;mSHpWF*%o(Q_{Q0K~mUSo}m5Sg*7yGYmej$4P9fBZS{psPP*l zjV1yQ+gIPQmFCbCSgU$O(WTQ1D-e4Law!!xvcJ%semjH85M&Z?j-tN(6CPIrbhz~u zha4F7W80-zCt%U(j3ZQ@wURT9#aGwnPs3l=(}2j$h8}r-vgzO8H+!)xuKV#U;CCcnSaL8;SPGu0cZSo;rIcPW@ zS3~1Z{iZ8$x=M1w;IQM}>9saC6HYGNFwHlsp#i!SYURu;JmvNV#G^c|aLL@4lF)N%H@$4-v0U*IMAock!uRCc9{W)wv! zsG-YU`quU|v$4I4lDI=JlIsl2x(I{uagCc$n=E^Hmu z{QQ}dsHU@Mk+3l4;1F-z1P!$#>jEV?$~5NBSAcF+{3+XBr8bsLxcE_J^UF24bqAcU zdbAA8-?e$9jErDxMk5!|!u*l9XW}X6Z_l|$Z{MCz$=`Lgnh_ik4=XX>wBR+kYGt$Q z{7#(C+a5^88=DdB=Iv#mi@VrhK}tQxOtBOSruB zGh3PkuL`2o8lll9cxyK{681L;DTj){c0hk@>aPE#bAezPgCok8W^QjYD!lElkRLVD z*zke?8?sroQz9~+hfG*7+0u*|6l)%Hr*}PHTO4@$^bWQzcOGR*)xOLPHfu69#NCp4 zMZCO6r~Z&un5%oZ*8h}QmZ7gWwbP?#9=F6!wGP{QKt|+tVEv@xoP>v09_>u5XpDNX zhiwn;9kWDy5dgH{C*`qe^{#5>`>Lw#jkkCV@Dt&60j*+)SU)K+IM=!sQMVT92OjSb zVsH$W+SL6~2F76rfb_ba*@nm-2eU`UR2~F}(d6448k~xy46y0;S0-0Y*N!+pqjUR) z^}3kdAi3!D-#-+5Eg@~?8=C6S2f?e#G9eCbdxxZkX0k@9X$4xB$ZDyL;1R4`#E$c| zy+z4fcH+JS$d}?^rokmb!a>f8dzMzg#t{Imf&yksGg=HmJrZw*cSYPf)U#g%J#nSr zUqU06SOMkG@Y!qW%?kE@v4T?iaBl3ggzq>J=Lk}Q+Z*#EX{d$@a)4I;J$Gm0>R8Q! zdo&yQd*sGb8bFv5cN|%C%=psl-NLa%brhE56{N zf-yn5aTyc*EVFXq;_`Zd-#uL;b3-qXh3`)%xARj z`bh>6!j|F{588Z{R`_2|guc$Y8CH%S!k#z=W;ZK^TOk}U&P%`5u~<<0EqX1R{j8ax zL4$P(il3x z`1($^A@NvdKADHO($#xI&W zbe{NSiKHlK5u15vlXB6!`V{gQI@I||h;((x&sf0{9)5+W15>fKvLE~M(AE!QEK#8e z)E>%9L2lsIfEAh=$cympt0}$eBVOVa7aIL(JqweMnoMj{81F8ZjuH@M#yO0 zD7G(EMb}K}M5gTO?y%Y1?VGzbtuBgVOj3IZ;Ha-P#exR_H$)!;R=J za4itB9a@_c6YZy>&rlbD7rGPt+@AqXiDe%u4Sq?B*;J-ni-Jo6mG z-&eN(yP(!-)21m(>+%xfDSvHGwS|4i`gJIycA`*q23X`N{#T+Q&}2D)KNLv8K_1+h z;-aui>&ga1y?361?N7&3nay?kvA>(yo>hE1(r=;-*{4BMzmAVXtERZiKz4so?Z$z9 z`c#!gMAJjJe;CV9h%AjmS{L`!ee=~D_d4E&{ryZHa@hW-0e;=y`+Ye3hJ%f<^IyFN z|C*%)i?@I_tlc<)r&DHzf@W|8t$p}fhpfT&pRFDnVi3jw7cU(V5s_zI3!YH^;|Htt zo6kJZ=~B?{s*C(%FOO-XT6kn+3pzqXw0_fud)?n-Vi^iB@>{Z@+Kb*OnKqA^JaOe) z08Dt;OP5A_XuUYSD1RvwlllnAhuevLKg{2B0|6Ed;J2LM{nUvK8!lO;R+bdJhp13RdDO}$-8Y=q?3wYh#j zIryKtK{h{dD)*|d-!1C}x>K;e$K*V}oQZvH-p+XXW~7RNTBEfI6d+v$hl9`0q^I*b z{oDmO1T$USw*efRj2l{`!ghUb=`msn{*P)2T|bA5J}93srQ&V*{fGTmNjQ#qpJ03O z{Tc-APd(dQgR^wocn51M&f-HA(!8uF)pd@{2JWg^MbG7diW}^Mo>@Y_lB62n*fV0P zTD6#k%_@uK75=+c+x2jzo5F&bERX(f&T~U7?8{q zACwV-ev$cr@s*WXPMNpl7n@Gpl0JJ7S69n?Jn65~SsL#Ya$5dK*5=Ydm|so@G+)0U z^qD2kQsOS%+b5)_#cWTUt*i`53<`DkZ;?t5(Hf~hUDq8u;_@e1;a&koibgf@`4zu{ z8_|0Xe%lW+r;)IJl&ytp^4Tdv>%%~7<^4VV^n_ELZ4%)C5rEAlW8 zPF@x#$vcMtk+)y*w^AY~=nhTaox5E+ecrxDOY7kH>o#K2YB3xj2zMLR;-7XgOl8HP z$NSh)!9W!$LB88+r^+>$R^4YNdFg5GEx~q|gd%!L*5vthn8;fc$fBE+9Hj{%; zD4y5D3d|LUR6U1j7KR?AA-}ki$qo`KbdRA%m`C%c074N~UYln^zcg&X0IeU}0;g^4 z?tx;{pS>a;RSq+dTpgXK+~ltdY7W-Z&cKU43og6eqe^gd>e?$VHg*LPhN13-eK5E= za)WC1=V82*%&u=(M3ac~0FOU8kZ)WRI*2`=)2uVu zPd_Wn|91N20u`Sw*zmH#*s@*jNr@9#{)uh!f{*Xkv!}NZeX;`tf` z?k3Od*F7c-Cd3~I9cssYZf)-~)nB|_B{5(Vk_28qxEvjcwiG`pkj|M+)+C0=x}zfZ zV0oXdYk)8S&uP9-3knjTsJadQ|I%(Wx%~Ps)^2!aML{BfHF%#ryBfja1F;5G z3j`zX`Vq87j7$hGXqUug;CD&$Kp;-58#nL*9I3*7Vj2F>FpgTe9BB%v*tDz?2kx=q zKFG)P%9xom8zD1T)uY0o+=mT(KEr|5LFn-tPNq#7adA5d#h48+3kBPc8Z+iHVz`kf zg*-H~hOMsmom|?!waPIVFY=mDh}T0SbmY9Rx3%?^)fwS_&BT=M6w`_+Q%+cYSe^8I zR5Y-Z(v++DN1ncVl_Xffidew5$o5(4xI=X}nIw@LdU!71bmJHrA@<C@Pq#R~N_qTOV0soG%n9~bUtMF>l&y-%E@ z^y(uilH4yFjIXZXJx2s)@n?DS@KVCY+&(s}4t;cG&d8A;F|CYPpv@gX@=Z3&&QaO5 z-Ur`>8`+F?Ut2$D#>|9mX1;@6)^%_57}ys18G@*rMC?SrTHzk#HN1MC+M;yruRHcf zuf{)U(24JW(YyNzpbn%Md4pc-z>17O_XT#rrhf^4232YI+f$DgaDaA`5hlimyqkTh z+FmAA-;|B2b$vg!v-s!}2ZnYUIW8mbqd~v^bFmuI9Pbcm8fFw`8-2mSCNl4qU0V6h z^8EaWcxxM@m0Rzc>ql5_J3B&7uIY@sbLJ1vhBr?33X8;TIy${|{n`DmO|sdar#zYM z>uXOsSJasaEKzV4PMbPaUt4E{J)(;(Pqb?=$ zp;3AN0G#d6p~K2aD$}RebGr9j=y~hqybPs?3_(SWLo4K)pco;UJy}_EsKd~?4whaM zmn7@q%&h(CyTN`VgI%eQiIHzuk9`CILKd;b@;b81wq=P9YgPfb(Uy~6QqteXCu z0|_I`^!BgYyje$Ff5zas$f1Zt#^;@ka2Bd2+7*=a3OC7emu%Ej%T1@i^&9Nn`d55g z+vJH8djKMC-8!Ad0NtS3J0&>TvuDnb#UbnN5F)6ulc=YL04N z;0;x27^ju{SqzB#Zy*ZLh|3HHEVJh=~$Up{_r^>H{d(kog*H(EB1fP zY4Brx#@-&-tcq~=M#s-noz&g|2o~pgf>U0(7k7JwDuHa+y_Vj2bLUPFJ!H)Mi329I zrmvZT6BnjTU1Cg~0QN}E<{aWudt40n*3a z;5lh`8$orES0p%>_RW!8@}$nz=a7rV_(3H_4IjW(vL-X|Nl*y1IjBFGoBg zVEU*(02-nF!lI|-_AGn3204%eFt}J}gU!NoZkL5{wW^bZ#$y0jpamfv#Lg~y##H$> zh>T2jczk^e0R`!L`NKc|m0EOlU)nlBaf;j%AbrLOLo)6wmULA0j}s1t`Q z?sFHOycjoW(v?YuS1gG%5w8E4&}>kr>1hu^DC2gvt1Ou*Uiz-7YISD2Z}d4}1K zxC_Gq()ak0HGrEwqZ54AN7YU|kF-!b5rZE+ifa^47y$E{*uo>DMv_KSz56yj8s|a5Xp4!l5u-e^*6B^D^$%(nVpT&d=V{hd?zn1;s5`#c|Uac@aVPiHD^F$Pqhub zsj&oRFT#n&Y`I}A1|Y18VY=DIO% zRA>1)C%sp7mQOGX5F2toVJ2{-Z(ZB3+=5W;ekId_xd95}#vOX}Y0%)odr6C5aYLu{ zMFt9~YV|*-`A+0v!@D`7_61wc4tx3(9+NqT^REzsSjz4R5RO zsh|lOivr*vym&;tO>gHd|~x~@o&0Mrwm;_9`x`}E(NYV zqW;xTDB+9eE%8sI{$99v5fqRedqb5^78X{mOd}WT4Y(mshPcde~q7@bovh&&2jy3)!M}JxrK#AQR?cvhWzjk zBit_-COub~Td9zRJo%n`H=+L7r)38`blq*$Fh0z{#`TrSh0Hdw`PC)SbqmD(=A1OD zyIHp)vJb)Qo@NHQjdEhS9h72dgG`=6Z}AHM?l7>H+?b#D7J z?Qq;BY^Ob9se-6)2}3r2`9Y0 z=MUA%Sg|VACBV;5+kEkhI2P9y9Fuxx_I*I8!U_-}UmYzEht8QH3>_dcckj*(GY^~< zdUr;7BDMQzz$}x35z)U$BxC_xB*(04Zem6 zT6xlLiCvPHQ)UPf&bw>iI7g_e2Haia1Qn1OxIP&?cI61kW^y(movf3`dUOa{g%s+u zNgp{~t>^A>GsKrC?Tno5g}JcGGFf>syQZK7sq1I_WV)Lj1!m=ybg0JUByIv9ZDpLL z{@S&{QGGk)O#LPya*R?Pa{tpmUL9X&{Ud7#`th$qvyB%s4TGv8r=D98+N8(5Yo_O*KW>Rc%wqMnLEKyB2h zc|0YeuMz`}DiQW3eA9Zyabyvy z?qB;3=YKUVHUIj2{CE7rgSVE5f)21)f`2yFcz`>nPW91Ph|m!50w-wNh0;pKT_@7# zjEwrrT!PLSc~Y-}=L+Rv$Ri<%i^tmi1Q-DN>^)61!vMdoj?I`{kc8P*i4~;`HF$AS-v$t0u zTSLI*aNpZHeMn=VaK(dqs#kPxjNk|yd2(x_aQ-!}<^Tx5#SkQ--C~thcE*!S2Bo0$ zc;J4mq&=;qZkJXdSVuctQ^%?kbK*M^9~zBT&GkNWCj99Qeu$G=pd>iZ1)r_FErt&T zJ>HZlId{|>in{V9cvmtN1n_1jw=fMZ8w7_Y_zC5_Vqf>dG_}#AH8*BLlo~yJRap3B znfJocY=`HrZycbHRT+D%3seE6U?p3Y$gLgbxj=mF6e%aQ)o*t7h37>&hpW4on3&;c8TTg}n3h*; zlE^(pF09NiDcZbn#GrqqJfWM)oL0X_Y;&Cu!0vt_Cm|oXQQ}!z5crYsS-Em z!p%2m5TRx-U2k5zXc4xZC0nPHU11ormgmdDBLDTPE*`j{%)^M;tYGWMWKpG(_sX@yMXd7Dnw5$pR6Wm&e_73*nEn-Wj z3V*L{H_oF;Kts-GKq+M4vv@xGBa}XO?i~M>xhV+o* ztsQDKQ5ze|jdADsA@|oiz6R+7=4-_#VWe^gHL)<|9dcc`VD**j!}UV47B4G^!+$-B z-&j*mayuQ*vX}$mUfJhnyugFeQIKr>YOJ;4;__AYT*REE?5~Pa6?WgzNlQ!2>xrVW z@|EsZwyO}TiQFvUIQ=pN4F^)*S+kN51kyrq9bchhEGpu4y99T1E2~$K1710Mz?B{1 zl^wMF!oAq=1D5()DTvbP5<>lxi|0__*n48knC$2T@qoR}s5bBLHOv5HOhA4w@@zxf z)Iztd7PvHE(v>ayzEn3Li^?5SC|pesynMgPHY>0a-kY-j4TB>Hj@M@EJ%54hrLLH?#AZ9{RI0Z5m!CB_LQ!v zDqb9(ayi{xK|w*;Us#jy{xxR~xvaNt`Q88Uv%yK(KeB423LhiRmcDWYTMxX6jW85K z{1#E?7#L8Vz`J>hd11{93Ol&Du>7|!#)qf^U!BI)ZJc#zl!a=RxB{)Yu`$-67prdd z;F|~`%5c4`_44D^>7-rg&A5>-46gXXG_B69c))MX(WZpk4pntj zds+!E}gZ6ObNz#q^I+FI0x=HAScmFV4IuKGJVme*KutFly*Gdq7;1gDXVIU*Z z_hoWQsF!f^zd`J>C{q*gG<458k;tq|nZlCRWyOou2!%Vb`9%)nJbxRadA>lTd^+J@ zp}v$6_Er}$F=G>x1zK8VGymYZfyE)J115+tZ1@$Vf#AHzTr2UW@&1*6MDqfV5`B1r zQ#gNI#Tm|SxBR=AnFQpD;`VT)-*#9Keq8h*F?40Tsh(M5U#qzRrYSPF`zL~A_thPMzL(2B)jZ;*75Hf{V;l|IfZsE90#xQvD=ZKm3N$^>1oeq0PUA68KQ4tPj!r0G_U-H7pSk7XG46Yf z$l642CxQ)Th1F;W3(yM0EdWvAMDcFpN#O8iPbtqRq^%*_HKn;~aeImfLiC|9)_8d6jf;zkSsP=TnJ7qnbx}Vxds5)3I0z6I3!l`@ zm|0nG+@%EI$}l03t%x%Ph3f4zkc6zPEqH-Sec^j+23ecb4+p{wfiYoABD7qS$3dTK zV|atLbBw{eK#Fh!fVr3h2QecR76PTneL}6Vm&GkcV-%}=u^h!0EEPTIN-w+%<&lLn zd&17NhrwD{wExk5C&R+5I0|Pnwr%>%jo|M)_iOcJGF6bdtXg%RrQP%*ls}OyF`pcG zi~fK;TN17)E3aSQE=}OV74^)G`Z?zyUZ)5s==?;Y#tQ;p+fw!&LbFZi?csoKi_z%dzIwxTSgdEc#NG|4$??fsvdY&CQcSD~b8HQX$SH($BB z61-wNz{Zg_aMe(H?rGv54HBZ|QTVjs8NGYM9}1x$*mFez~_{ zTG)yWd0nm|p<|@V{Y2%#Xf*t|(Jlocwmd6v4v7$B`Ya-w`@c>hkUM z;>z8$HW|f^yLbQGefy#95T3qqooJeESs*_LuNfSC#7+w&2A8y{Kou~&oLmef$VXIn z{|qYj+U$cAb&Aw6Fo>@+|A-8Iic`QBq}Dqf9F(R`wW=z=k1X}UnkiB}wC-oQC~n`T zS_!p&&2%E%DsLI|vCW~}z+ToHvb6D?UZ{E$Pa3aFJuAA>=WX38xF)hc3HG1vLmiW4 zN!!AL0?Q5iJrM&Bvjwld(Fxj@KNexzEzWrUgYIby(r~T_n~~?!>p8l$AXy;JK$<}p z1eM5SLO#$%GM_0I?(8U1zyDZN)`z#6NJZO`RLei4rHhjH?!qd5!?e;4LiTZR@P|~C zhV&g=Wmr;&iXxoe@EW)p7Q1D@?_oDIJp8%CNHcxk3XaBZGBCKH%2$7M#}0?oBiOAA zGbUpHoVG4+SFvvV?q$!muQ$T@j#nB_4YPRdc|NNg^ww`T4g&_$2)>q*So)gBn=t06 zShp`UM?UR;{*|g|N(2Ye4s~@mt|Ip;IayWdK`vXtMi0qDeU_2Ip4b|^(Xr7CbgnA| zWkpoT@&4+|YZO;?;w9$6rM$6cg^epp(}i}dRgo@9p0go#*^psMgheUG>=-*$-a$W~f zkzf8c@m`T>7(j0zrBr4OU68gsl>QVYB}Q*C{ zHYjht(jB-Rmi@}(sQ#AC533r`6*xJ#=4JP?Hq9!sG-;ciBRkbs$Gd{c7+K&5`P;xa z!|X3e^6`}tMBM8f4os54_4_@-0$TIjfLpVNdEVN#ixPx+PEGx}b3^F2KWr`Y?k`f0 z5VUJcQF?L8C|Ku)iUmH%S$Q)YGj;=9uy`U>C$H=#1KJF%Pe@L|VaxgEf~;xglaQh7 zrby9Qm9>Tk4$ZxGu7mns%!ngQnz_Ny9K;=(qT5bGkS85-ByC;Tv0t^dESpb7D|614 zmYT(W{Wp5q9IaiV1|4kUeBO=i0PEmiSDJui@^y=MvskZioR?VJcaSl9n;)J@qQOU*=g4+}R zMNAhQK4J4e&u^t7gbkp~?{Na?rduW>_^w5){KScv(C|zLoBCQV{6RfG)~3B=$uM^1 zNvW!}VYfEG#`vkR|G;Jb@&-DKwoM2B(zEmy5AR8oYC6O#F@$2hSC^+*=nz_kbZRk7va6?-A#2y^ed` zB{%uKblp1Jx>5r1lsP0(m|ZT}*bMqC>|4;~3JOlBXa=Ag zWVC5tE>rvT`pp|^Mm_rloKdrBOIb-k>(5a~+N*6=W=D~wsvNCMftPwmedS*BRHJF*DyOL-n?fGaH zKa8u*Q6XdZzwooo1CQJTQ z$mrppG{Z&>Q*(m3xyHF}HRI(e<)fShGiS|$bv-|OkHN^31eu_NR**Wz{5D}@|6#+< z`}mlyTzQ+>N#ikW4)v4^?#j5Se^D~iS7AZQV|HP?$xxrjntGg>DFmJG+jo~wsJQv+ z?TwU9cmtBCj(d8lC@EFE2=PphwBNR^v?x9zeNL%uSF1pUyvgWdSY{zXf{}-%^bzjF ztQ_vHCmnJ4ynkJi+n0eWi&w2IQM=IVq4l42U(gBiNFpWpz>*(%))>cr`ts#rNi`Nr z>GeGRlb2uL0J+ORA_Qr3_+yh~Hhq)G5@_(x>+Ii8@qG%m@IVM%`~281a%W~nhLG;X zGl+`PyY=&rACo1;;ep0^J;4Qeu{fDzhi(g-b(t0uhMio>YK!D{I;uU3Ei7b@rokef zO0Zm=L=fd)ch}%qYh!90b^_a(y2?uZ9$N{n7_MsLHO%4tuz>?ZP9GEaD&s7CBr-E~ zi@Q1dkepGvq6F`aPnCwfoAxW~|o;>PA+-mHu>OmS*G_5i)3l?ah4Oxk6aRy!_U{J_0q$9gmfY~ktEyLHI+U4UGGVZ2NyiF#4>2xiM& zGEFIiGjq}9=fvBPLAbPZspG4;MI}15N1ykILx6;fO#U>Lid`M#S8tu~@O_jhXUev1 z+l1ZA%uIRmKwBCem>&7rsl+jO|9W@k?Ah4D&D2WNW`Yuw#zqJT<+v17R#m>8)CY5J z8REA=BkD*$27Sd=d*SEu=%^^6MbaDBj90|PEuXb|@=)WtM1tt)@BRCUcRn(eBoXXi zAAOqI5BM!mlJ3eNdJ}a_P!TwEU$|F5Z}<q7U>UT9?@6WduxNGMXOXmL+B$lfhWdJ^pLe^`eEFtJiS3VJZe>OLCT#+0 z8VdS5x&vffxnM)ym0%S2w{9a_%zUmqtd^UbC{1CQ?iiMzuv?^To$WpJ8BHt~!X|P0 z(X?qQtjjOo(?TPMAlzsgH0;G znJdEEt^Hy!G2?wTa(fzG@Ic|q8i!7xn)&EH%=hUVuK!j7=&I-y{x3p+WhHaQH@=>0 z_T_5S>bQc(=ISTYlTEADI*R1?r>)aHKf8f#lQ#$-5CNZ`<0{xevGcy8@aSv6WqMM? zP^?qXrlrz*GjZhZ0n(2fjsy8+!I^|=p1Wai&sE(VE?@W<9Bpu{TkqaHfJ+uG z)Z4rgBYgVK;??D_jC-cO|NcD%cGi38z`=vdKYpwteRjMu=o(Jx6J&pW$sC*}QVVQr zZm`!l#N5i{6}D{|?^mQCm{=8?zPXzW245H)FznBy%BJV#C(hqdbNn%`q)V3M-!@Dh z&_>FxxJ(s>GAtagyuJHbZf@LNuXvhCUhkKMg?Lb86czc&xfu33@k;6+;=X4)asOOx zQ}}AS$bR?njZxZ^H6(NV6D5m*dY86O&cE#`a_gnPt7~Yp*YA*Q>Qc5vtPoYgI8Oye zbO>#KW9unVP$0l60V_TG7f~%uwB>NH$iy6(DcQPdNkoWE zL#PCIb-51CcH@Q(!iqDp01P3JlGZGqI*0WL)U(EhhHMrxx~`J->Pg^nQ;nitf9?mXtt$DV9tJcC51*^SmfNs1IB~yDvtmOax3*`lW5Xsf%I%S=5CMgz% zF3s6#H79F|(vL%Z(h_z0EqJYeVfv`!Y#4vpCfF}v!3W8BHbYCw-}4_Wz{c1@5F3WG zp@@l}e;%3J>;?Ei&J|ULT65IX)1=4l*5*X6EmQH19e7(LrH{Sv<^Cm4Jq+DKbp0(D zm*`B(%Y~zevS7Vp0HM8JPpG5RnAS>9S3L!ty7B^H_awJC%;5@yhkVbZ@Pi1jj3)_5 zreuy^!ot#>cNy%*J9JjsBV=xK^o05_H5r;qZ!em7{_I)ewiymLZ`knQQzdW(8MUER z2h4yKIrsXnpyRja&`3K&nW6+BAf+ z=@%&m>$QV`TB<*S9E-2MoD6-B%v18PCg#Ze`P$LmG|un3DBgZA_v_=#Uuq6hvZkm{ z*p65OG{)ef2KD>5+Zaol9~p0NE$PYL^St~WD!b;cU@G~_B=+6WtcCGVpPF#yOnzL1 zIiM4N84as>wEf&$2Aghhm>xTNG{Qb~u;pyvcv}6-(mL6*Lf6hn7(-9}KRHe}NB)$X zplGO-0vQ^p<@73{vy3D}k!G*vNbXZ+aU+5P1%|~vG|XV605oAk#;-Z?AC~%fQTbC& zkh!v6I3=po|G6SZ3p1vh83+;32m&& zkfXyuh+qHc9%;mGCcZtK48UZd`U?1lpj6-REDM-E;AY>R-~`i2I=fnlvX!3aPsvPirg!U+VF_C3|G6e+` z3}%MyB$P@A4;%m!#C!H9FPv~ow3Mf@tPI<6;b=29GAf-p z%1xOiHQ~dkUqonB1(eFa0RWZg(F1BJqtqeqGM?f#PNq_V7Im-lo zsrPWCF(s5X{4}7$1L;o&g{%#BhQ$y*WzY{!js#Y3c$E5R2&;N)?sf!AFdy3`=K>|F z{Vz#DCnR8q6awD0b?ZjT=I+}G^7fA~;NM7-f7K7RcL5vr|M@4lZ8R{s-{0QGZa2`d z1mC!!%%sZ1M;{p)8JXvxCDH`|7k-uciYaUExY6^$z%XeEfm}T7@SQYkx3VOHtkztG z1QD1NtxsV3`sZQ# z{=Ub6xS4{+Kz%Y!{3J$}#h@PN8B)E%9YbjH7P%-~h5+k~ z;{3!`;syDKm$iERwH{N)szt)}7{|*5n$iSMCv1ws_;KIk{1;t+TN7X< zIB2C4U^(oJmsc!^JSuhG@8{1AG#0YtKqg6U*bCYmT~Al0ODyBXWoWG@4#;>#3*-Wf z?bysM1+prwpqO%lmwfs}(QGGZlOXHJbINn7l&%^+b4TdDeN}k_V2fHAWxiGlPz;}~ zI2AJ3dO1Ol$Wxzv;z|#M^2r_ zNGdgG5HH`e^}-z+%zP$5(Kl=dpUq&$fKR@xXU}a*Bx;7XE(rdBRDo8D2bx07zg)Xk z#%KfC8p4kDCv@OqJ*3o)f{(z+AR{Oh5(osd^J&uzCK`6`*um?Gp>OZN?A|?kTpE@k zA+K==PA9z{|dhkkZ?IY<$N&r%HTQ&Qd5EJeN=W%ob%VbRaT7i~P&gKX0E8HapSNg?KXgq4Q z(W-3}EO_eSh&aFcYCO%%49m7FO9f3G33@(kwZ6X0fTLk@0qx2VN0=Zt7iOZ>WC?=b zP@bpIx^ieTmr(P>hHnbWrhd0B1%%>P(}eH^3IP{{=ezdhj{DY7X>00*797opAof^i zZJqw967vJDEEa;01sa}{5iojqC@Cmh3>O#Mf42m4LZ}E3+{|Q$ptjHn33=wR<^~2; zJb(H|Qyt3AOOEtqiqg-T;? zWIV|EWcTJ4_|V0d8XLb?RN(l%+_9OS1A^KT#}x6CCb=+HPo7u_SGJxC#-n8L?h?`$ zS+7MEmM|mq4?&LvUQmbL5Lp<~Zf4DQT+dyl&*$^bhaN17Po|Cl(y-_w^p223ALE0z zqm<+Wx=X^wV@SdmIB)UxH*enHsxa4+6+Gf=0kCIw?`mnno*FIC3ObxQ;y*$Xj#xXwvGH zwyqW&h(4#Lo=F51ol}7{n^=sx=Li2?m2{SWunW`*`8vtpJsqto8 zTcci;7%iMWb!udrkxTD|sFc(99v8NIoGZsJNY>x+{k89{<=sSP<8z}T4d}N(geaL& z6g>`bz(ZhxedY=`Vhk+C%$9;3`XU2h=gJkWi39fT-5YHde`FZe zYpj@S*nxYK`qU#?b^LfMUUaq%K|l*PH?%ZR2nS8uu<(;g-yl`IPaG*1ctEzhNpL_w z1?Hx$W>=u5o+pDK;3aA|-ConL5n;N>vv<#)LQ#{z3Y&VVw$i}x@|-icn3&igC<3;>uH|2vCvKlVLK1~P zUwXX_hZ%KJe9IvE{axzhXbm`QRHm9Eg-}S`9_M>9mo#!3?pE}Y)h)vaR5gWgQ&wTr z@>co_jTs#rBHfbWVg?B?2)%;YD8OP}+gfYtIkMo=!#6EW2CKw;n`Xh4{7iMt!$ z=P*K}`6KmRT-0Pnql?P(NILKDpG2NK?;Ph(4eE&4xVT>(=sgowh?miIBLJ zUt|+_c=S|#56&eZ=>11U&u7<^PL_YH>*;y;@C+}%If-2cE9%(C?ricYU~d;q_~DMmW=FTdW2{6BBqD#DYNH?^jonT{59nVh%5ytNsx z3ZMuXH5_jn%0-5U6Gely08#6=)XzTpguplK=_cAn23gzi$WASrNO-BrGSxVwIg$`; zU_d*4{gW{<+bG}3w{K&0^agXCmWGc{;P#8htrQ^vi&!N%=s0vm1QTRWG)q{)>p8R% zkU&a5S)=hdj?`xFH0@ zOq7!w=??C)V51YFA_(0ug>#0B!E4ZR9sIFrP>+LHDNElnF78Ysn}Eb+=EhBMm$c5Z zcc|Xugt&~Gp~THsP>8T!-%SSefv+OXj*8uwg~^7Zu%ac^pySqM!!rFb&>AwJy?dL^ z=t`=qY)y@5^<#w}+J!FV=E_MzXr$$#$qs40z z?=($EwwjVgA#Wq&e5+#*zfRUBs{P4Wwy`0V%utZO@#T0V)1qsaE&_SYR7}l6hp_;) zl#m2>?%eCDC!k5H2&vLfkEey@@OR-@&nOYul2qh8+FRSTv2<*i@euQp_MaB%rapr8 ziQb&Qc%o@}4vX{}GJQxEw;0{&NdP?FPrBIh(LF6AxR)2f?szESHs=efGzv-iyucY} zByokQ`1DCAm}6ySmCKcqh3OLcIehKxjp?U(t5n|gox68`k|@cEebB^hpR^SE$IzBBXdU+S6a=OI>sthMSKb2dsLwz|j#w^Ap3Qi3 zZS6dIZayQw3s;~$oUar#E|2mH1+mVZf&P_Vqz4bZ*BsSoi7nx+CGCJf(G@{BSe9o4 zSI?EB~=>@S+6mpN8>Xxkj|cQ7)}duB$d`ye%ZozH@EG-yJjV)uOKGbD2Pdz zgX3Y84*%F6dIPh z*J+4F&TT(F*ka9k%)%dT&4M*myNuWI4!yOw3>XmRT;nE8Uq|n1OKe1$k@;#iTPgRG5ZO;7UfCz0K*x9>6I;eNr;QDi7~A;-?L2qtzq12Q*YJ+ zX!gWqSO|YEdBsFn| zdsLaHPrnHw4Gws(tBG-OJAlI7Pw?8DJ$+j7$;@EYJ9`RlSa6>?>!`SFFPIEOTO+JO zQ0m_B1JcqqQuDy3;r_`#Y? zG+(yiJ!+S)SI&R^dN!vKs}@VH>BHNR5^D@1TCN#)VG<@RP9S(6GP?9+gr`gJADLtv zR5>_<>TSPVu5?pZrHPwtzkZmr)PBdlkwdp!|H5aqH1FJTE?pVyO4hjYSaApJetH=ANtk#)ShtOa068yx^!@X3X%8Mg zyl#4x+HOAfLVt(C{M_8HqXTyyy1Rz55z!RkvQzK=_AQrdVkP|C+kd}Dj~+b;wWcj7 za4#n-CIx9L?XmURwa?rm4&3M-AMrLOF0L6lC%5xe$LulQ+I{=z@ zf1{7;d3M_WRUhRMNs0eI>Z8t91mMtV9fJbsk%^(9)2}zX5G8C@eg!6?s8~bsKJr(Z z^z&IR&C@@n(uX~w^|7*A)Z&L*d5K^D*t{=kxfVzJEi-7|Iq&Tstl(s4|KM#cn!P@G zKz4REK(}ymqx>bonU@|mQ74Fp;?oah9}11CXQNltFzSVY+PxtxGs z4)~umgfK)rBbYo2;+fRjUMVq06 zyhozO$oN$vJVQ6bT>Xti)Y{2gs+=|3R0fHH-kfULrRgm;MOnGEAhosl$B%b$zL$Ny9|?JABQz@L^$c|Ier-K;tDUoe|74# zpSIu9G#^wF+_ANs_G9<#xBNH=nAq#rQGP5C7Xt0-Nvlkze!%68iiFDdI}E-h;=ZuSZKm`CYeW%@opJdk&spH;4|J zo|Cl@L`d`=`0`%dUe0F3t(_H6um7k;*e|D&!F?i>>)Oa)DeC&tyR&q+^ELT3m4YGnM@pD!>hqtGl9YXc#*fG&}N`c z-GoAoYjni5>enwaR*pQf*Zwb*FZClN{=Y=N)NjuHb6k)&Z;DglTeia`9eIK(1XOp? z=spfy)ERrKF5TV;wBvq)+uWE>>XEQJG_R`cZ@rkndLQf7t5<%?i5D&dbDIB*czx7= z{qSeiy2h7+hv&_OT!=KWwXG0vuf<;Q@q+ZH{8*5!Mn!lC2skQf2D$}hxx$0(e{rYx z#VyC93mU`L8iIoN8%$w$P8+p-m?stzL8eFa_#6}@gH-F)N$5lJ_ms^GJK@8sn#=~e zJj#A}RFtt>n79ISdePCWql{@QQkv8l$M5VtO(Ei znCp>40M=9s0Cx8ig1zXGOXqLv3e-vF?jyr2+t1Lfx(8Um8mng0S zMB6B>V?lB+Ig?Z@jObC;#oP?*v;RhiUrA--c_{@$NdOSeuIb!`kdRHWYUY-f<77UA ztD8NfV#0jsdpVE4K{Q>O)L~5nESW!5N_7WbSSn7!RhY>^;3e?YpyT7?6NDqCsVP8VmtzKqU&>adg07&F#ccH63_c6Li*|;;E)-Zu=aY3Ap1C#VRftE z`v4N6#u_%p2@9+-5B<{P)WH9^09`wY{rreoM2PhA)vH(AlzW$Xqtof9v*Lwk-#&d} zA&5e2Fj^xhoHO75Oz@1$GB!!cRr!y{wdB5UMf*sVPuYziWO6UWF$&p&VH+?}U# zE?IfaBuS7oQf_T{*bo{&x8~|wvR$H6-K_1Obg$~!&dSZzz!~jbI+SvV<=}NHw^?F7 zIt>*T)L1{m=RMgN)!kB6j>N(F1Tubo;`-jbbJ$X|4zu7@Eq)Ko7sKU&tGzDv7sN!Q z_t-^_967(aA@@}b^bT4C!=Wb>Q3wIW;;Z&~V^!hM5nnZRGOy>Z-v&Xste#@A|3TxS zUd)>N>3~wi?HcL_2^jo&67>8Nx-0M%J(Yo<(>zI3UOi1CPSQEL<9#JS>_+2Q1QvY{ z!MASaxi>$LG%6(Y7`;3}cG~Y*7utiObxme>62-CIyitqA^LW2)-@ftNLVT%9OG>gy zRZO3!T5Qg~8s@mkiC8Y%r%za}GkarobU~c{#~H*5u={_UK{S<>=frON&l*Gt+VbZo z9!W{flQMcC{J->~IrIMi=tU6|#`tex zr3?%)XjU{PJF!l*UVxb(PqqADV4G6`m+O*LykU_5|wyi zbCcI`@;NO3Was*Izzl$O_2qMwe81GywWFH_o#yAmUGMN?yW-~3&^*3sEnLCR@LF<< zyw`pi@XBEM*F$t@Y*#DD&$r!`5;gURiq8}_w|*{@0@PXbs7HSpnTv4&ba#rq7PM8t zn37b6&%uZffGrt0$4<{XAKMcWn@#!)lp9^i-#mxPHBhWEC<)7r{PGWD6)#Z%eIDz_ z1mBS$2qzxYd4+cHH210YvY{ygv!y(LLBsA-{jXFnc)m4-c-If+(ob*vFe6xM4RW%r zSw%q1;$~kCD<1@@$e#eh^mZvLP1Td}a0&4De+xHCriUTBRla(g3n=EhQQQ40+~WDy zn|@FjRv0R5C?5Lceb;Wvz_rW2N^TtK3lp+ymoEB~1Zhp8+NU_ib@l@E0YJFDIbS$e4!IF(TahK+=mvl zo0=#hxQW|;E3R9&#vuDHLjc@b9;uUl5CM5}6zzo9~ z^iw^09C3L*HEizA^#l9%eQ2(_dh#Vl$o99Hn{>Q~o|y?dU;UV1+jng9vRirIfpW6t z_`1Or@TFmGHF*;+*zD~OK<9yY1anV2m(VYq3pIVpu$>KKw(Oz`-?(w?PG#F1?_*2n76-U&!XNv4gcSs7`x~r0Up-Fpvv`<2YI`s zcT0N*`-qYA@Vgr-|8BVLsc>pX)rC7I*~8TuhKWS_G=jwOdSE9dEf<>cql(kNb8Hgz zx~~??y`HVjTsF5_7B)82V6^W*waK*c!_rUN^=ABw7cH!r>0$c1J$2qk%Y9w;=u4ay z-#oDnBm(;U-^kM4WVe*|x}Y}s%hdU!_`TVr!*>W^{(w~qYJ=6Qsidwlx#(~HG;s6S zziel5)M#-2u>t5n0|S+czW~apVKpqEIwD&)*gQok?(KA+P)%%=|8;i={aLzI14&P3 zajClwl_#(7*n0*VbV+-xQ^>X@Rv#XvZjnsh@8c2aG4!vl^Zx2H@Azep?v8y_?6w@b zo?P|y`u=!Xqa;bA6GJ5@j9z(V;fbN6lizmH(bYXK`@_EN_X+7vuf2Y_l^!fSmvt^c z?(8YoFaH=?tH!{1K}YMgfB&|_C;a{0n`&v?p7TiYHTW}bTDIB0)EtpWY8*{4{P46i zENRpc2MyZKo)=*lMY9K<-;P@E&*Zt;P5;N=fS=7(7TE9T&8}asrQ(|5{kU{1;>8F^ zxWW*OH31Ph&6AgA`~T11o5v)aK*XOm!>g5)#DrDTyAVhUj!Z3_XZq>;S<($7{&U6< zbv}LZ;wK^n;hrW0{`TV$uK{W)4jF;Cof%kR+0q)^@%;J6RxrmP)NiJyFSHh~VtML! zvDQxi@~uRv+6#tAOLoz5w-Ni(@2EUe*N&HJwop-bynd}joVKlZWOQ_O&|>W^*!cbP z|9@L_L4Py>1ZHqm09V^IGh$UCDl3B~@sF;7WpL z>Xr|UZZ4rVrN|uJzlbz317!BAGJN>sbO|$5>!~HcZ)ozN+cpb((0@HatOCoBn$&sM zE~w)1uD2P(^W~uJ$9Eb*Z0kwoiaz>DZ3-+7A@(+R7;+~A+s&s?O}WKT*=*^K9k(db zASb|jZ2wa=5!phX07nM}y(iKWrGa6JmQLRn()B+MkGbl%#>QgqH^G7hk<35cydRiX zL)xNeqaEO`)!dMJ@RB3^&%;sGwdxq2IjBt)N;ydHTx=4x+c~*kyZ-ZY5gqV8b7lbQ zskRrL93=kpd%uTgolNS>egFH{oTYAgfwu?{Ewjef z0&esDrQIAl|4TXIkW#=qbKe_Rt^}|d3Cz1gf_{;I zA%LQ&#S0jrRP!u%A5e8b0YF}eqeqa?sJ>@u8QL>|8pdilzLjF8sfF49yhEuy4w}ip zMWZ^tr5Q7tIgtE%K6>>DK0LXfno&VP!D#xzA=3Ot&A{`-NoOED`e^>(yrHpIu8^Pk zP^`e+8f_937-)LWISke^inWm=^B$!ah+*h}IkKjvreG4!sE4}1Xa;jwm>LiS#|$4H zM}>O-Z*hm-4M`oaxJdOZCs2NX)t?-J^>TyYOCs8+oLNJd*=ES4^#7xFNhs{F%KB zf^;p2@COBB84{)sfC7Op|HH1G5{KvE;U>>Ex@H5`9M79~(Z-3gZwpA_=EvWmmawAY z1|83YHMgT}oFrVja3)(8LZs?-u*}&8p4g_%ny~{WKw!lSN`9eccZ{fo3tQs!)#@zE(I+G3z z>M<%7R_N*Ke*5vGV>`k9ZcwU&HT|^i$H)844?pbfeRH<(pgkO^uh|J`4vJ)NPk1W& zP}y)D1}?Ze)_J3b59cR9ArLGtOz#;P8z-bUJuz3M6XO}W?#?XvLU`5g%^|673K-%A zX1eOy>XiltIeB?$_OQgsFYLDybKMPbp1}_^HmE1K-SUbVjNQd_xXO*c_{csvdDNrG zS!3>;(!B^DsnLPSZ648(^Ho$TJ$!o7+Wh%vYy9o8Pg?#Dyi z4E7D$Y{+hJC3C3G(;t1Bxtf^RUih*NP7^x+wR0z!r!8C^%Iy=ni_v%FzwjlQCDSV!ajZ5q_x;{{&=M zZKt(1Fz6Ss7&x*8Icz~0!30~$)s2mCu{nkZleMN!ZRB$G9Y?^yW}14D6JGp7V0eif z7NOnJ7G9}@Dv|<2O@U+ErnfPbBaPqq&aGQZ%F3!9Jnwt>vFV`WQhWMxCmi@rLZH}n ztdNG2$+3n`No_<_`qpY2vccYd_^FKiecP=jQ)(MS$4X80&;k%CAs0ZL!~0ctcyx}O zavVGdgpD!j3;`*U%)ZXfPjm#!rjOCW=Xl7HVzLhl=VJ#%$cr%VDJHo3J@5bB=Rl|GJ(h^ud7_Or ztRo|$nY4$1aNtyrIIsPMylmBD(TmEsgQxA+NbcPqVO?VtbUzl_XtV z-@!{-_ox7C8$o@G3`s*%Q&7o_$>5`dwKy{N;RBUA_mHbF1RWfq6-V!p!MvwFA>biDqAti!wm5hC@6rdFhhuHMy z=4NmvGmZ?S5+fscNeP&M5!pGvj~<0DGSFno%1Pcp$iX=Sn%H9nsv=j`e_#noYw)#Tp-fa$Ab z?Th-{t@Tg5S`Y$?hO}`<_&_d-!Fuol>VN)3l=KtVls_}@n)LJA$u41j5PB$#% z@j4zK&y5`2I0et?zK2Q2l40CEG)DF}drz7a3zr`TkcImQW29&`T&U_KHheLdAUF*q zZ=_$(1t8!eRWIGh)g#cLc_m<@p}Df+D>VTaZ|s_6Rl*e)QlKg!xoplO>*0kue%LUw z)gCHX zL`Avc-^=pYHodEJWx9W6oZgCCN)#IUGZ8yuh93S6* zpbjnX+7&Avul>6eQncfa9djj-x>SAr>H|9m&0==;Rz^kn@gJJ%KJZbB-o5(+e8zmfYRWINkd~*jt|A=CVp{a>UQQM2I9(wv$auX8)(v%^#wpG&>uqiKK z|7PN1UK`o1E^10;-%ZO|=HP!W)+CjOs+4P{cI&hMuc>0rCT*p{bQ+WNv{BYwnYk)-q zwA#QzK%4Vl;sc&F`!jh1oU*!}`WV8@AlGJ;yYx~|iua%;_W5xMz5{adgULmY9u3pb zIQ#qf;*pj`W8fbK;vwkT!V_ey|D?G1%`e&@BCBKI(um(qKU-#YY?bg%P7eP8UzWh@ zElqtD9XxeVtNSl}!tnFbkz6w%sDR?=#Ws1>>ULqK0y5#)I|JOSi&qn?~}eudi(Q42VPh!c`&M1E=dRTP8ni_gx0& z2M(m+T;wwR5szx{>^-|}$Y4SNM^6uuM>`1r?AC48E+VM7NMz!lKCVidetk23d_d(GV|v3cDYj8qf^321$vu^youW zwh*4~xVP_V6e%2|Vm3VJAwyFC$Z8^ZC@fS|{#0J>*xu%5|J)+42BVUVaBZvK z&xXQJ@F{pdk+-_7r5$?Qep>5;$;pe_L-4|b<+Ar$P|*weY+_UFRk@&c zaX}EyGf0bFAZHvoQNd^B{(fK3@cHuGrXR+S;P3m8Xs$m%AzZ2kM7Fsl073 z*lP%L^ADAwJX;1cK2L7N#C+ptsHv;NmPl)naLu81CAbOW=ToO>+Zp4(X#36)z;v+T ztMh6K7G6qA%LqAm6~hvOmUApVPGxN$9V6dKw>qEG6Ar`8J%h$goH)2&zekGuBBG)O zGFY{?{`~bT_r~Pxp}S_F^pM5-2z7?W>OY;njF=$ zM-RAy^CaC^G05_}c|(_7S&=8p@!Y<4WE&a-j-AyOCkF?sWHFdzOW&ZtKX(8AELT^O z7|l#UoQf=@m|pWi!9fSZD3AED!BGIp7{*kLt`Tj%;8kEi3cvxsu88 zleBFlLTFr+YBA|I*4*)N3*N*8jgTxXj?`*n6oN?4sKE2h>-+`VDI642l4$vEL`6MX z*HH><6mOa&ooySpXIv5S6s{P(4lgKUQzlQ2T(ObF8xd|=>F|dUJQ;!730E-sSMok2 z5t>?1nbe8zLDQW`lz;>@q4x30LVuzBOfrnwGSq3kX}0Wr#_p%2WXOaG_c7UXa&q$N zH)&rnd)-8mt%gaHZ)A#zYEo^wh*HCOx^yZuFqGX5A7UJ-%@BTWQxXi7@G`bAsWKXW zY-Jq>t3)HsmD&dz600rL0c}G0`W|{dVIMYcOkaQPA(Gv?;mytW+mw5+mYhny?p!lf zcl`Kk%(lIbU%mR3*h3o`3U!H=fCf7+wqnpSF71jH=a}+f3BfPYSf3?lYO21K2SuEx zZ{l_Ov<$HIlqt86Q^f2EgTqOi)$sG@%itNpPnxg>1(pj8ETRniz_PPh^DGp`2!mYO zHB#FJpu?*qQPJat@Tpa`p|#MOMc;o?+b6XR(w@SkbzNLr=*Egu+?NjTOCLX4W}%y# z)>P+aT3OKMb-r}m3BJC*ypgFx%(y`M`ua#fAV}BzxpZa4M*Nu$9$a8=Uk&FM*i*S9 zo8;M4eWl}r8&6c-aqd?M(<{DpS~tZlr{+s8yOK-Ixk30(c6XroWM>%}uUsh@6BFp2 zU0iO@$fpzrGmW@>`8tp|)Q`eu^S_5Lr$?cnGdE|^!8PVKy1GwO>-H-xrv02N8?0xu z$5Zm{XVF%PkUpYR={^Got}!wa66SNwRc2-%5Pl9l!Fbu$+}z{IZbksSVJcS|rxS#P z=xD2=PQ4ByJaYL~vKOzZ=0<^Oq_6KWzP4!d;X1vjA)3MGgM(WuXS0Lh^71RNZvv-n z!JYJNUfvlJHI+IMWYD0Z++23a-op7+$!yl>NpeRi6)?`gq;MVNCzwZ{G&T&*;r>#6 zkejguWZCN*n_Zo{>`>nuhF_F5E%>)Z5)_M~#jx0T4blqo^NR<~U?$tYzaO_Sdd(fZ zf3!Wi7ytZmdF@VJHzUvt?~ZR!9=IG?@+FhVX>5Bm7ldNCqsi~pb_PXFNpS~OJAOQy z0}0^};>%Q^Ql$m67R;N+kZBle0B{pP&qjqw(@|J>ZhFwG<9^s>VQ72ytTWGz=4IyY zU)di%q=a{MCRj3Q*ov79R)`EXer~&p=b!4lzZNXO!kiVrS5w$>2tGD(E~AY3^UG=R z;JB^tb%pX$!0xy8$oe~sb3`0#b=S~yI4Ip6WTi(BJ6l`x7jg^2^sy8}qPxl3`iW9I zvk@+TLHpB;42xmj@M#Xs`^eV_O{`#4(V%A^%gW$@d}l^QkeYxQI>-@q$Dpos6yj^~am^tT5^__d4n)!lOLp%*SvJ6<{`fy}=QxlW1bt7G)>~Rq4`-|$E2aFH(oIE$obWVuqJRv5D53AJs<@pW#+r)P z$Qp4f0t2N10Z%D4EuIZy6wI1{!#vkMDWbw5@xX!kpbNlWq_v0jTAhl-Td$jse%QtX z{}%S|lhmd&1vQp^A_ zUaV3Q#12DG2onMbkWo>B;!HBpV!r;-Jy%+W1E_`5uT_nwVdyD z4ghlm69RJ_j5ixZY#QU|A+tYZW=4g(6hC?Lm%jd1(6<$1f07d%G>dX_b}%!h!so3p zT~3_blBw|@7XbfX3ekzU1#x;fwon!u>ihIum0j2Uz=f_o22cg2KYpybCUj{xQEkbT zP9nLX*weCMXFJ&zpa~qlsn~}L1bFeYp@HVdChM%lx^)kmQ!TUjys(d%6j2T@9eIlX zF2JgrHqmWnG=I872LkP$_CYv>D_6pc0Be3C zgY1_%bNY0{d<(2OX5jbMHprJ{EgUG+A$%_yjY%MRjMQjJ_~d*c#5e$K=&vN%Mp=cw z-^1EOnG~>(IF7Ay}{JV>Jy3j&#dGyVx~xXABRD+vyi-o%)hkU^}J^ad*1Q-o% zFM4s(aWo-kpkLjMzo~nB>m8QW+lh|N4+2F2zC@$S({EOJIY;XMdM_gLbs@JU=7zu8 zvp%MtTZJy~J5Ll*G*zsGgal(AB36iEYKtA?^OrXDpn+U>44^<18qMipky_jNL!qmSXZ;=aZzEGl5Ty4tS3s%o)?jW5fb5C@Rm*%5Shx z1OSwZG&PN9L&i-8)u8Whtou;M0Qkz4-&2DA>?CHP4d3?h@!@-27;g-OxwdFgRYgU` z*RSi^*;636W+tC526;h|<_sB~wB^>;!g7x2qOF_9jJd*-@%JAS4m8htmL{yP^O9`D zoD}B=bQWp$S5%C?awQ`(BoYD2$E||Z(a$-n+=j6C;^3pd=Fe|*8angA&Gu(BY$&oQ6zl}Pi!$w4xcTI2$`5VFA zG0P;lFDdP9RkgLVta`k?t^n8wmz2hd>VYWFm$43B5`G5sSjF$hDEWQ>d%^{T^9Q^K zBW7yOWfydPJI76_w8HqQCjHzkFe+ zt>y2ZnUO)*r9gEdL8B$0$SZnkq5E5Xg0`5P1_r%@DOOE~3Q}A6eF1@iy*@z(g2_kQ z(uV4zP0wpo;VstW3RVndr!^Eb0=Sgs7nqb|DYdg|6=!vVg@a}*50xL!)bgYATnXQ4 zrjA7EVS-&C8u5oJ- zlAStng4XrXEU}^+nIArw(Ilj$T~6s&nsLK#JTU!v^$wSr2&V>nH@8-*C)_AN$)S?q z=oCrLZc4N2!axe9GxnZoW%XM3w`KQ)WI(rOXy`;pHp2f(Z$XrNe_7ey1E(HsDqT-4 zK^hUt@1H-lSfv53#6~uR52Oi9n!r3X7gnNS;VPiNc$SeN@6qEuOT7*I$*o2@1kB<) zXhvRC)LecfJ6Hu)RMAzldzQhuqm-2`V7gHRlV$iKnHoi(;PNv&d7Ygt4a$Dz3^j=( z{ofDc?)$fYdfJEXlCq6v1bCJ{!_r`$c@mO(hGq{M-&Ecx=l`QII&|a+oUQJz-F~Qe zV0at-7s6NnWJ{1TtWbQe&Bjx&?$f8!pmthBNtHr(3M~qwaUd01u;FVRGNTJ_4wkG+ z16grtSv%>2jIweVMJWHXILXNuUDercbvEUs71q(8AU|>@?%c6y{ILD+v;GFOH=g-q zLDH*?jLeydW^J4$q2Y^&puiZw=9FmipTWx#Uj2Ow- zhm}bLG6F+v+of__NXgtT(hN_Drz4o>3sORd%H`0Nh+8`netr7##T^Kjf;)6;O_$Cd z%HQL92qGmElm%9JUF=K(`Ww_7!$Lkk5maWLj5;g8O;R{>ORmg8Y#&hV?=LD$4t<7gF9|kL}UyxWYFDCLR zrjipp2x)}AxAkQ>3$O7C@c92!ye|7 z*487!7udb!+S0l(7p=Bf5FZ68hw-wV`a`{?OM5FbcY0_K*}jL4F8|Z@EVIPFb|tgEhRp8$&%+ZwL7V00*j(d;FE0CSPLla|Mqs2NkX05o2x4&!#KZFLdL78 zpnsXHre<~9y#H$6MUs`K)`Uj;8Q0`ozpk~|;?0o6ZdhZ4Px@l#*;9D1Sp)2~Jg{G= z?dIfib`UdEk!mPe4IBNTUUZJX_@J}iPdqk%&Hdfer#@xv9oo-$)Kg?tw=<%OiL7{| z{{ntlZfM9B`9e?LBdaKmNF%T{!bZ#U!QDNrn!OG(HLE&hB{7~lR~qbf!@70*9+gV# z2)1@t-o=N8A~aNO{!4z)+w1pI|Cqd2XV^E)i?W-{<>)%!(sG!c$B`pFbSfw)q{J-| znM#H+&_P=5Ge11GZeP>8H@NcTY)*EJS7}QAXVT6TI&Z%H!lU@xR4cCvi#6-#;Ifwe z$*U(hmaV0aitS|yhS4M3bK0dv0pBPZw~SV$>6Q#5NAKY3KdP2wGV38Wbog*#3#_Cv z*Qjygv<+(LQFFN!0+P$|7}(9eDDfx76ZbqhcAM(ib6Tzg4nC@^s(Sn84YfPfEY2M0 zPA;YV__O~3UGgE}^m}`c9X(pIcklG;<*r?(Iyx~g%$m_RcHwvG;e26)x^shP^{qbq zp&?#8j4?6`=g#$viP@Tzv@zCoDy-~2IxL5Fr2qoogpCPGz^MiTD>zoRar0)Jp3}dM zDBe~uro+Y04wb zJ6#+PE{?FPX~=c&#$3~N#aLnF2dR_ONb2zQ_C^FdwNWNu@gBl05FH+|sp@u>kPyP|-EHB}EA8-6t)p@wl3!jNa55lZ z7k2^QJ2s-<;s0|tXx#~^;eZ;PIh;gseM*R!dg$l9*o|-NG$Gqe*d(1a@L)W8oxaK! zX|V8V9U{%36NR;%HeM$>w!{ue%gDeZn_untV9gylq?7Z3C_C*EM-aO{Z)Q^iTJi zp|zM&IfgnAGSi9U$5X3ao>Wtu=;;aD7PuAwXbc>d>gfs7A>T!v&7WFrXa6Z65Y4|b zYOW0%s(vHHB+MaT$z7 zzOYW7AlQ`i&@EH(R?Ui~!79~DP|&R3q^7Exn%40Bqsq2 z$lsjkDrq~Ra)5L*%$%_42RI5TkW|6cA6f}gzy^5g6DB~4oBMJ^FY(#f72@+jB}Oj< zUFRi981RU~&OuX)eZSN{lFG2$8Lf~!ef(!v;HFw_8}LWD2W{aWEFtzsM*|tmY*OvC zcPsLJAbL_Ar4Cj^;Af_Z4&7WE(PmLf!xN%ng=@eN7d#AbjEzx!cie8`^5Rg=+7VU{ z39HMHY(9LrFQFAtQ2D1%$XJ-|P|XtbZhq0G73|zOc8r7(<4d3fs#?E!LIAJHkL^70 zWtMklF->B4SXe9~Ml^Y9YHFmHVyaD0V-SHa*2?idw+oluzW3|*j{R+}1~fV`#`^#= z_`__;Npr3FbIRs&j#t89S{Es!LE|gp`ANLRbsxSudP1|KV`CCXqNRI>dM&W*E?oHD zFJNC7=VToK9*6@Z4Qc_z%_|ozI>N2_;eOi3=LM6(yc=dOE9{fgn36K&P7IH&n!BQD zi(glB=~6ePK>FFnmwk3DQCL;|;qANpeAN)Dkk8iVEr39nuhZuPicqYB|MMm1ouz}I zX@VfqO;QpzDE~dLasYkcH2eHe+QsqZ)6uNc9%}}_kb3&;*<^Kfr0i6-$&Q>CH8wi- ziLhQl(+$1I@APR#Gq|=o<*Wd370I_zD@wS4= zuD78bSr)yZj(uNLifXUW;EQ+bl4Z-6uTFH>Ks!?x;82-`<-qa!6!#XIw!6a%D=i$m zlyB@LYB&ZmIe743?pC=J%QJR@B;eMtET3rO|;9+aHb3} zcfX{U()QiXy19q^Spiis*NMnsSFa=B0N6Nt4?i(YrJ!cSL}oyh1GfXkaEbMV&SU)) zpdkQwA3uJafBO3$lvcnMoBW2@v~IA3s4y-*p3VvSqx9j&++rKf!P$hG}o#evvzzUIu<5 z+;8L)b3mJXe<*r{e(3M9w}Mozp(rq|aXI2&P|~&rLlqWftKdUHn7F_DH+YX{|Lc-r z3>fYvCQj~Q$kYZ{fywhmbiTT}iv!g*(3sKO5LgxObmI|}2)z%eoqfWCF$R~x4jg0> z7`^a*K{=YNT{;gC7`8fFCk_?`?*(QJm7sQ@fM*9QDT8l)kX>|SB;=n@cSlDd)MfA? z%=8K2ds==mvV#bPy?@iDJW)eThxK1F433#fAyANCeb+;Zm;L&6;AHECgCnMi9-Ot{ z^YDol(BXo20|FS~#x-%`Y_wM%9v(cuf1ocOH1>xftt%Br{gx;(WQXT!0|Nx}Y>tBh zL-kLle4un2yO8@YH<|xOB5oKy)^9arG9x36b&tsz^M`n`g9OsX*P5CoVfp}016Fab z*Q`0x?8|(I1u$n{c2adEJ;qvNxB4Hld5AKQEOXNYlx6_D!8=g&cy*N`cL^%h9B zA|Sxq@{o^DF;*mt7axT?$jShWDi}+f>o|+fVs3(~8S9lAVQ?sY$x#X{x$_Shy`-Dp z!v50I=3CsG8?PbO;qn3kuxEn3rIQ#?2<+ImZ=rv~p#up<_b)sk6O-WPeP={cb7Z#U)Pc7WokRr$)9a_rp3Wi?Oi4QeydU)s zWHouhd&G*1i;3?<8}>7304u^I+>(2oa||3yUqH{1=HgIQRpmd5aPk13NT z^%*dr_L2$YDNOLjYHI!=x(j|%3RIxy;mp(1Ga55}H&HaVU;4ZL$`okj5SOKI#>G{? zKPs-svD21RhK2I$JH+%<56ChD*>A!L1RHO(;i0 z_CJfSi^&CQUNs45BYr`NgEM$P!$$OPPI0}J1EOypIEXv~;F5ktTvP-Njs1kHt% zpsATSv4^3TK6M~N9p;4a&I_(@ty#2U>>I}DxVBN@%$Z{Wj~;j+zgk_6wpTHT-|M(x zB3?M_?hlwD%&*dwdJUd+?_lr4QAF5DQvzUunbo>%t++Amy2Q@a{s5yLAG@i}?bJ(A zmwyx|sUGH=H)A+d!&lB_qpoL}VIN7a`%6JM#?P4H3Z&zfVn>xe5+sc81+0g{n>K+C zTHuik8x}sz(Wxx0C%$MyPe|*~Kdx7m12rltRyaIQVaocW$;od1rg+;M;|hw##(DQ0 zC#}=lc9$hKnKsCF-~Uj{W8tMUetz^;sCFTuCzMJe26%6x_wT2)HW5&oUC@}YW#T!H(0arEU z$H%{&oqtiwVX&FsRCWf;z6g9%#-*#6IUbsiV@;scEW89LCDc%6W+}e66!iZMQQA)2 zu@t^Sh!C_dpX5p^DvUU?t5+*XOK-*f7sz+-s=HV^3R6wDdDb0Jd(OVDJutJ+Z!%_4 z&M+^kSlGXSgM2&KN+xj9$nK6OAUKgdW&8CTCN0TfA&h7R{mm>XIC#)rZE3cRO`8UF z`N2DQF;C7Jj^SW2Q=*Z~pUY^p+#q_;6B=#`y!5H+*iW&+kb5!?OY-|y9edK?;JC7q_}JU9fzD};70ba)7cNh zDC^9}M`y;NTEGg+cEbkr9gr^(ZJ2+UrP}%K@JF8)b2D~iT}6)se1md{+Xl3Y9m_qZ zIwdUCe65N!s$3sf&Z0Ijx@mQo6w7dzOmwULOlG(0h80`=8S7l{n)G`Y#t}4KM2dkQ z_p9hgPU^^7qTalJJ+PA9zXd5nd0`4IHVL=3E;lh8)EDrXFASJ7x`rAVkP+fP*+eTT z9gIbA-wHlpkuwi(gRFvr>Ff;!KnHW}&4YSFu7*?V86u-F42yYFFypCgg6KuW5OxRhh? z;0cHZ1J0wzjs>VI^g1|KlHw1-ZWumo!Z+j5egg-|VZ+L(A3p!GXBZ06moA>(JAncqnkD^Ty}5qx=VqU+pFB(pW7& z1=0`Wz0>~h;)|5O-Mq3gYL&8=((FB1_B@sU;N%AL8sX5QuTfxvoe_Q4`R@an0roaj z2$ITXW@dTwA@UU485aL|kZ1L9+}gX+{9?TwGxruQ?J8?jb2TjNL9_`oLGTkzw^IGi z{{+2IUshH+s`Ohv`gD2udc|JT7)klmbEM}=qWHM!lLS3~O^i9ZB}UG0a4H=%s~mzk zhjcAWhirVeY}&*h1h&1bKf1#}n#!GPxPO0a^t&h60tqF44~d!j^uyeF>T(cB4%4k! zaAC1yl7)ca`zVMsDy?(o%$aCQV;0R~k13;|;6n55Ltra?NI*t4Atx_yF&+S=NN+B! zY{DR~Y|1n4(QNlNz!{wSAKRyQUti08mew&^;v>{iuU~((&IoZ7j;cUBhw+)HnuW;U zVdqZhd*rkkfloEHw5Xx#vksy;PhbELQK{?&`v>dNSo(8ZLb=QGzUjAo+&*gsINF@F zoW)#to+SK|AocH@xO<-p-t30>k<|h*vA^;PR=sH&1rBO5cqMH1_L8RI=Jqp=bapSz z7VF5v*@qE{HU?dJXHstoTgNd&@G@OrwW+0c|c?)I-Ua4M?SO#8_Wx&gVLl#rshrF1)n; zs7;Mg$%x`-&%Bcd`KIUCsx>-#WfNoj=wKDqvie)%MBUn=BhTxb7*d?0B7 z;_|~7d9RzeR^k|N2vzsjiVA>N)djPrKtCqKpVE;;`gL;GVQN0{g}GV8hSVV=$Mz) zXl^dl5chob=Crf`vxAtuqZf(&u$^>6kiaOqdgJ7p7sFj=<>ztOAWJ8nurd5$aqPjK zgAR##z5ERx9N|F2HST-3i(QLjmyQ@f8!2r%k`^WwS&kFp3#ftvdLw^Y_4-?VJ$u`?AKh{%PQ*It2{Ia{{irZ}-b!;1 z6eB+1hO6j5yD~D5t&@D)>wBI8Gh>!gk@D`Sd`8y6vfqeMNutsF73Y()%hGT*M`QT@ zFV3LTil?aUF*F+>Fx!XIza*OZ)Kh9uw?~h!>E)%R6m%XohVZb$Rr9Xx+@K{zEAx4_ z1Mnx0evyH}6jjyZmaq2r{JX}CTJ4|NtL3K0W0&Wx+>!5h=FElmtA!!y_wz@dnQM4w zBEJNS7K+;8eeIqX98(DLUqW7>r$xm!IiT>>@Wqj3y~oVJb@itcVY$$TNnTOi(^ZPwg30w zUXqg9l3b<1t`(m@?_>)$daUv&IjI8}qv=FOh_5-Rdy%P_<+S@ly(lNRM$qAmW|-_! zgANKv&sR`=`){GCn4TUhrFC=6s8JD=o1|fq@}z<7Ek9n;$O4pGst3VT{4uQ^_OSa@)03s_n<@io!=@m> zy;52S1&yFk{PwNHx`&ou@AQ68TUR)A@BUT~G$z%ftg;f{1S7z?{CqUWhv%&WcH*wl zTcR9FQ$nVkaes(F^4Y(?Yx?VYR2IPX)ip35J{l!~f-5wCPr82HC!V$PMkr>ris`F= z`YNt^(7jtXQvKE}cOaU4*+|X?;Iz>wUoWr6Y(2Q^f1hJC(CVpTG?@I0N#CiNqJK}R zO5>dq3>!Ty`mLVq#Im!@de>Eq9M%`4X>RXZb&9tv>uDH1mz2Z+)DY`4 zGBYK*bfGXwd-~LtUM(vIP9kzjbcsFb_+Yp|Lg2Q-=CK^qgtV!i8_6SNZs4enGC!7S z45F1ijQq$If&n{!1>R-iwt0dVEgc19p>ACs!de8mjD>U#c(Sc!N1rGJn3vZG_|48$5HbJXQO=}gfPT0Xl&@eg1mr;@VA;D-h|tZl)-n+$8b}cw zvFJrFhIm1wcV@1L`faM3nt4AnPQF6|(7^`hLqc9+1%MR;cOIGIFy@|&-DlGeLU6+2XcNhb;sq`5 z^=sEY%eC{Dn8Wa*c;@@)zW;SlI&G;O-!EVicV982wdMD%h=?z=U?h6M>z=$wl7pcP zYNO(y8LTBRGGS|S@49pntGf22&AFnoa$8|n|A!0-w`ECGO+Y?>oa*Jw8Om<(XkMD> z&g?m4^ORtVwmQz1p?y{RE&3;)UcdP$ciMI!;Yw-WI@1*V3_SJyl54}p3bevwbx zJZ6C;MLD8wxcNfhX6%`jH4g6K5gvF|g||Y`;zJn)1@d-m*X5|-7JrdO6?Y||aK8UB zA*lXe{_=)Ko}iN%pn=-*JT;doJE-U*7E%EDY))Q+85KRGCaf(Lgfhw8q5y-MI3 zI+Sh&@6J?3tyrk5e89lKL^#hpZu@0998>Uun{8b`os{yQQgGt`98^JmW66XXnc~6s zGz)ZW`LA3bxXxmdYt>Tc^qir~T0QfKhzrB=XFdu(u(!RvXr?H%iSa*j%5k%24^Dc| zUIrb?u;wM{+p4Vx>+0!Ueq{@Hh3}iAl5f6(6@5s@it7$a(~dsW93QK+s2c7D-Gz_S zbFyH3(lqgb+Yc|9`%JlFVD(Dx8w689QRag5WU`l8^)_9^7_+Ppjae?fvBChYNy8UD=9pF>$R(R#hAA z<5SKW_M_ko)nz7j8#X|~2mW`gA*$gJMKEsvy=1SM;Moc5;q7LRf#wpjR-y(?ra_!} z@NG7At%9A9o>Xqc9G#AhK@a0WA4`4c=6S?knmVy4qy5H)hW_WzzfkP)Y$Gs)QT<_y z7qP+$=MK%D(Tu5@r;XP*s^!eZSxHB7po@1;x$-?8HTUH|zd)~1P!Q1c>(eJIds7J_ z`S890p0P6&j^E>qJI)P5I!inTP1QJm4v+?tllHY9qF;aUe==|D{c)F{#6G-{aj?jn zxSOFA)%l%uPrUaO%7{`F@1}MtiC{`bfi6Kp>Dml&6G+wRH6Mn(eNLR*7kVPWuvvl{bq+_TesRCCpsu}Ay^ z{v^7%`hxQMe{@}GcxnUY;$OThOBY7u;BlD0*=H!2AL9bP+pXt91oSX#tRcAH{P~v` zRf>tU{ZR*Q3pm3%s3OEhbP*XDS0>(4u%##^h>qkh+i!kYtPc!JB!!QJ+63q6E$?qH zLH-1tB+G+~--<^riCHAB0)|;}nce{FERc1ak*S^V!JZT`)XD}+ypRyzmDi$o>M>|Z zuRMZ)tI{D6n15kq1iEU7kSNfk!Kl7rqBHZEX+eW7QVl$i(%Na$jjSlj+EPclr z-28E{y%Q0b3}Eexy(I4uRTck%e+D<2a|l)F8CiM6PqvpYc9fG!14H1K1)OT?HHXn= zg{M*`p9XfQS(4)>P74hjf1SufjTSvc1Cq@df8+bR3S>kA_e$hojJy$Nb|T>D(W7mN z0Csk3Iym~DtEvVL9{e(PoZ95c48XiWQ;<&+lliCcxpqpYo1A0d?c^)gFuK^zgqoeX z=9@%%a9;URYij0?nF6^j=0)qFpB11vgen|4Zl&i`Laf(e=CE06iG)asLS+(#17AtM z^oRYG9l#g(lORGq;*}msYA~GuSbD}S5&LG>VO0SElZXThMBr%vb@QTA!T(}G*Um>j ziABe(_V<5~#WiLO$r~YGM7KM}o!kho>^82Td!nB``C||KA^49q6765*<%v(L=%o4! zp5=tqaZpLiQMM>{24e!QXLZ^th{&^E^c0B_CCcT+L+!9$V{l_;zf+&0@@!!zDYg8y zEt1ORDN^E{S#No+Pv8S{ggPZ#?}BuyE*MGM43n*n^mGGOWP|irKTSRqnTh^*+d2I~ z$_`AAAZf5DVw>6P1e>0zyAFuSNt&OxPWxKz|Bc;yUF241ZV)tHlhgm) z+}c&-5h4K0CYNCw#H+|HUT$E3k7vtg@AZo<)m5th&gGQ#NRw8NcSwWRSIAe|G?;C4QLbqegS~ z<=*f^`rOJEWG{`k*z|!&dCHU@3=}xyL?T`{+kd-P7ul@sSJ0&TEvQ$RmtJIdqtpY* z7ea@G9vb@for%el;+-aMYu~vVI@A>PcOTnWtly_-WtR=!Lx#NW2wn}3`_jWn zOUAr<^_0s>xCEf3Xo=m;K1fy|v_Jhx>_vTe__GjGRZmHijP!lAYrA_Oiyc98Qu5>X z@w=Tu`{RB{z4R&x_>JaNQaNDp$RYY_MYT)^F~9U|&sY~5Gb;m1HzlP+!X64jjs{## z&j*q9O9d65_OXti%A^MCQ)YMXdUX+nN*$a3D6yx?jj50c=6H(Zg@ovJ<|_tlI~^S_ zM++2h3rl^$<~1aR!n++Zdv5y_ewrv$?$dSA=-~s70KQaz{P>z(9h=fSiS&Cj`4f6;!y zrw9H48thFHokZE}>=9mXs?ZN_U>;($T}4`wc}dJ$h@qFWOsEZ&*qPoqkv}x9C_XlJ z?-v;lZ&`NBMRXhnc^^RDfU^7O zteqejW3`wG+lrWlZfcbZ9mA|1@CfFN6`=nmzut-(p;1vsj%i&s5ej@vY;T8)LN{}| zDIVYXTu}1htJ}y^CF#F%lp%ijcI=FTzOvA60xV2m_FqN@9c4=a9Ul8+ZnlBvK>D`? zthBJ$J9|Hs7FrZ1s8KuHd+85jo&z=x34*@tYTQlT;oW=nIzN5K<-Dan%s)F>I&{`P z{HWA+hL?+oC(ZG(SiVQrKN|YmxB-A#ysChxj1oC6++Yp7C$_gjI_DedpA^pTIvkP2 zf=Rd8o%G=cyn&Lybi~@$)@Z|L4i-M8aMKJr?;Z1{rsi4=_wuK3;(0Od#C@Gtf(}sB zsf^K8NF56kclJoS0{>&LKgdbW%a^`uXS48dtRFm&Abip*Dk3L+Pb=q$kPk4S5lA$b z{H!MzK!Tytv;q9yo6kpRTrv5arbv4{8;=%;zk-1_(a?0+vRHr{D$N}UYn6Nfc~fZD zH;&MW=fEQ$VP((-RD%k5xvc6e?01Y?wMHzo&$`J0-nHSI3vP~}%^27R>X8ckdOA}f z?9jOA8oSd*A#jPibt~u18@A-Y1QoVQq@=X-4cq;YRnvytKvp?9);D3O7n86)va+oA z+4s%|+ro_l3sE2C1yhFe{y%*u>%o)ZebyjI- zmi2ELRwxx_jT*I}^K+WcHyg3vTgU`{VAqAa0C=zC;9+jUE}+XXrI%%TdRA9eHGj^O z<>OXoa-tnyIznFbKCo`{aH=HAxs}n?Sa85)Lihf5qb`q8j0>Tkgb@LAS;j`cfJB}q z3k$w^`Y-6t_%}&qXjxp}WY%m!HHJt=dHL>;p)$%6o;7UAD@KB7nZjc6<#&#gpLN!E z-60ai1MTs$ExwoF{S!(T#HU#0%dmzRgB}@?46xJNx;Bw60H6k<#JEgh3UlS>g524} z6hPTL;ckSAZ#^#o2-m$kBK~>w#~BG#9F0CYk0UZ|)!yOdO9eo)GTHi>%rWt+tBB^b zLbN0H$DfOd)GTSLPk;74pVImz#2sc}>{{80R+9tB3;4}``a^t$e#)Mv>^~Jr9M~0N z1iV~6|jvMGm0#W0Timvy;KWT5vpw!Z!=-kqN;dVD1`ubN5&`Jj2(r z>yB))dzKk(2byAZ;&6(#_rOz7v_=EBEj$e`1L-x+{T3ysepuRL{(d1qw@~Htaed^c z%zyPGj=7f`pd?ca;;kw_f?U94-fI2&v4PgHU{QohRFA<_HHefrrcMAkz{{!bYLM5Y z5v z{YcV}N8zy%-OsOC3-#Df=I7Tx_LStQ4ES1KUtcWZq7zC)b4Y6pO&BQUFu^PC@^u_Dt1 zvLhIWzbi7Dsd#?3>ZrO;z2tYrz0$`h9yGA zxCtQ{kdrVmR(BVJToK>s_fL`}pv%#M8GgDu3}|@LmX8hyvxw6_ z@u7&pPn7BpIOWK^3#(g3gwFCHfa??QrgCUAva;?mja|Ju&^zRz=6x4u=cj&xp#z|7 zhd^kWU7nP(mAQG$ruS3Foq7NfsJF7$=iw`LX4x}w5DdAjtqEn4`*pya5=L!dc^{v)mft&;FMrK}q4W@X zn(fbdppcgG$g_-e2G+222Zt(^z^|C~@zTDoKh_UI*0MYpBBr40QxzmVAH+boXBPzR z56e^Fu|MB4BJyBd@XXHoFC*LTshqgbeBFu~Ut1E$&akri&34Ov!v-9op{*l7OQWck zFhvJ_e!{Xt@)(`=;@X?CdA%=@!L~akGc%)I61)S$D&{1!@;-tEXeICc*i=GA zgw$MHaj(t~ROg5S>R1GB$F-#}$K3CIvg@4zWn%9-QP->tPb*ZPF=I{b9Ej9Uzx-Hm zOkw4lx^$({|I6Cdif4*kqsZG!UU%cqGNY-y64}rc7g~pd3hd&%$^4F0s z*Jrmj^wX)p_|~~=wehO|33QseY!-HL0h5S8L$o@9dW7H21cw2ui>+-1(d)C1WXJvy z#YKOi;s_27{$ISkc~p-3|Nk2qVjDskLS#q^8A?C{0Rr9+$m8zjM}E=kIf`@A|C$-MhN)`?}uOYk0n%&*zKiHiL}I zSaPJMXT~efZZQQYcPHjjN0LxF$*n%VzPXmhi9;})5*~9<4O(RT?ldMQ{yVoVXQR%| zW_}dBlao>qwV*%3ioIG|moI*t^?+rS1qH@ITYVTE{@l9@|ABp`G*`%n;Ba6vLF);6 zHCK%F>C#U3i;LOW30TZCf9Q!VJ6(@I0K9{@1ie6GHfq>8xR{ayz8zRF%q#M9+=+Ca z8dVxQRaJoysHtf^S{zVtTk0Fw+p*Vn1>P;YVy)@@f)f4u_3L;)K&RQ{7pXwd)TGRn=InZgC3roH@VL(e`#-`CboS-!%^XdplcloZB8Gz{P> z+p>?#klqX$j;|qN;t@f^D-1g}$wx36%u%}+aVI-3k9x9+`3RdPvZ{Vj7b?ih-xwL# zDzLmt=BWTU-u+Xbi=Tkxx~~x$`^Z;uvG%p9EJjN`+RDSxl8$U(sJi>qnKc^-QGAyL zwJ(a;Tx$AgSV~e7cndW+i@jl1;dM!j82gIKHiUuB{{5ndAAYQJ=Z`=cz5lMA`~_6a zDCp|zxn#EvZtg%ena2+wvaNd+Wm{feEhD?YHE=1;cjo2p)68+eu4ur3e8~NDDE8h9 zhKZ!65>^HcY2@@#K`oy2n)EqSYOo`Nzmg3}z19<$R!%xagLU(!O4us1G;YFKYFNGw z&L4vhuiLnhy|Y5# z?C&34@D+Z(@}_nQRlX4!49Bo>N6n^UH<;5(FvYp&iq+GIlxL@>Jz%W0YCnw${J%GZ z35vORrK{F+u;_)ViMpwj*f1?*iE3zh^x`3=X;Zz@j>!f*5f{-~hv<&q1JTWQ8{L`D zy<=ra(Sm@u@yOBz1Dy5iWw)9i>``#eXSSLe`Z9>L2`R%>5i9^KP{iA(gT(b%5;A?f zUw7ql`Pj=}P-8i1OC{Kwc&>rUE1@cU%%FeJQf6g=G%T7{%`u?P)(Lpm58! z$Uf38mHn_u_ zKlKhfb(r;Y$TaxwJ+v|WoJhx!LDMo*efr;(b+|^QT)j%gZ+|4CZsC$8eD^h+g;h7~ z=6W`B#?JJQJ<3B)#64ixJp9PcJ$XGvV$^6_hx6Iw6%|>UwvmRL6vs;Tuo)YUl2>O& z;pGx@@S)9`W7+n1HhxbD`__m$SAg6m-Mo5LT+y0Z1G<1*^^A}un|h4)dXkq%k6lM` z!LZ?UU?9nUJDW3e-bl(hlO_mt0)v;0WC*v*K6vHKt?jMVp2DNZjx9+K{iu9Q1enO} ztnujTDQa-xRkVBtV}hb_p??OYECB}flerzT=cy8FpD(*e@bo>>0D2*W82HtJ?y;~%@} z;edmvu>}t@f*81>uw<_$LPVKtDqv0#^kvfkv$ngrv4iTx1hWA>|GtW<1$2X7ZZSpo z!JuZ%=FPV;fCbAfT6)A@_QXP)wQGerfoD9`Bj2`o*TTN0$kJH1TL*jM9DNO!n*XqD z(IU1tzI3zj>iRFtB~mJI|JfOgnF{@r*2g$qcwQh6^XM!w9pc)fe>mFCHJ(_lxZ>() zb@ev_#GRK%Ra1SOwSZXnWLG*X58DA{7`8ml>^xEmCl)dXSpDEd9~w7C&qxOw2?@t< zy;=!oBYao8O!%Pw&(80J)s9|ivu}aOSC-t9T4v8SHHULfjR;#7L%m746p=b&z)FV`96VMqOk1f4ut&pDv5+tc!MZhpoauu+W8 zo0^(doC4BZ6qSEal794Y)vw|Aj#Tyy!p*I@Irxb!+B-hhK-;UOT|4Iq?=7MfM*+bT zbCzX+&w36f2q!a9#KgnAtEjv%x|=r5aQDtIF5%tI4)VqDwt#2rp&KA46NJL)8yuM@ zu1f0a8d2~SkDwB=8AP8HoX>c$nLFC6#XS1o<@L8o!unvYa%UJofp7=acbt6iq3}Cwf3p;$#=`pttx4;DGh5gha~K6h)^?tdHi(CVK5${H#nHG~2@ zWI|bpiOXd=Wisf+5n@t>(k=b}emMh0XbXk^$|=F!vj-i1xIFQ1v%*1y6$_)0)x^_W zz8L8Af047**;x+_EZf=vG@d>U)Qc9Afj_ZgMGWrLiPns^ZITNA3>T3E%;wW$eB?lR z1$2tbi8JiMl>PSe={|*bj@bhxE|DQGl(940)q5m&$=ytf6U23E))+=Vucd0C+Fs|u74|6jw96y zio+IB)XL87uwBqr;s-MH*)7>GF0FV>-WaJ9CHn74)k;7;C#WzW6oE??su^xrv4VZ~ z!iO$w35|&3$>;jMBn2RxKKZVwWM1~BnX4T4jq*NjA}=Y{EwB-OFO`?CI7JY&5qipp z&YyT3w40Y3JSHIs7casSCiyUYvJh+o?-iZBHO3psHyD6GWGTPWJ?@^-D|$hpZLqf9 zgZe2jHQp`%s|)OpoSP#?j>PQBsH2eq3|I`EET`4@b0Rc(EI=-?nnc>HPEsxxtZ-?<;-)^y$bo>|$79D6&vS)4j54^D7l z`rI&*H9B-&hZ+&OR#jIYzRmJuz7|b+ENax?DVD3lhF*iIPYI5qgohTN2Ya=1K0Z`8 zH%5wzH2Oh^CiJaYYX4$2WIWvL77`yPPVCirljmB{C?1$1@a}zom^E>sE-eWe zBQ7Q=?L7aulNQ7oXbOK;L`r$mI<{dtySvXi_V}M(z3Lho3f&C+A2O5Wc6bZSB61Fr z&8&`^qWEB(B~zl}V<8fdXoM1$w7Ep~h?0r%e;KGyl#j5*)%Ac{nmybBH(4e{@)cORXFwEOiJ?GBd3*a2Ow0KucQIFft zin{-sX}sbF^gbY<&NJ38frzG5U;|&77!I`x=ezmOZj=lqCp*&6FxFu>z@qE^wnJQf zkv1$G7wBM%K9fL3z!F@>f9*-$^!nuUDhG|F5o`YY>@lPnJ1RhqKVcu`PCcSJOkJl=PllcOj-J=c+WOT)t`#3OSDLvY+aLN}~ z2oemXQz1W5$>DS#g^L8R21p46l9x*gs4V*S@4ttB+jeD<`0(K#JeYWZ9#3EO!2eZi z4BY|$H~tjFTHN|lDMbSt1;dZQhe1Tj83)itsm-2UbfRkM`LSD-@9h?5c_Jd~;0JqR zb4H+qSAxUq9~S3OdrLXY;S_-`@|Z5Ck$f*&t0A{31sU!NEnUlZXqc#`#< zKg0vR9T~iJ$&K6MQnQ&A%`gYn$YlEzr@q_kVNARNT;_rQapr6u|wUVtm;aM+It z2{lwgXt|3z+WYqZueR%CBbnVb%`MSfdmv}bwl39Zee-7s4IlW6cdxhw@WAmo!;$wdtxw_;OMh|C|9UiTo8dp4@ zOaQbl1U>xekV+L!D(@l8)#I)}#JPOAj|@v-pgm?wUNHK~N%^}TSi?-H$;sNQry>Jc zl4#8UmFV!t75Am4Gf!-#XKnGaupV=Qfr6Jsp6zr-(DPOb%Dliv^qx=X$ttO0E1U-( z#?o!U?{@51#AI-b{(kYH?Q|w$6RWX!Jvm)kr!ih)4HePiHp_wJq=oidSov-{d*U%g;BZXl`!)mgKSD}ZwIo9KGE#)6XM)G3=`V==Yb z@}X<0#;LO(CMq2Liy9hxGRJ858^6Nmf$j8(U{f!M9(BdcRaRV?ptHOl`&mG@R13r1 zs-X;DbXG2139p+?P^OKoE~>&53>*PoV~{(u?aF3_*k!}+o>T}mHP>iK&LFLRbe*H~ zHQYr0VR10xLcQn`hrx$+Leg0iGs!YdqH~b70R_mofhVdl?#$h+IaH_dq0O{PWr^|c zAi4XAPv=;lQb=_hy*;5@TSR21B5VVu#Eem)XJk18qeGB&-kthJx-+8wja99-Xq=wo zu?~;?E`@VjOx0sepr@KQk;;M>P*$uO?g?^slI_E70X;eOxnCxYDS2w0wOgZ~({q@(39hteGz48OJByd^oh}q8F;{C+$TaWgZ+a}-e zBXxS%fvyp%e~#X^c%&1RFJXCj2hVg;x3S#$@^Kq!mxAe${E#ONX?EBOS1(C0gGXZD$0`?MLPfs+ByaRY63VIMP8M%pMp@iR15{q35b|-x7Vf zlsIi30J$SHbh5aZP+mAY;}0-D$YeTuTz=Or$)fR%w2b%L0^^fqgB(8g1vuNUZm5Uo67CM`B_MOr zALTMaWOR_tQLcmF{KRr*$)+YA@`=@qi^YEw!qo8^nfZEC6U;ri>SnUUVeib<@71&h zA8!2lVV+K7U9D(Vi)7J+NAoW4VWWuiZom1{w|!pn@nn8M!P=x1IKqG(RXr}MWOz1aR^g2e#{8wtcs$jIK0P(h!aL|~bw?Ip zUon2XPU5FApQ-xsO$B}aX+5?3;L&a`9xjYF$~iyxq0Lsbl`CMG%(bg~7EsX{sw z7YEiH@Z)U8vd~mfQ5iSR+rrVtW;*4rv#=Kk?m|*=)`BxIK`JB0($I8b3ZGp`n&GdE z2?lOAD4&i5Qg9hdRY$mVtt|iPk7Hk)I5IVZa7Ul!-D+UHqn*ZrYR2XYv+UC%!t4r?RXIEn# zSOKO(=wU&0a5@(DYGj3JXbo^Lmn<}aE26D^A2Ji9PtpYc%N4?=qMmNxyP@iAU|8EE zpwsn~KfL${iYmRt*$6rd*S9oEdv3x$R8e03K95`OL|^5By$^sKJ$QWdK8!F!H|wX_ z*(J9fP0IoM)cZ-|-Kd zMSZTp&(T=d?5?xGf>JwYn}g8uweHfO>Ui7@a7?g5>+NLAcbSBGA+Exa7#nwjxEUgG^DQ7H`1Xm zA$|NP#ga`zWh-~=Qr0KMt?%c+GqLvkeL#2;nyG@ax=1lYMlxDld`W%Oz2~D29-IAs z+0wn{AJHULR5ZYM9P7I;Eoy0Hm8I5=?9psFS0Bh6#Gm;o zmbmqpFw)eez9qQNV+S~KT;z>9w42jyaY=MO3n&Rb zS1#E=E^F~s4_1ELIorFAGg%C?%5z z?eTtIQsT}ywxX5xkh3|q|MRFXC2UTX8Z>DC$h&@(dWq?unzg5`7^UN^Pv?@3$JZsM zP_*Qc87)Ptkz2O3wo=0a%Kzpq&`hcWsWo zdiD6@Plo-51P28junrjixrzA!$8KzKCq(BRL+_Gd0frEfJGVn_hnlncieHp8z7=mW zb_7_2mmKrTE-GrI`l+Y~w9HxaT%rgYl!v2MPUv^PSU64}TA6L6uu<`kt;i^I>->6D zmUMYCR^9LAjh7htE~%zVXQ_`p8!0FnERt-xubXgL7i`TS6)*yx7ezMSOabrc!c<^d zySQA~)q3MI{S#PJmd!Lz%MohYkg4$8K2d#%;yYwU&v?JarukwNZ`el&p1&5*(=Kv5 zDxA6FIctT9>DJG7e~e-R0t)C3pEMFmgkpL`e6O-0t5&W|Sb5#Yf)U?{IXg|J6octt zSKyo$Os5UhFY_|lh{|W@)*7La(`ALj*8&;}p92S!zju)WY5|*Nl=lMS@0W|KJgyF+ z9$d{x5UhH@n$_yw$LEh(Lt<}t0NGy_rtWuJw%`2?n>RzgvT$xk8pWviQ~#Ft?Q!Qq zL+NrBCw9V=2T!jT?=e!fYnT>s zcr2zg1F4Z~&FW7epV02AW9)#=R%F%+2S?6VTAw`$XS+(270*&r zyUR?~LLgbv&}B90wG_=_+wt19^ImUHx^SUeoAfSgpYeMq4bIKWTl(aO04o*>7HqUP zL_2*_Z}P~)Qs5GI*z*8oqih#efy!W;ZT)R= zo!uD%B*67{-a*hIjx3mhO-l>TQ?o}1BcFgLS$X3y5n%rDv#rWh7{n$w z6_w)R;%16O|38`%6^U9_tYv3FcvO0hxq-n88e1;ZP5UBTo^GHaaZVEg5t!$bXGwd- z?J;U`cAlQ<$%0OXv_CsvuGq=00Vf+9?ceH0=8s`?Bx^MR^^1tqX=LskK9Wz)<7wYc z5Rd2;Ih#M=tb$B)qpJ%p9ZT8YH6jNs>CC`6t<4)1C(U`4pMQRY@ziP4w*IR@ zes@s;T@6vjx&7zQE$ET|($YmQdyH&K7g~2aN^1~l*?DfoEitE4Z{Hj7$1j* z+`a3Bs@ayAosZXFmwYyb6YD>s^4@8YnC|;CVNYeGL>XY&#PU^5LNA~JU4iqD z`@cm+&fqH1%IRTTz?od`sCU0de^pnmBrkiAD!>40Ahg&_HZPo;3qrhz$Cpw7IGpF8 zz-`hQa@laTO=A?f0Q{0y+ByLo?Jtezi0js^E7?3qos6g`c_g%38E+QN@E|}U;9kqeQp`LbKnYDZHoMQ!H6qn`%5*yOgtXN*9q*zM_J+0ow|FAFR3lnz`@1(c3o{ z($gV(QD+mSpiubV+m`n0A`)?A#U#gqi_e&b0B&EbS)tS|j9-ANh`tzjIi^NScq3k> z_tswrAQH?m-F-5B-z3+m0st~9qw-$!(TtzadH|@w@@5Q2ZVZ1wbH%h~6?B=0Q2vY9 zS>pqO31)4lCf;{1zYZAIH#ip-;CbN=@<4?OWEXXJ%n!9!b%CHR_)?XKEkPw*+FMk_ zpzlDx3eLGvtPAxcZMpv?U(Jd{|3XT5%0|%e9x8uf+h=9z+pi#kGxO;sNOM^y6)|@| z3fcjZlCGbh4x5Cj< zqEs?F1p81Ct!-pwuth9Q5`@Fh7R$)dtO9R`$pN5=0m@V}anvC_h>SPu4u-ik5YPQBT`a1}!?{?UV59R~Lxh@pWhC?mut4^fMSK z6z)|4nRWv!5+hm>!$RqE+f&g^%8`X}h3iDAuHMsXnz3M=RDjr+_ z;;^Y}v~_iTJ}+og=h`k^nnxZICfRKH7_Y2+ePd>iK3bOS4YB``dqDdGf)-CwHdtPf zl|s?fjV~`?6~tamTrY6^SqFwlKpr-8(@-lJ4UO_=&nDxtf=jlX1jH%|0%qDBNS+dO zw+Um_C$`|L-wp{&EvJWIhGn|Q&wLX*$(i5S!m~VI?$o2NE(;mg?Vz%hYO}wyLUUl*Q=ohnLC*5S2Iki)*F2 zUG+GE2^TfoOFa{C2ncVVoKti9PcSXL5tsJX?24p_)@@dnz2>Aqy`mN=id|dF^nu)1 zMht}=iZbc+nKSs~3d$znb(ZB#wnAZfq@A8 z9M|Bzd*|BNM4G9_%bBauAN>V9>@>zf&mFs0E8>ggvrXn#yNJHHim^%x_aAppVBKFo zf4*>=)w%bX`qz&iwI{i_x#^01*bS%>O81FQysr1AnPoib;|5OL^SP_nBi%74vV(9B zL*Be`yzs0fPXi+a!6b8OCEpdmVh&j;47AF0_#JdoV@_d96^0of{P!Zqq@#lSzV8Fl zDj(WwUt}_7!{>wCD_+*Hh7`5{U0285U7mu$LmK@RhK zG-)41HQ(!Rn>B(56$dj|lpmLkmg|K(yF<&=K8&pZ1(@aFW;{~K&cMcIjJ6!D&#Xt1 zGo|pv%v|i#f7H?4reH5;``x8B6SvQI^pi0A$vITWn+w8O^kNFl*y;wMkgZm4`*bVtZb<$nq1sF zsh%&6E_EZjB3bv&Ac4XOnLCOr-cmt<)nO43rEOyLW~S;Do9L&gE`dg;r6q!_!#S1e z{Qn3oh~`GW#Ja>>;tX<&;5`@dLC8>>edlGqeCbR+h586Jf=WB@*)stLr%Qu2T@*_*gZa_C4U7tku!26!!`CS~gCj7n@bY*4VCULt4l{}N_A?hzg1lD}p$WaPM8M@?2r1SKv=O`cZ>!*WV6 z!0(+NMc4KnWYCXbPiM-6o>y`P8sGW(B>t@PfHgrJ4kqzzW%Ujn*>C97)eu6d*r9v( z>DNzJZ1Re$BApAP&z`GSHhqvFm|0WufEhxid2HTXUq4+%rE1HpZ-)$e{ngAw=*4%5 zn9KyieB<)QUp+&XG?5oR%&Vt}fcJ6anpBF4uCRDz7jCb$Z}py!x*9NO(`DIn@@nFy zM|U%X<4^19$-n(afEJ|HsCXwd&l}oDmj*$ieM|BPtBJ~aQKBLBF(#Tljqm(1)g16} z$IhJ}W6x1gFB}*5@^Q6tw&ife@1slT}yw1=kZO+fTn`T3YDh_x99y=ml5ktHhD z#-*pHLqa-f>GSjzXX*0k)D(m&k70vgOO` z>l5nKIC+M*-!w9}f*}HIo^?^tGHw24cEBHArxDacwr%?{H<(x%{ZfpsE@6l5oLw#A zGTzn`*}V&*4#vrZsU%#p^UnOS>k)kk6MAgg`JJYbts!=rXRfo#WQZGTHG z`r#FmP6@b?$y3XsYjX0I$DAE}W2jp2WEmZ-V7)(XzUkNAW1P2b`&8#1=w#?_<|8?p6zm+O_Dzhsc;(S)U#amlko1E^>rh`cw8FjVTHQsvg z1neW(cFhv>F=MpL_Qr%Cs_KtRh@2cXCVIsgq$oADHs)$rTM!B_yqR`rek2VjZ7^dE z&5oA+EoRYlKB@8l-ru0li9xjouGWQD9v#VzhTT-_n_ZdctZ6aXce$SbuP<5`)#Zzx zX9g;Eo~cOWk5Y4+wr<+pGu0s~g-wkR|29=arHpAP`Nf<_`k3##UxBfw(py_vo}N*T z-#Ngh*|M^C((gCmrL<~IB1;%uCUu?JS_fm!cKYih!)AzMNjsN1;?%-%6|yGp))D>( zDFzwNO!#AIv-;YeX1kUy058I+mytkT9_&JX%B^HY!~0Q`4-=9kRYRAmU!-z4n0w=!|XaFUJCO2du&c|guLMs`Bii0 z!-PDaZ+91h*zY5AgY*|W`c1SZit9Uezj7`l#4niH)+m|z>f&jssl$d26<=$7=gCBC zW92vBu2xA@Oz{@Ro9=E#|7wE@vnWvU`=nA&5jE75+*VAfD&tMso;L~2=;SdCZs(O? zZ2k2mFzN3%eY@$pp4z$^O{QnkbUOI262yRe1nS-3z|AizTX7gByI`Z z#6I3HAOH*UbHHrUIky|fEvQpwV~^0Yp2cm*;iZD4 zReH+5!&*T-y-3q3ND99XJnCFgcFDO}U<=4LpP<8O;(=wbP}FbPXz^^@={7LE2Ww|D zlYQ_3>JF0C)cmesiUYJucB!w}Cvfm7-k8O*sSiL0YT(BpHkZqe-`##Yk6c^fd;(l=keuID}zwIexJUX~Aectd?Y>FXH7op4&6n3O7TT3=3IDa#k!WJ6mk{iSUOujYNatP5vvwb1Ogo zQWMraGtCZMV{zB>Em@5*f_tx5H{Q&@2#9-c=YA8Z(}zwk`3$KP+!em^RxG|#+k6?( zQt>;!ucU#;J6$2*&5B*WZ}rj?23OXnFr%`YCY~cwp`N5B0IRZ)+dwbtzEa~gm1^18wgRdQgUKQ&Zj=V+|`LR z=uoD}$=&PuTFtpa%#4Nv*7E{gUF2lz)~{zBdk>i#@a!{s(Ys3@gUeITdO+ELVS@(E z>&LpP`)}SLoco=neR(5gHqG~72?_SV=ON;F`LdpVB(9t6*f~Sw1D-(4QfYXX6l*(u zC7J2f*mVqzocHn`|6d9qi;peyD$Gn)PG`J&qj{R(Dckrl)(r6G=i-l1B}WPw$FJgO5GNrE^EamjNBM+@r{o@p{qxT}lk0ZkXH%|SlTx-f(bnE4zZYuynOk2_ zW=v^w+9R;vZRVA18<{nsZEMLJN75bh3AnlNAm0V$CXd;kp!fSEV1C%l&1aai9)8Gv z0AZIeXibu0X3rO66;)HKdcb*zPb9RP>p1kRg7XuSF28cWy~SLCKgWbJS2FI}#uGQ~ za}Hdwbq!c0)2U_iqlfnkz68;7?xf7;89|$zujQn~pK)@ffrqAn&fQ|lP3{*62vEZD zv!b!G1LX5hKbO?fXF2)a%+=ha!k-=puyZP%VL}HTnR-bwE?l$X^XCrug!Fwlw?11B z|4{G`KJEL@f&1Ka>af*=*0XF$u$&=8XJwgQ1g~CKD{s7}XXGwLZ~y-D-)uX%`05YC zJ45cTd7JIGzd7ebuH_R=3;zvLhct@ZkN(>gQrutrV>)|)&?hbsAbjK7ZHdYLyIhSG zJmG*b%GOaizSqq(&5c9^BT9B&Bu;d%i5jWe;3a)ML?Hl2^SOIS**~r79U;r%Q%18T z<@tn05%&wI0P#w*Qq~t{{_3fdsu1$XZFC^^3W>J+2CrQAsgG#ojdW)RQ%+arFEoOJ z_dIG_STm5~4Psns82RSdPT$nhdoytD^JmX!5P}qnpI$6lJ@Y+v%%onA7vqG$$4siV z6)DuGYtp2q(7*Ema1NUBTktjlDz>O;T^%-kd*5%|tXb~N*h=kh%f#+i7*Z&Z0%I&N zGj|&>q;PIf;>bo4N%>5~B}@It%~= zTtZ0+;(m0>r>+r${fAEb$X6&?>#XI{|?c&;q*~jBjWU`3Jml?9|2>SVMq-8ePew zMoAy{bnRs;Wc5laDw;?#!Q`RR#(a1+e5gkFv1g_)0BnF_;201_)W?jupPz47cD{?j zVPPHD?4^4TF(CA9o|;vqAFHNzoC*UArBSLbU)UoOILBtDQ~8m{qok!1Km7a_m3G~E z?q()#i zD(`uL83*jjVj4kvWq!nN=>Pb%vB0I!4;(4r37v6!>`|;G4?*dsH1)-Xp~4@FxClc| zkblAK{X5s`I~b1};lIR81O?^wcgWOt*#G{mC8ICKsV@af7lXV1T_%H7G-wPyfG~$l zHplWXv1SW@TYA67gZG`e&!1O0r~aKed;@;3`0^#DMow!EnC@&^DG(L9F@ewtF{kus zjM+V5MppP==qLad<3!r2L3)o#H&59VTO+Pz$>>&G8+m}CV8;yB!oUahX(NLULG88L zMXGyMe-S&Ys5#R4S3?zrFC$`bVBt8_5GM~G&gEQBRA5|t!||<@%ni_YSUmEJwsH$& z`qlm`$BGeDf`VHaBS}Od5=GNy%qXMA0~MkuQW}QKh0>-i|86+a8V{C8fRX5ZfI`Ur z-JRPh4;Uss>i&pTYP@3Ja0k-dYT}}zet~o@wBX46`tJrA=uR5UM#JZ9lDg;B1-OnC zcDiDe6hZ`~tAL+b+E1LZR`>tZ1c7h_2oyvRHP>%kXBIS;umB7bzwO^k@_9g6$511} zNY2m;5ufn(@_21RY5vMpWDtq7#SYFGCRZP~%o2E)B2xbh{qH0=W4$t)h0Mz|+g*rf z+!cGZ5yF`0{hhz(RIBoD&)4sb9p_^+G739mW(5Mj*j!EmO2zaLiQQcop|Fqq(CsOc zCIv1WS5i%`{r2_iiE5O4{~XZvHb9VGH%%xZgk5#jkkiy5xLDkQcZ&GX`8Sh4Tz0Z6 z!DN66Mh7oDJ@o^efr@&h5HK)K-j;qouAwv<6Id{*6TwvFk}CRPBgeX@ZFsutkNS z37Xl~nY8`VR(*?_clVZw$%u>f8oh6f+=^iPtrG`tH=NrgbDxPrf=B$r=qbrXX(p@U zlg$zkAtu+l#@B4F_A$}1=rZI@+$~n9eRmjmU-tM8tEZp-ROKzvUNNqV=er@EPd|*? z_UznK+eDs#6@1{b zcKpqG7PdM}W+!PAj@hRKfgpc=tT&b6)qpZcX?2M|NP0W|zEinC`?_gS95fnMV)?m4 zDuxrh8*0jeL)+ldyo^mxoS$1%6x967wWwHfLr}G;fx*3gM^X<)jE6~&oG|`sOw5<4 zV;UcyS>A1{RZF{Ff@c>`k^8n{o;r;k_1>!(8?9v>sN-+)YMkjiAhSflk-PjVSw=9` zy{XY=z8{TnzywCCk3s%dSy!9)+P!Aqmpduf@ZG@8ZwjVE+~Tyvw^@%K8A(Jp261JO zeq>npiZ+TRVcI0kAM>F@GGZ$>BA8YSisXe0F}Aw}F12GvDoj3=cf0mGtP&Bq*dx_p z{{q86=9uz9T+S=Eo1}iMxCnU}l@3HV?F6&Mz-AsqsQ%Z7=|0oqTG zc=Yt?k)=rxm_Vf6tJ(eaGW7-bmMSnaaD%Pw&qu~)W=$AtP?;yxgY=bq=7OtG25v2A zK_kxp%P<=2KF9T}e|%i#G*MivBpxv=E0)A$?32&j9CeH8h4mNa8;^t;Zl+71Bli}^ zKw@b>*XSw29%VKCqHnt^5j(|%-F0R%5O3Z{j)@Io%dp&`kNPgXWCW|P-E}(uIDHzZ z?a3}!Xkgk0%@fvJ-+iw?sh14$;^F+UPR;$+OFIu#w*<;4MUZ7bIR_!4fqH?kg zesLJx=|rbmVYfngdozg-_1K3~+bQzKk3X=ggDYA7>48@l(JvOEp)rHBi9?Ul7m+DK z0s`iBzJnL}dJ?2p=Pg^_zuNFf|9EHnHUD8Ve40yFLN{SD`Rk-#PC-Gy&AXP>w+t(r zhe@4crooGgglh-$$*8)>IPue*=Ay=-B8ZE*PfZ*5!tkNVYo5RN6)FbR6Ki*#v&)!p z(fc@MZvE$T858b!qqGr+9OO9LX~IyM3S?#=ev?&3zI4?sgM`e8x>mH^CE*IhYbnvo z*02H7!&gQd<>w-`>f_YTU}u|e{$NST(AnF3REoiR9O1a#{6j@XTl(n~KF9U1*pc+L z?55()U5qtgZANuIuT8_VfU4o`t;XQVtC}EDyM11CX_PRTeU>b2f(!{+BsNV&Wy78g z=(V!a?|6%cEO}%xevS6aq8}qAB(hc|=7QT4Ve2*U`vUd8McZh>E5 zMjdP}K3bAugSfWK=BLmoUd(HM?WZc^ee+lNA1BQ{_V)6>jEcujBdkAeD4Vfg*MVV$pHg0-xOpH9x>u+TTsbOr%~;>(XQENP`){}O<8f(tAnj*eOy#qS;zO&s?Fn* zzsjvo8ZaJ^-lZ0s(OgrpFi7;l&XUHM$)2Sqs^rzZx7>ATM@@jXr=sQ%QT>){^(muR z+h%ETZQ}`L+fvf5euyY&F#;rys;JEdyB^soK`+waa*0*ovZB0P^{Tf5o z^dD;*-ac>tBbNrWDoM+X`0`KAdcu6Yo}LvlBb%f8%Srl>DYE$>5FH+ckT9Q%~MnOt|O+d7HJza6n=HX ziMjE<;_2h`6%MSo0yM9)3VZSG(z-j(TenS48+FVwQK8tTDpF~u%TtrgDAxNq)z)CK?lJj2RVk%H@|AadBJAQ5hO3In z5LGK~XH2K;Cy?*ey7%3t!Nau@Io6fuxw*#jLGnRyILL9dcrhlKHtodr-9`4wPW=#C z%ZjB|D+u0{0L+Yqrc$;*2Tw2~WiK}~#d7NKHH2oK&Wf-*$A(TDBP*-RfL}?9s!4W_fR!_YOC1|7 z5IrO%b++$g+Z#?+J6Fu7{`X{wgJbb4SOQe&d6zG1jgIjB3{MTb&1V&+0lQ%fF-iQs z<`;>XWLoTEZN00m`;4cjQxAs?of>+2aN(t-n-k8;lyyo93YMyJ-DzwMcHFrKFb`f|z3M2DTT6LMAhoRb#O`m}%AgCet) zE7@Z#Xgjg6z(KkG=w6F!wu=ZvGFjk0coEA6kl0_BmECPBb;t;(chI+dFyya&=laYC z@%J>GUbrUytavi0cB*!=Ir_tAWkvKkPtILYkm&tcDj+d-yUOO$tlR67ZryT6VVhBP z6oS!3-?#R!udWgOk^POL2r@16TswU+96>&y%Gg@nUrK4A!wT8Zy^JvSZy);etkWIg zogGsXzotVI0IcYH`5o4X6oA96FoSHk{#@hJD?uURUcKknGZmqRxhc>Uqy`SWpD+r? zAyxzOA>B%s7#1QRXw6?RYHQhcnhIeK=w!G){5%z+fQn|M3ach5NN^IQLj6^V@wVcW zDW5-M?Geo_xcEHiYtGZ-p5(qxR7d zzxipkd+uPywsWWI_7ztWGh^VCH1r+)-JPx^u`SwE-jreQD-oy2whsFsJ*bLLYvLq( zAIK=9`E4UZ5JMVVg+#Tg6OJ6cgPA5oGe8vm`e_-$j)Dn78pV$IY;#NM z`e#y2o2M`N(acwH-e<^6N#b;fVXj zJ`Wr47&p@9#jg0oQwAl3FB8g`UXIV8#N|KmgSYy6alY%}PqekSW^8;?>1=ta4HLSU z%a<$iEQK~=x zn}VrTUYQk8ZAK$S>ytM>HZN?4kYr!f+}M~9-AG)eM_D1(H2lZp$7zmR@wyhh{oLN)yIA=^7I0zejEMx&Z>k++@n#a^ zn|X;7O}Q*q{c952lQziAy*uZ$ z?2%zJ58~AZkCo+x=R!jvNmYOS`kLl0I{7T<(r4aMe9_1F?nD zkLOij>R%U}p1$E+!)?Jz;NzSWBrN%l9h-hFM~*6NFetbBNM*%F%B{IvR|YrFC!^b6 z%QSSxqaz^+4FvX#yK?~4mg?)T8@1zWpP%?>ex7riLf2A=96H^Y{}6xc9Y-~tp4Mb} zIPH3cfwyz(+?)jo^o z*pCBo487~~icFR_7QB3UW4&RdNBC)l`67KshTf>o0de=x4S!_!Ozxt?H|N@h^$ovI zCdTHw&e_+2oT%7!wy>ehGDM@>*E6M|VOB@~{Ma>Uesf_R-E7gBGk5r~pqo#F#MZYy zAga_`P5IsO=@_-i?uNC_h#A#EX3KxER#vXn)^CSI*Ez-e6DRhGh@D%jCnf_emrC$A zUmGZ^i4g3pz+TZUgN}WROz-CsTKk7-d+!7LG+s>I=F3_JDuiF3&0$>N|F*ieVeRxs zpfbOX26_NBg1AMP+HKf?74N!oi#kf43y;O7RVg%l&Bd5)$KyZ>A%E!y50+NUACu3( zbH}l_-`jSS6#sVdeD0L0IW_nU1wBP>rpMdEJC=04?UH7UOq^R%SMC&W z;>6q^0p`sZ!}2=a^|Q~m&pr}6@2#eQa&Mc7hsWaQmAc_E0l~vsaMN*Lww*#l|DCa} zp+vfh+MGF|D#6edw&49qNMLKA;4lv)ky19J{!Z3o#)1eE-x7NgF)4f=T)e)Lmr3a zztx!@vP3=zLPMBJ@x!%gaq)hi6RC?pRT>J+Ue6GR5f?_s**xF`NsQtyIJeU1)XqM` zgtrh`BKW_&-mgS%){Dlx*aBnXAAvoMVsFrkv%rUHmzo2AVnKefb;A{D{VChu;^jH# zAAASsruTKO`~HQ$BNf(P^UqJB?Myd`)i3$RSl6%pE+?|&pWO>&em$MqD*cH5(DIk} zynw~kU#rejU*wnUqI9j6izvUCNnN6}%PK`ik_O$9db@wMWE)l`CI!GIxyH*;Ps1@5_SE zU9Vg`nSPDUVZwJ75$U@)AlvfYj;fJ<^Rd+4{9WtszZHp)mD*DqXA~>+gv`Jg#ja(c z#NU^4e#^Gyj35t1lAG53pI`8i+EI&ts55X06Jr>93QCNMCIfNjiq!$4Q5iajq))@b`z{1H$?uAq`anX6EG;2Tl>s50OR6 zD1`)-zA(yB8)Ll2@wa6~A7Vw3fNUoFgLx z7uh{cbwF%uF-0)0@*l~lMtG08FfPSpBy`qo*g!MB`Q07MaRXs(-Geb!a(>v#wbC<2 z3>~@xofRi`?O(A80Ddwx$^q@nj6yplKryVhjH$mcG=%fLd+ek2F8;Isom+_5X~CqU zyE0n?C5I^xJfXvFAJE2*<}dtGy+s1@tBk6vV$n)v(7GF6I6@CD@4q4?h6 zSbIV{{yy+G%T2XLvw2?0ZIZGICVNRMa9GixG$66eQP#g2Z9i<++M)|)gYRXTHdp8s zFES4zR4BMVdGzR_n_*`-lb`Hrg4okxF~&I0CRPrC7)#Heud z1VOM1Ti5VZUYRPn6Qye-#S*-s&fis|qPm$y1Be}qlp7S%p8m*R021kkXeS-<#sf;) zX@6$*;-cv-kKz_3O!D%JfAu6NEI>(Az*ZvVlFcp8_FvQX{rYpcLpINE-k%&hhzxm= z3R~{H!HS#V7K>#YSG^Nw|G}0r6Fl!Nk(!+xuiNX13tvYY*FAzF!TWi80aG_*5Q3}& zhJdnD`J2qlHJSUmL&o<5%sqDON~b|1&UCS_HooS;0P}nLY+Bs8Jq3mm(fM|_wvR3B zU;gTR`jO9hp$fsd{sT?-8=uw1MFU|KvLM(5JL5vuFD{a8S$-j{XN;=vSZOc6n18>D zmyg2OvGb~%v!6cQ#QcO1p<=f1qG+mvO{$**}+@GKfDQ$dZ@ zIH+8qo`;B;JXqe^o;GP0j5eaY1HEWhG+$m#quc9%cC3kcr;M^eveW!ji?HC}OVw9o z%Z-%(xnpQ=r0~z`k8!Tf(N*L6MtsP@LSF9Bj)@YzaTBT|g-cBvSCD^t8*W>B;P962 zy3b=@TYfv;@vUn4T?j;Er#bFFy)0XxGVj$g$gPxm%cTJj#}Oq-w2O3_`6V0>Prb90F?RuqSm@zVMJ9fwL``tc_j2PUXu1Zf6KaA<%~m{QB_KD0zJ zl6q(6%u`loL7zUN&{TE*xq5wh*Y)`}NrL1agE%TiScb9FukuSXu8X1ZbfHbd^EES5 z24ThQ*&*VQ26EjdHO&79M0d#(vcT74Ik?!)@o*!`Aee=JO?JgL=Aoq??pNtQ?o_{j zuXUHcxaj)2FYDSFqHqkiy179ofy(oTVf^X27t&^q^zi%s+R^^{#JEihEECp>Sx#Qz z+xNm_^t0OY=3%dtoa{<}x?_Ty%9a8oN=ML_vQcZ+tRpy0Mnt?PyYP|0FQE#bM$dsL z?^j6c@Zhq8JyK45kg`}@*TX4$nqH^I!iJP99io9@BeA#$a@oE9Y8|EQmy=xbY2gM5 zyW<{N&ul+$HcWSOKVzl!iWt(DmzTpe;Mz1CLhcdWG@xtYaM5Lnaq5fW{pGjI&1=$| z_oY;!y6L!8W%=g!6Hj$m>(2A6eto9CE=7+)<)t=Dq(}5<>@=AoF4kv{$JhnRR*sUUgKe_(ZEk2$Q{p0VWu49G?3H!o^ zm2?^G4-^L678d8s6OQ8lhE@K70}mNxed&!k47D)F3(x?l4*Ovk*H73K5-GQHoYlgGVz=EeK1Bs1pcck5qetI+`c#n5eV@v50t!OU)=uC{Dl9i=XCxc$ zd=OYfAg08BKDu+H#n%6E0bUs)fu#mpWn$t&$$Z}6bU;AVp4=CM)GwPS%ai3NOxS?b z9b<}ybG=O+Rd@Qq1>qLUoQ=$JE3_Y(LI__xZlGgo$KcBWJzjpI4kwjFdG;g znct41%9Cy8?XxKjg*<6Hw&)v|aga985ME(`7wkK^#aGwPo)Et4e#XP&kBaQ)&r9iX z*4K9dWtV%WZR=#yg!l34x`t`ZbLW)?Z&#dQ`?%^(K9r2i%)L8S?%y5P!#YKNUePGs z$^DJZJy!CCkx61|QLdF4A@sIs>idTrjY z`nH#3b-;!><4tRWI2swg4TOV@2?q`2nEqZ$lq|lxeYoeL=&It!$KUO=(*NZi-!t^m z3v18w1}(MeKkzy-`JE^mJ*_HHLMMODcfuCM2K0wu?ekNoZpZWgu&=MLdBR2Odg;N+ zQqQtc?yM_B;&)Y3*rTePYsuOZS#dEYOC^Uph{q+aNX5SV`YJCPWhH%D zm|Yc~zcK_0`U^!&0`$e!czVknVqMhMZQIt){FLQ4sF%$5{jR7JR!qY3*GBJ4^n6`i z`-Gz%ledvVC?BqM3WGAn`X=%r14XwLtG<>nO^GtAzMA~6>*yQj_Q`e&O-yGVmu9Lz z#(RXi`|*j37B9yA^LtdrhqJX1j+cbeV1tu{Yd#M|l_JQtc5Gbu>06Fvc;M9`yUIj!g~UBSX8w^_{QSx> znY)b{XFk`N^;ca+gX4XibG@xhiV|1Pu#m^ST-mUvio7>DZ{Uc+Dp@XzZ(>sB} z%N3M-suBsQ8>|<9vb@o8_uAk4bUMJ~P^8B1ib2}h-0<8$mmlSpV@}W~O?O&(*K>}? zL>|oV;oS~JmhbwdU+=u4`*n%WgNB+q4x7wFHs1TokCHWtCl+~pvo$sumS9%>0%K7O zEa!>+CM~2o<-gnKGhfHFNNeW{4{IWL`U~chy8~5r70{7SlLV#L7GvA3jUK0TLXci# zw+EXoEK`}7Wiq2VrZsv6x?`2=%I&A(%zm0mN6Ecyi;$_wO%EP+?bGs*^jE9H<1&L2 zats=M4Raqz{IF{JeAdanW?izM0n+BxVxo5^n1)#lb3eIWto%*U@sa-*ZEqfxbNjyk z-sTWOraVc;6iI_flT1kz8i-7ZghDe?$e2n<#nXgPlrd>gnG%s`l9I7WQPDuD_UoPJ z`~9uG*4}IXwOgO%V|l9kzOVOno!5CD=W!h8QSX+PCDz|>{C^%&&3f5pAJC@EU`Fewx(L7IO3;Cg%^-fBbSUFKi-2d zTmSfk{HrcS*!pCM!^^$}56vJIEEHY>o7_u+5ZAb?^3d|U593yR&rv?3Zofzj>LiMK zf;ga*g}M{haDP1_aZg#e#k5zONcZrv0TArcxibZl$-ALqvXlt0zBiN=<;K~xdRX^J z4|kb!hPmCcUheH_q#qV5SN;iW|8q#<#HmxTSh|&kcG@{X$_H7<4SQjdUw2tywnlX% z{wkZhMR{b(1w7+qBSU&ySolh21}%u4Glt&2_jTve3O}5XwC8(xNP zU9)DTn%zh(p-&3WVxA>Onetz$alo4ugF4-LPC=~B$>M=BK zx?$zpOyW~?+?1-X^SXwgj@C?2t{oGzLA)?R-|}D0p{|D+(xcdNd=GsbKkeqri3*V$ zYhv^L%?dh>mVI(v_Zh9fN&{EFQ9Jv#&Wu_T!W2t6+kSoaYs$+rhYmdf3#F@XG)rE^ zeE(33!1`6YzNzWyFJ9~)YjpYkz*roD{NC|Q%;Y2m@~s>t{T50BL+ib3)!i5`FU)n{xFd0JO#8L!G;!6P^)}|_1(Z>d zSFcu6V1{Wp$va#`z+Soo2w{57hji@AD%;n$uGm2TgYh0h+QUdC@EW($<0H@~b!;r#Z~X;6;M`^V?fnPO zl0vRBarsRQi)h7?MK|yE&L3$JZE=6#y(e;7bwl5z6yJNECjFj^o+__e9mBiEOsVRn zdDi-un;%|&vDTnqReH)r;})?_Ul#_9H}4*LA@7*%49O_(_d~k$+fLo=v}4D*zLl`8 za-W^+6T#CcoiO;8qG?-xK|uQUzlrJmRseR>?kmgfWzT+oUsaAQiw|7S3=LaY7_PAATT(^{Y^_pMyU)-2!VrG_}o~|)t2HAKK!(_B~Z74I^F!XLE zQLn;&o?TTo&=`|Q1lBd_?blw0=Dnr#J-Sr(A39Wj)v9@yO3TV<3GWzvwKe|mi?sBZ zNJ4E9Q&&QN?X@i7Atc_f(wSSiR=?%Pck6@IzsPtYURt%GK8$MUnsMga(mYdZ;USc+ zJ(?5_XXE`$aoM7XC^MSg$3nd6stRFu< zkkILvvTNwtV8gPis?8ai59I@n248kSL+jX$e}nx4PC^V51{XBsF_457h_bgB{H&+z zSu6}MRMgc|U1~|HqrS5|Bh^dgsN5q97okkn%Td$8-=x_3){fyJI| z4{17ZPA$0LSQ$sb0SI?t&@nGsV&EO5Egn zgvSs4ivUhjrN17;py*=9on^yf>n>&woMc~hojeWi6(OBe9V34xl#G&$h@$HGvE9%P zu}{A=Z=lVbW$zZ2*VJ5#OTihidY5vaO)D*%xYN~auym|`deo**WMuT9-2qSJN^0L! zMW(vDH7U<>{^O_979{ZlG#i5#Z6MWmF7L4=S^K`m;(xP8kCD-(YYjF}790(9b#=+z z3;$*lO-cJ?#s7p3b=~pUEdHnX@Q#FWNoi~B!GAu<9i<|}uYZ78SJaM)X0zKhA2v6A zKNGTUQ)~0kA1hv7*)+nw;qvm(*8&dG*~KO-HC5+l0TB;Q2$=2SiG#72yuGzZfW9}p z&s?`$JIXK;q)c(wq5EY$|A)o~|(x3u6@qvAoo&&<{J z7ghcuo!DqNgJANJ^JZj^fP@NmjR`OD4Po(*gEMuGdC#~pX4fmWvTMz~CKyQKJ*(+t zR4Ux)f z#2ubJ&{$v7%CGFV_Tv7}b{a87Y3aVwD~N|iw-YT6eJn43a<+fvru!OOMrvW7C(K*e zy0vmb12WKx8G&N5GJAfl?K4$6smV{`2ZSQ{W&V~wxFXC7?-tL!`aM5f?ina7AIjoD z$Ma2YvpUu0emweX?VL(&-O9qcO}!4Rdv(< zMCP!qN6@-P)Cl{q9R7IpGXb`Oni)c@&t+WDva;&<|(2kbCEnRaj5 zMoC^Go(9a9`{Rw-~jLllihprQ^5c`@nkkE*gG_Mbhw zqIF_#A%pFjIvyY`wz)XsE}%(7d!2l9`LKiLF?~}FDjXKJ(8nFT`VBqvuA0P;Gb4qhY^2 zT|8~4+?TfH8s&c~`LFlB=$blWA>G+uI+U^XYWhkuP4Dz=4LV8V9HmH}x$#1-Hk4xS z_!PBNDy!z$N4@R(K1lUqz$7_2G3=+AsVx=O@hiZo8h7I8dxB zU@c=k&ghQ(^~rnkG3oxQCMdMuyEnZ9QnXyTvcj$Q>mgnBoxQx$H-eo5`VuKuBsx}7 zF-5mHTd{2u?|&X@-As?KvRn(x;WQ@u2;{Yb*mt~HEltg1(o&xDN%Q_uk??o>dZe=2 znB3vcty@KS2yqL!&O3Y+_q!wQG?N$f=BHFgpbe}*y~MYkbjG&`E~(YLPHVnm-va=1O!`2j(5DcPb-sDp-M_SS z8ZFZQ4f+r%IsHc;7p3LO{)du_mLB@6qWJ&*#*F~~qVO{ET}DS$w8P`ZQsZ-aN5`o4 zs94847WG*{;|eGuVfxE}aO}UZBqg`*;3`|fcSUt+Vj|delgjL<@*Ad7hEwBnfwBrd zC?T})1v3-7R=8y`_H$d@rgWX3)eLU3Kyiik?aqXhd@z? zq_B^<42#Gh5}Hz!36BEM2VAVU8TAW|cZQst|EYds*+hSRT3oEZM+`{E^@O<)o3~%DZWkbX>mwivxcAZj_N+Kn(a>cOxk4IJE}jW-l6rVK%YZ zu9fDTzdk*q`Su;{9~+JAnqCXvQZmu}#@dK4VIBW|ojuz^-Hg^}TNSkOGN83PlOa|# z>y(s~aC#eLQllUsPMkd197gbQ_?bVH3)~?NL10Q~fVZ6lsz{qMZ+ocX7l|GB^B-D%y9sxh-F@BK3r6M~m&@ zNS6b1^<&K5-`zf%1PApwZvb*Qgt&hpb;fT1%*D9j>G}qlF}atxywMEM%fN3K(FN?= zlEqJC-0K&EfLX*KcL2nGo*_&UhlIGJ^l1O-)1_Msbm z9Rw4{q{Y&%G`vuM&u9bq&;ql1U*|F%fKY+poZqicpFA5?$J3i%eyXavve|Yszryn% zLkm@h@^Mg@6fz%?gyGVqasg@4T+m%4Cd+K52^|Jf=Hg2@z&us zlkT2BzuNY>2frzs(JZPW+qh;@4S|1?YuhlRCRxS$*(v-BMsDw>gm->K?Ac8~_4EKM zCF8NpxqbVTBFvUeK{w6BAAm@7Gfg42~Sk1`=!{=*rp9ct=n zj%!QMqIGWLP4NXrw$&q~0#{fSDwr~Qj~`YoI44J+&(dOf?6Qe_;;Q&vBA+3`FM}bW zUV$PoK`2T`4?3$j8iRXYGeh@q4VTg|=ew z#33ga^VU2EgWM#-yWCjn+dlP#TBpGsl90GwwA_v5zJz>k<)wI{A5W7H*Easd-Vxoz z06kvsP3eFmV+d9&QkmTda#*~>Dvtx_bL2=wyS)=ZL+iTJ+z^ds8;WL zuZs#gI(QGSg)Br6RCb;ys9KML6}h8A4H9{3pjqr!?Pa;lgi# z`cUkf2RS%7xif<|o`Edj>hA~vK?7*E>K%q~W54Lc*|RgO(#4&7h78B7%!A<=#e#V) zeveR9ElL?Hj~L)nKE&M>5D8k_8Rsz0-o?=ouH@+WJ*b3X!xoNZ?~&?|M?-5U&3~DZ zF;si^w4@85vOWJ;vS7`}%$N&3Bp^yldVK*vBUF zaq;oxG4|Hhk#P+b$p^BW?*c2CU0d}+5!e-Lfr1ZP$B(~68!bkU68Or)>vOyZ(=z@P z+s%HwrtBjmK6b0AY-WYl=E5Dawe{%3^h6<3t4oE4vAZ*UDn-@vNm6 z(p;303sW>-ZPs1Ah&MN_d%NG;FL7>36mPGmEMqLu%oR7|b6baiIkU9$UNXc*_*azf zh%kPG`~l0VZB+^Z&#ed#shJY`b*|ubY{w5X1@E-(vySMzSppE_0n^6M52Mmp<*6$*y0w zK9`(BEF_s>_wBUV9~y}r1_J^4Q*y|VD|xn&ODeyA&pMkVXozT}qpw#C%v{DzM`FI# z^NDD4!=!-15AKZRoL-)VmtF`iLO zl+SZ{@4nHMaRkK>%LrK@y~1JPtE@~uz(7VOjIRIoDq&C|hWHUni}CtI)SJvs;apk7 zU+d!eicc{RaBUsVFKZtjgcm_zw7E>iE6~Mb9vnS-lm#jgEUX0sKfazlTQ{z76jXMv ze_&;;NJWSfWx{V=S@30rsP^cZz%EPs4;<(KF|p*3x)5e_4~F(AhhePp^7AAG1;H(t zws%%mz9~ZyPo&7u+``jFosofj3Dy!Ej&1Fe(i9F94>EiVVMq`$YufbXHyxWZac~lP zKgL)`eJ(F|;h^!6Nvkwoy#S^$G`ua)(=4G42__sYA!n4C*8pG+VMDr!4kD`_Crd3j zYScP*OGhap(v^XeLqBEMDjiwbwRkRIm|VXAek+n6etaR#5LhzT#?AFi@0p+w|6%|GB;ZMGxx+gaXERG>V3L3}4R@T&(@I1(;+Q(cs zgwBrAkAyo-N)>(ygockF7hAtLcI+4&5H=l1%m6(yJlJ6*ZZjaCkd&k}>vu8~Zmpg; zDe8#l3Jug9BVkwZrNoS>b3_rDrKX?ne8I(smG z^;TRBx%$v2s;a06Gm9?;(sC5q_&O1tbNd|&6@E86fy|I%-~`5f@7vp&U=5OWlWp?@ zxX&KBF{{gEs|?Ese-togct>(b3?j+IN&VA(GmKHyb>8~{GDv#gMZ1JJw%6mmov*=OX0P zw8J+gsfdX$`4m%KRaI3})BbdPm>@o65HBf2==pA!r;w`-6-4>8@fa0Bs`xj04V|{_ z11oe)I&db`16zCh)iH&QK8Mxc!iNTS*vZ3W69@=!9 zwlXumAV%KYED+us83skbHgE!E98b z1DN$>SE6L6UG8)GdnxUN*p-UczV|>yDM&#UTF$X~W_#-<#$lS8D@XbNtK+fCNbRKU z$SzjhMV`&|EE8?Y&q%6K&5RveO0S0~u@&`l>=Uh_rCWM=nm?Ro***I+7c9V{z@b)& z4>gwi2SR}PYIh`6Yd^g_=8g*>^TLrZ9Fsmg({aw(z`*g6y&{)v>Z46}i2YkI$FYy@ zH*+(^KyZ=D<|~_~9&ck+(hd_s`ND$8$b7k}EcuWRR-%sGlp zJ+q-pr61Hq!{Ghf2R-S-BTTo#3s_PdzZq ze)p$GJrge=IWTO+wS^WJwquJ&`C}dgDgO^#@apHYja&g;1}C55I8uQ^I?0|)etG53 zdgt2*w;0aKKP)|9mxjzmSsVyB+^gSJNgYhqLKsh2rNoB4d;Qj}-s0i{{ZBm_CNU5) z<00_jr9r{fC$Ma2;Xv%JQxoMPQoDe2?NKZrBvrjdiE5$ zbjEVRuz;lFBQ_2lG$^wD9;_T$uazmggDrV4v-C0$&pbUPE~T2m@C=~XMD6mA|9370 zVgX_fWieHE)XC6Lq1+}nqO#6$f-bOqYiqr^!RwB#zuX4B!$cu45Km*Jmz-FCbb&I)Vi(?1$tkEz_m}{sH1EhXI%)z_7 z3`|&%v4?hITHp>W+MaUf0Y74hn_I(kl$;t{LZrhDngB zLd0PMaWi@yH`4y?<(A1$ipi2UW4iV7=~J{v`|*k180LO)qjm6!zti2~ETjhw;u>nm zUtwPt8}h#dk(+%kjNf1>Ufjg-%l!w^2|x{b!oo$1raDi|7UODIDol~WRfD;?_>J(G zqn#5J-hKXBTKf1Dvm4$N7T&luZPNcCE6@fOS{N!nU@Q&L;{-oB1v&2U<8_Vu#9#g& zenowlPIHlKddR!qBCQOT4~grnPh z&m1Rz{n=vpkqiUdKxVbGrbG*L)~9Q_`2MIMwasRZj<*dWDY|S+l7IJ**}dk&@XDyA zXpi0;s9(@{HJa8Rn*SPrzu1?|EbmH-`&#(<^Yj>{3!VDPXm8oDA$`xP!xR~OC$5O0 zXU^EJNjnCY9d6rsj{L#Yu_H&0R8;(q06%f}Z)L4;btVP?T;=s)&`^>^*j0tM6sBll zE?&5R1<1qNxNN^73DkF6FpQT@X=U4a9S29hID6vYmgZ)N@%Xr%usrE5qjv9m^#GC@ zpvW@oZQGPoYJ>39E`ayIzB3y(wPnI?RF_^7jpJ2FC{P6vf~d&j+SYgMA98{9>BojS zK!u82;s%(D;SgkCtT3(-C>T$q`E$jl zXfkYoe2aBKp0Dn;-anDmw z44075y|k9~z`vr_D1Xp5AZ$>+!Q8=X?(c)XX})=m{F0;&X;S6VxCjLS)v{ zXZs{&L&*P~$^{Q)Uw@3@5o5XaI+}B%OcYn#d5(`IZ>Z!K=*D=jp*xlyHymfxX7uuX zM$57@j3p5OHrOLh#C0*THP03%>~(w$S=~_t&^V_ z<5g9a`TgK)duEG!9cKsL>RlPgc!2JE4ue)1AXJI8=qLF5Zz%A`{IaYp`MgY}l9((t z*731(JNof9V~0a%0m)bsC`W^P!CqY@9EVvV1<9juW0p!{cnB?M=fj6Nb6$?GI_gYF zF^K#@g2l_xqbhHGBVVBJ9ea=gxdk$*AMJ;D;}(7uq($q+09mFTbPxl!WbAY8!%$Ji zl<`3p8W_B`QI(w2N(lFoh@N}$c%2243Xr0sh`eAdSJ7s4W`g?hSRvV&pro!|lXqoE zl8R>x@?MhJ_SPDYI#o5LuApbNL}9FP97!5Bj1c;Dn6KPTOuX6SfY}5krQ^x}^qbE> zb*(o1bb_AW9?&HLs)Jl!0jw z2H!AQZMxRAoB#FH`&4tK_339cq$AA8KFZH`&b_=C08|I#%9y^a^GhV#FsVuiW z*7lw|lX|=)Hhk;Q-283RtEDKdT)rGjq^jy_Zp?=7nQH3P6;V|tO>KUrLx!KY3^aQ-Rip7HS}_bIlo@{c4Ln&d?Lm3d@zA*9SqbFJP*-jb3 z$r$#lcu1G`ZM><(q$IozDY4Gst~_BvB~*Q|n_rM?EBqg-jg;&~s0a!UPM~v78VK6P zqkHYpzaRprwoz%rnIq~F_RoP0Z5SXnN=>cducg(m8jtoQZdE=>UB2)vel0-YSw}bE z%Z}ftCM;#LYCEM4$$KkCiPB1Ibou*Nw?X0eT>}Qr?38N|S0=^#oD0Vvw7|#b1*sSE zg4^=hMmhN^FO}vjrkzK}3_j3+BNqr-s92W{uT%id2d zK|_UF6j7hQ%gp+pN`<$6y{6{ky)biPt+jO?;r-VySHPZ~=B;4I6OMwy{BIZfK-_;_ z=tGyxhK*=CwRP{8ED+-!x@tBn+sGTX{F;Lv9=|>wN_?

w^>cA@ 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

?D~Xh>c5@b$gVDo{+5pxhKHi|GJx^=*-qa4bTca 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

<(zmRKK_v$W-p~*+u)?m_v2Q(ux~^ z;0Q%He#*Dkw09b%v{D^?zDQF6F5>00cPK8o5n@VUas=!zW-TC_PZA~IWehhqzEh^k z-AisAQsGYkfF04vAFk>yevBHEF=%XXhyL&(a?+WM{B_3r2Mrv^f?@1J9n?L*&}+h- z)%~mDG6fT? zGzX-<@?;)bynY5>IcYn8hGFlS0WQeze3+HRRqgVwh?q~tk&>1M_AeM(SXc-* zdM@Lw*bs1Q1|s13W`|@mx=^%$M=N*f0u_Z=uO8f4K><5NaqN+u?d;^V4I@svXJl32 z+%!!X)jGbPf>#VPfr3++ktN1`d-g1=5x;(;UqW0%n~5e*x%pSS7N{Q+nK!HEwQ+-Z z3}~-86Q0NQ8uR;6sS@ite3AQbQy9Sa?KFaVp9oGbO`JeJ!djsr93a=VYyHQr~uu>e4D|J}Q!ExEw{-ajk5EB02tp`TGltL|R0$`cVFoRFNO2h)CcoE&*duMeCq z+R2QVw|@P;T)%L^IihtRk3v&jy&$oT(Xss`-Jd^x5F3Ku9)`>f>_oO|!j;;(A z7gt5vr(4gSA+6u31o%bt_ds{>(AbG^5zktt8K<-R*GJLcCvt;0MnGsN&Ok-@;?Kxt z>GREvPX*#rz#a(Rq<@4{`CL0Y+=F;Ek7r`7cTD}_eJJh2q5Xmz81zu6oTeV!&b|#k zX?tANnipHAp2VA!9S4*BqW)_qB6M7` zs&a(CY;$z@9nhld*AL&&Xw2({yGj54_b*Ml*R!qgC;pJCcdjTrcysz?h0}<{kis>o zGShlFKt}4N7!2)J@Vj!2;@Ixeyd0G?#``HH5#%WdNJB?rfltJ~_YZlYX2d_*K7P<> z%voSRVS?s^kbY{r8O>5i^6VJ0P(amY&7|erH;J10I5w1wq9gX`3)&QIqei_Ve)9se z)8pGM|LoyrF@;XBBh!BVa;|^jIelwE5T}@{3{3$;6`Z|SuM6FBeJDd;~Ub-x1Dgzm3_Rh*ALC5Nm~h( zYvRv|vv`}dm8geV_L?{z4mE_qBV z_dLNO;mVaRw_Vq-XGiHtvz;cvjy$;!%2MB5t^NON6Yklp35||TN_xj?X5vF$f4*e} zqIWo9InQ#9_a`JIAa>Z-Yh>`2xi~7`Cu4e-!%(t;%`om}DshW#tQVE|K=( z#}s?dp#db_Q%4|t;^ss;|KKImeBU7VBFjwUzKX=EyJBRK5}o2mGC*rc@F6v!A`>;W z5kskxIk|2(b;`@+z`MJ23Hl9le&V!g>ktLPOyIQy1krH1Opl#>WKHxWe1i_VajS}mq*;D*{5Pma;Yzi_NDdITwLedS4St7 zX&}uJ<$?)S6Kw>C2(xgoz%rLfzk3Jw`Cl$Te>H}^&Ey0KDv+`4ZRz-s*U@{NIej{* zY6l`^jxyuw9X~6>Stp3)V!>LU?OBf=4K_2oi@7UgL;K*sY6&n*Br*e!kUf&<+V8|l z3T||Y_|?;!)>Lj7Jfh&q$kXeg6HM)|w)awKRU9e#$H}~?!f?5S9O-&!3Uvj?)D!e5 zM2WgXgvlouA@mcRw~(JWR>j}}AZHD#SnZ>>apxjg?D6;8(cUr|%1bsW^B+X4BG0?VBn(m})_shX zSxRLQ;m-zAGAXLkno1aeRdLpb_`rN@4UPhH9S>m$l)Z%PyE z1=sJcAZ7AkMN81OHcD{HZw?Z`GijUuLbL`MU_?pdQr1vAYQ%`7ovj|!N}REAWA|X- zfr@o8$a79k+}*n!|NV?nnF>w%`~g;3b7*h#it($ZJl?*&g%qsd-v7Qp*nDGe^AXYn zA|qyWqw&&4Ns(zp4b{nC)UEFLwHf@K(q*gJo=Im!HQ(sb?wrsl$wc?g`#1xKGCfkN>yv;#&> zk_JM+bR1T2K@&a6D3th|5Gf3B+X0XR<;;xdZ9<#JX}bmZBI*D;sWDF}0+~(hC6^fw zi=Nb#3dj%I!eb+xg5XsDx=7VaK~I^2zsx-~YE#kx07#$nseBZ+lV$+)mb9-s7O*v6 zXEGQEc54(J2#X!E19%pprF;DR{DOkAfV<6nJC-s#q#`yvInKm{z_k-j9i_sT8S`6C zQeQuKW6uq6EdT3vwYc(&Sx?cXn>k)+>n^hr(*_3*np=v>UwVX0;O=`QH)~z#vc`9lgc1*Fg-JhJ?$_xZds${5w5KoZ6SroVf z=tDw)s=H*_G9S`Wjq!IKnHTmU8bmL_k4eAsZEcRFfAhpY!?G*c|L`c`Znwcir3wIS zB?Y^=xk1U9#1Rz~#+?QKayDOe{d-*A4w_W(YuJYD=E5T9&%OUJ&ZSVKz~4v^nIg<2 z#6z0?@Zn?<3R?o13$_jG!|G23e+xWa;kKMIatzqFZzhaEYSyU8NUE?>pOqUA9y!7; zlYypbQ;J`_d)K>Lw+04aQ252w4-X%PwAxJwjot0%myXVj78a;Wv_nK=K#_aLQ{2_e ze3~7fml0D7F}P|{_FyMl>)k5JMJ8o2N2NPtLmcRjxZbWde5NORR(`d%GRHdl_umC? z-?G0-5P}^cq9k0q2D{9gq8nphdR5kB%X~VmJo#X?$!9qQ1v(K;tH?jn%;#k##y)&6 z?iM`%9$^!4AK12ytxuv`Q1MdFRYvMAg}k(@ZVLMEt2sz|{rgL~i{4dhJhO}xTNDw- zG@@#7GQA@$c@i!ud6NX`<&9b&e&{WO+hnPVz!Q_zx zbnxiWuT^?~mVjQeuKwr~^w{a$`}fpeOBOA%nLPQbeexI>wdUr&nVmiYp%7+gDNeM*@x1uS!DbK;K-$We~u^bcp!o#~e>+Bi1 zmn)st*<8!HIpkME#gXLXWUOQX5bW&A;}%017vx7%ORa&Tq}4G?Tg7SBs!bZ=;$9xd zjT=zK%=Ulcsc8ugaCFJbOH;ND-#4j`jlXX%SdF|p?x2K#c8}_Op5ACi)yBrhtYSeq z{?;6DCqCYdwuB9owihzHT-Iz!^!o*!|1C4rn$J=l?;7&L$^aQ^uk zrs0$$D5Yi$tRIx)a?sKvKXxKdDJG^w^fX+eA}Y4pmhGjP1FmJhMJXw6aF5u)gC;(Z zLz#XvRL;%OX#U1rBS74hEMQm@mKE@RF}4(iMMbvf=8W@9c#xIFTceSTl?yY^Wvod& zV=JN-YoZym0FW|_W03@|V63@U+kKxxe!rU7q%r{)*WZjCn~&vflwHF4^A9pJt*1_9 zIrB0rtB;&!K49U*{q0ke>Ps8y>i(j=bpChk{h9}yN-Zo>soO4QU^oB?7P#EqLlYp= z2~jMS2MK5k!;6%;9@TU!41OvB&Z#{jzi>Wjh2Z8?=5^o~MD2iM0g+NsTn3 zPI*#Tm>S*oSvE9d!70_$!2hZJj6yvYZUn5Vt*d)Sk|Hd|=(3drDc?_pJ}Vh!3IbdF zdhY+k-^Q`o@#CZP&DG3b6wRhlUWmHX_yL+lJKCDX4Fha6-8T?)tG^ngs6$7N;DqE> zRWO`B1hk{QwF&jwb!A8lw0xQ^zaVU4P822X;Lc&hF=(y1Z%0*Sl$(D0>hdCYmm=Z5eYA{Ebf z;Mqhl?V3M-!}|4^n;?O9^NB>Eay?mBEH3<;e~M{`Y>89(>2v4AGLB1~`ACbAo|en= z7U>>gt5&X*6i77ELhX+qqyH35J%W1%d*>=DDzfgc1tdMAyZa)9wO7uZAp`VL%p}AE zN^ot(h+6)tzI1B-|%=_vqFQ(1M9l!<9%*2Bc|- z5PXSdQQ`>d>9OYt*4jQ@m?Y!v;ozXHtb73fR6G=M=z*{hJCqG_Cua<#g$<}zp{hb& zjvk%m#+Y&SC~`8cNO>DRrGH;zH*GHOz=4yHku%<{(!;4-U0of)1FsHFn&3xA7bvTY z6c12858t}GusWQf0#kJ0k| zL_F>-%3)@+&BbRvb8#3sVY(!<@Rj2JoZ5p7u^tm5&h=;}&E0k$dng z(nSA;H*0pbzJb9Em}nUePccrgy?_hH=Dwo=%I@E{VIqR=sKEr1=Rf>JfDL3+rpNS> z+e^|V;61LV?_kc#I4*nRRixqK&lI8zY3@12sB5lgx<%5rVcB$y9LTouj%(J0y5B=& zYPYLRf+dUx91Dm81SFfDTN6!yWvHv@G3M#PvEF>p=35Xq?cREc)f+X=|NGaiJyx+8 zR5zXQ+94PBYL@%~bt7sj!d?{J2&_GfLpjRy;@6^&nlLIs05~>Xtx?CNG`3m~{;j=z z`_FIeYMx)4Hg6vgJ!4d0=IXd3pe=k!9P%_qw1)L1NpQi3+H9ry$6mf8gn z92XJeLsMA70V2sRT=}wri+F>Gyghs>`o_Gh;I1+S=N#>1Bnobb1II_Md8Q7y46%io zVNsI%dderQ$fER-1-99tMZf&ppMall=l}7!sahCc+Q`3Q`T(MYp5ND9`imAkQ46@eoy;p0>6i z02fK`|3phv`|SzSkPfRcqeg8lzt7z6-Mj661GJu?$e@YH`0i_TP37htt}5+ex7)v( zfAssHYs_E@DOXM=O(CW7taEcp2RccuI7a2R&eb)yNVZkdv-7|s6{V%L+7n~#u<;1| zw2g}JYFY`;S#*pT>w1d88A*}4LrK%*mDjiCgVL#^TBOm2pctoNU+mchj-BZaWvcoK z2h}#;!^FJac+2jzv@}Ww)9*z!Ga}}Od@@FEmLv0`m{Xc(_LVI`{ris^Jg1AP&uUd^ zDJeRb$!DH3hJ9~+BQ|l4dB5>1;!8?PE9$}ri{-26{N37mKz;bV$B&V=uclMsLrWTz z<{pfW9;Ty1cRCyhNAROi08JGvAU^oan>`zsU8Xw@zB=G~NDRiA4q@Ty!A%VbA_1Jk z!A$|}QxE=*l^&QM7G}G5LmXq6l6!pQpmF28XaczTBFr6awr}_DhM*Si#RF0qc=_p__mj0kJ;Tk7swyyp^0Wm)B`F1VvFH@T zy|?b#1Uh>pDykiuOB^JiSY~E&aBqT)6B!sHMdPi;bEX3@lC8=`G<}N*4H`vvmv;Vq z@W?@fIz2DuZ55_?&BdrZ)2K(-El7uu>uG^=hOS((0xiv38cRdgUeOCoM+|Y`07S+Z zBoWAopcQ$p@UBKC72s|;3~e!cxEJH=D;FD54k`i!l1-cD{7 zSN-9mw0!rdv*3mo>;?nn1P+xut&p@Ns_(`#p+225jMpNm7hL!iJ$mQ6YCGULuN|XUgwaEi4N?dAjCEs&HEYMZ^L=02!^rIfW%-7GK zYwPPX^Oy0P7;oR`j4fad-68%Vj4oF7nf|;oGjOd!0#yk8z@pLrLM^suS4zq#%g$C1;(cR(nx;xoa1iSuk_=Z#~Lar->#u zR7|rl9od=$0L#HmoBoJ4EPdDkEY?tBP}ef%B(RcQpA`Mv3Rk%R#guR~9^iS;9*^>n zE|BuQ%0a1NcwINA_7$-!iiH^iffSLhXP+PY9Agv=mq!D@ihc8-91%53L}r~{XiFMZOu2FJDDragV0 z#rC7OvtK&`?Y}F0;*k)r^Nz)YTCy@PmiFk(bI@F5>G@u1`Vn*4$(0O27Ep(xm^vP= z8$%=v*iS}B`vM-3TbX38Y=AW1NDPtT9mJI>wddAIC%)#A+?*LmMb z`cY&kztQlxVK0wXdvEqNe&PKBKicR%FDmNb@dOT4O%Mb~CZdNP%M8h2P_UV%3Vx7v zsb1K!&DL|$3wyUwn;~|hdZmppfvTl@sDK?w)nHiCg3(^v6jx}gz+&py93FX$WEm)f zE^9sh3J|84djbA)B@z=2iNQec#ZN@DT5=>rK_ckS4Y_jnn>y92bAzl> zEizDRCI|-()SzdK`~vDtTaJQ(hNkj^xc?KJl*kJrOsmsb%F4z@D)|j|P6FyYnNI#H znwo{P1{H)buv`P{HPjImNL$NK{p5q*g|FEag(f)ocT-F!51XyV_balO0q}(dDCgrr+5D2 z!CLYVoiTw#wcjMVBGGIPCZ^s~T=c8{qJ!fu5np&U5U@Z|p+k9uIRa8Pt6t86jg_i=LY8I+^u7KJ>#=nL)f~exv<@F74qTmo+bQK72CS#`9L{ zvcd_=yQMB0QTQXQBJLm*b$HpF@0IFc%XQT!7#MXqJ8y!pQ-wTO07fcKDetFZjKb|3RTC2p2NWy1M7{23Dvq zShPsaoE}w8z@lzK&KQuZ+QhDJNT}B@Qw3p&^_X2&)HBmdB%6Ro3O4jGTq-AS&<)XI z)0iMsGy(vI1#Z9g#lUq~Ha(P((yDb-Ob{Z21e- zP+#Zeydyl-)z%{LzdrrWEBjU_z3oSP)Z8?Qgb+(@vemH0OFHq;Kl##?59D`oNExA)-p*q;qX9eyTlxBgqR2D`hq)|Q}_P4cYV>la;H4>XNoUbkVu(V(C zbbJYGepL&@X8^)1#uJ%s)cod>Mk%%3TdO~e%NaxM*H{>cke_vewyqf6=^oQs#|Us!g18G;F8uXN5FprTdX1hU;Sw{UQU` z_S;diT3^R_f2N!+L)I!a8in5b7=LD%eFk2PJVPIE8!q|q2lT*B0{7B*vSU;F*Q;qA zodnJ`}8zBTh{&aB)yabUiG%!?%QH| znR*ypsD9G^iowP8KA2-j3hBSbuvN^?!Xif2(PY?0&!qLaH!{A`L{Q4((R*XHX~V)w z@1U}?nKL@l?E`MyF1wsu7WUD5nA@1N`097a!3QwO=aeukP6C@Lk(cybO4OOWUqiKL zPdr16_d|;k1Q-x+Z^7(g4GhW7X!3C9g>YZQg@S%l&AL$zJ31!^=}icBq==f1!{Mg! zDA(AJl4-;aNC-WRvjJ!DI#mmHv@d$;<45bsz>{H8eQhmU(PL+|*l5crmWRA3D3IGQ zQ4koq<+RIDz%Vd_d$!aACVnz79VY>B3pXku9q`EUH6xWjN`#lMUw>&B+v`NkqM6oE z3Xd@9A!bmzS$?y-Y7zZ_-?!qoq?uz4TTFeRpjA~bQtuawSA_SMDve;U;g1iKD#PI~ zK;-gp(wwSpLm{R2iMNc}SmwR-4hrPfZ!{d}Ns zfbn|oyGh=62XBv?xK(sSz#c$fKbwbhz`WxaT|X>u*(Dc$A3!=pG);+n>w34~7TaR7 zRzOIQdH92Mi#d7(Avi?-J-6B60elT-+k*$?l$8gm$f*a@+r!vHbVtkma?L3ABbP4* z4F1mQyM8ZeZHV;Xnfuj;>zbK$4C=Sw&n-bU0%i;dB${(D9U8j)ZpyG6Wq)IjKJA=`TUd>QC$X^H zp+8FADhE$!>17Y9fg!sJ!jAO{75d5g7uIHHWl6U!pJgNyIGnwO&2ib!pI^)TwgrS( zZTG7iA?wO!n=r<0tbDDnZdXmniqGR}2(0zVQnHhn>w{t14dtqhl|BX4@9O6@Xm2Qc z?N9s%)QYyzbzeLAo7=!fs!v*IZV>5L19bdx_c1rYj2 zdiMA`5b~j>V3{E=V6M~f^Cv?kzB+*K*9;NW_{V3k+|Qe}9<3tpw*2A>%J%v(0}LNy z0X*yn8HUxcFYQN}O06i!_~Q{5Qw@k;k7IzL@{ZaP392>%d%*yTX{hyhh`f67;;+Qb z0}Lqt7TDQgsSAyz!n>lio0DgI>YeRBQ3Q-}W;jlmk`+L8%e=kehVS0DZwlY`p#@D2-lIm&+t zfv#xe>Zk1T>m2N8=P>XHv%wGwT3~Aor8T~GW~+L2o@B(_{6TY{2i}ai{MtXSVk?&o za}ogSt%DYM>OUGVVVIpV4O}My+lI%6ln3Q1i|<~&J2BEN@s9ap06!)$j(4Kw1l zSt_6VY{up-5#@2W^Ti}^y6i6-Ss-;l@A=ek>f>R-VND{sXm9}oS4gFAifs4Yyt$4f z?s#6KlfN5nG@pa^;#d&Oz7`&8UcD8Ar(3s>26fR(N>ir427rNSO$VH5B>HmG`qEtD zvxq%e&@UGnR;b>rg?#U>tFJ$O`Eo=^KN!$(Rk*0;!*;{q-g_51c-hylP@9_Tcj~HG zODJYC8ZB8rLMBkBUq8v>)`mU%_C3?`ZFAhPp&h_~(FnLx$fPDrNM&MkT}u;s0z^Jm z2X~+i(v|)+)4Vv3$Wq3hz&HpWw)S*B)_UpN>*^=ZK6acf zyp=s_Wag^>BF5*hhR^LcTwUmy8{w9FYeKE}JNr!^*JiFXoMtA4N~I@!Hh_kS zlOc!2FEdv>O^3 zZEh+IJQ`=68@A2>Dr7-%eWTXwmo4&Vla(azF7&id*k~Uzwi^Id{6FW;Wih=5;N!b> zlb>j3=f*HR>g7B<9yWD#<}P8)lf~*pfaDHJ>vF7Hc>q!>*|K%t((@NlJYxu`GVEPz)i3SCsl7!tHxK z*dy@|Si%f`^~#m(TXeLFz&{GzR>5!ZNgszw<3bfdU^Kh4san%nobh?vf5-`IPQBs( z=?+~g{34TXN)a@e+cp|@t*%jvTK^v~QhI8=cS*u)dmZGXgh!OTkj?BLg ztKqiZNVf+09&66>@&cWl#l?rCqjQx)LoM1C;J5AL zO@0**hY}wi1ggC6iM|vhaJ@hb_|_dJA<>c*ahZNvHxd@HLeOxr%P zjgBh>gvKAm3$btCjV>-vA3l^V+^s`@9lR;gX9GhB!r|hD3ny!H=l!%&+7Y}bS?qZhT#)BG4;Gp&( zTyoV)A6Sqsp@)-ufjXOi{gM!vJ~t9=NfZ=MY^ww(&FJ<e97D@Um)VVJ*2{pRG+Np6$Gq!A|4^e+7_53Q8+9?zFJsA#et0VmBs#|)M zs-~$^D@6tFu>|ONHy=3Zu@lvnNZ6PNvIS#i|0jM@$h2eyC*)IMNT?lE64S`mCZGAO%vFvM}u4CXKHrgECG#x`}NoJwoO@ zH!?L5CvxL=RMG0eEWJzl-Cx6jIgnDIOlHXprx9=AVIb*(9%6fe?GupvWN_@~KlyUABo(2sNm$SHoX9ZW#@v5v{wbz}0WFTiHG=w(ZhI0E zJk9srV&gTWHS`JUe~pKDCQ!vn3yYxsKK;2J4l&J&#=}2!5GGkwnLH{5p?S2GJh_EZ zFTKD>Js9H>P0c7;U2!nEW(_V}u>Y+JN1&5rVkh{44rf|CgHVueH)l>jO*w^#<5XZ) zu*J7sAS1#@DEM-Xg9%j+z9>BBML zKf_M?T9}*Y1QP%e&1M}|6yOXa*+S>!;pQd?*n6V8yC)%Z5bXKC=wuP+ z{voNTHkN|-z|LYqw9~M_BH&XqGblbW*`4pg7pN~l1528^ck2c*iU0=LgUyOIXPyfJ zdq`lkXlrY;vzi%8@&i_oR7A5?!KWv`S42!_Ugh_gntDT+t&(dK{FtjjPq>`D621XX zOAr)gteKhIyY~^?gt)+Lo?P`yh!T2g7k2+cI=)|<4jOKu&F4dPcKi>wBRw~F7@v*S z#Z@+mZk;XQW#^yJ=p)He-EG6mlL?e?U-*b(Gnqt}qrQ@{2bw6t%*wKB>ifHO@6MqB z3PU)+AJJs(0Mv;o{nB{{lIFn1R8E1j%+p@uPZ+5{0V6Bp6~OO{=89 zXp{9yJZFdQdvk_D=i8XF?)5)nKaOvm>$%=jpvkiCD4K$^zZx{+8^mZ?*%0B4Yg%X}1hITcvEG`QKYgsb7kTC?46oNI0 zLd*jUWouTFS`1l)ER=fxf1j&Mf3*t2l*m7O^ynB$I&N%im-yVt#{pXroD>Xz;5MrF zP+i?l0_zPR!--M;vYUpifURXG0e5l+8|!(`l;<_>Cp2|SjPJ4w#k`{#Z|p-;&H^#b zzBCU`4uZS;4<4ZFRULEr>eb(+I-J*@L;pG(6XOOFM`(o}RCssi5~M~4as>b8LUQ{K zFRyV8Ke>GaROCP?>~C{R#tt98m_WqO>G&Ai2F9{H6uyiUy&K9^c_Cq0V&7Y|Qv0dD z#Q(j&UKE7%Sfn%<-c<+A-tVt=$@;IJG)#Q5m7{4X=t)R6i}=f=1o{{$;R7osb#()* zV%gyfr_QYBziC7C^+z(8^!VQU&=kfRn>TI30L+R%`Oo~zVEHni6!TxbGDgI%r$X7a7EdzAaafk(JO z^6m=-KSK2q&!7W-BHHZw$HK%!h`G9Bz3g6Q#~d_N2{M^R8V@-ya9u%BNP8SF$m_Hx z*y_^nQ*iROpY}Rgnap?_!UWbX!>7I61NoH4N<1>Exs0InLllQ>@tirQFI>RoMr;jW zrBUrDD1@P5xc~5>q*!9GF?%HYehPLINa5r`WWI6zdcnz+vI=MahYP132WskW=(2I6 zXpWa_e9PmFJ?40n1r)cX5BZr{>y!B#$dD(ftgI~1k0c}}_S&vseL2ewO@$p>BOW__ zG|pLZ4Zh*uCr>VcrO>mdCu~6;$!Z3) zt5*|w_+P+zwcXu%^%}w$+{vi{$2jy7lz4nxBztjko6W^JzTf?mM39 zdodZ3l&ah=WRQ1F&U8G$VVN=m8e*oANt6!*80uPhJD{OZtEhRF3D~AkRoy!cU!oI)tp> ziC6F|ODo7Kk?2JC4il4NQ0B+i;H?Y)oII7ZZq$Lufw-xxs1lhg{(TFQ)^FR^%&C2WdcsuhvoXs0Wa3vMAm}N2NEk{U$gO-e014PFmoml+!m5g zCYx9_G&NHh5krCSg&PcGpSlZo@d*>!iEebOy~b`_nybkwVtP9&ueSvS#vm7=_Xkp7 z2!23ZnJQZ;`SYJYhu1B-cCi?#4eW!PM8D5*W@wHj>Q=J_*4F)4Q$*(Gpurf}!N9~X zO!@zAS=)-LcelPV>gT3%yhn^01(C3=s4vEuW-PXBHdmarnU{m)tE=2DiZkGy>+bkW zPX$53V1wdS5W=SgJzhB1`^QG;mLo z$kJ$RoX`YH_^G-&E+!_PUy_y%;wVEEpnjJa*IlYpMt71keK=7a48&=K7c~((fQ{M3 z$Cea$heB=FM-^Y(ZVZIqCtmSrKU}n>#bm98qw0h!3ahZBL#Zk&`@#LC2|LNLWoA56!pr0bYA!KXJLZt za@@!L{Vlz`SjF&$9ql|2qx5_I{D^?>9uz4uhc#_fkIZ& z8+4y7`0=+xMhbL1#%Uq6&7a@>rZsiI=7AM#76m4o-cs$-62yVR@&TrlgaB!eSuZ|x z5G|TU27hGIMWp{)Rz?fZ>Fr;tp|P{v-EZ;81PAy_boYMv3OFtxupbw$M5rMq@uyEm85tEItUPc)*|bw&8&foUA*C&0tFPKpW& zN5aErPMby*)5615-in9DT?YClQAK5)2qhhUQ~E9I;1x&Fff?0~{3~DEc4lct!>SR&B zU^E%vcq}qyWSCWvj-rhmt*Acs z?k{@ldKyfJPfna6ne#9(GR6&n)E@vv3|QK6RI0cltgug`Vql!`cH;|8#!ccv6{=a}P20Fx z{@u0wEgE`q+178|c>ZMtN-SU}{%92262r3eh7Juse7L3U*A3=wSZ(j?Gq$0jVGK(` z*m=UCV2XtT%sz4RDz1cNg$rsfM?VT`#Gek+#q1_`zcvt-H!U?h1aIYCNo*+k=i0U5 ze`wXbG5~DxQ?3$3J_q4w?b;#1j+{q3PZn18P*dx@F72J7%u0Sz=pllD;(O@O!h!-3 z{w?iAbjF|fCk`c8^~|sgBimLqDO>e1Y9yM<^>1RC)g?$1_(Kj5Jv;%$=v{T zr=C(&QeuF-mcLmxIzZ$9bl!7|L$xhOOSg1}@R=6Sg}G$hI|B;QP+*-}Ff zB6mNXm7%J=d%v^a0LK}3GLWFMWeboH-<|`wW5Z1qEV#+&O2{TGLq;s5(!IO1XVRPe zd~RK1{nQ3qR+#VIJ7ee02|_wzZ#c@-s(dqW4V-E#UtgwoWfz!z->^@2$a9=4E}*C; zPf4MEl3lO^|BaX#(PpU#{r?|R=N;GM{{R20kd`puP7Zh` zvwA?jC+g~k>YoBw1fLtrc`|nFW}ptdv+Qto0wJ$VE&EZ6nvedM)YasZN(TRxTBQdN ztx*V21Z}k14q5C(F8vmKD_fg?e7f@ zWJJqF23Gq42;oZ52%RIH&8^&1PVNQ+9JtsSqMRW`Q$ERHWtPSWN-5%ToIDUcy{nzj}qTj59NB zc4zh$ma*z7y4^eEJcv4VD)Y{rfP8y$ZpefZyAWCr5LXI&$gpsAWb-t0gynlOfl74T z@p=HGM2RFO)RUe6uTC65RmF8E2y_F~dNNnpKzx(%p;Q~Wb`k(9_-7AEO-iL;&tmrdD3Uq7>4+K`4WNOj6nvC?_`9sw{Kms2?5f87$iAI>;K;Z zLZ3Fnz<}raPcrD$sHj~@*OI;#Tss3R#kB>bjH4b>5*-*^{@GNbnsK)2je3=NIW-M+ z)53*K0A_s0`D))eh(ygAtSoHH``f@a;+PN~LPW#>>36$0Ki7zW_g{hMa)Hfd@6MZ>>18m{>Xn#}xyK87@ah{iyQ=yY@M zZ~fk_x%+fFvol#KS_xorK<{92oqUPZWAqsVuHmX>m0pu`eU1%CdOg_Vk|iyCc4~#E zPc{7KYW=4L5NTafYs1wyxVrM;(G!bvWJ;{PR;*f;EkA`;G&7$ok=joXC~7&PL?7X; zZQ5k8jLTT>4(dkr#KE1s;k^j}PHI7MaUH-izz!v$L3hW>*MA+HYxi5v?!-w^Y{Sti0>p&N zG8&aZ0u5YGXoE}By{yS`8#{aMTx+Ik@7ZHSFg2hb=dX;yL=^%*51k&|Ex>jc=QzHh z!-kZF@;Kpe$*xlOvjt5^-TVbJFScPkWj$jpHiId{z58v+o%}!4!P6RzALTZN!mTVZ z;DybD9xny9Ocj( zC*5kId|n;08X^hE2!a8*1NKYo6jWZrv$Ucu`~TO}Y0LTz&~;U!k>W~DL}&W)-aQ#X zIP>`)Y5^`>j;LK%!7`aXA$Y^Wa@&)ZaX87uzzo%gPsu?G;P^fjZkFTw1Gn=fSCI*;!eFz}5Q~U7@+-9k@YuTw|Sq zF;M7$5x@l>!Tq;w8v!tb4`-||fkr@J`R?gLwP2L2*}*qJ4Kcd1BEHwwGKgWHb(e@Yx&~IAz&_1uB2@LqQivwVL4B(#SU@$o~up47$Da zmagkyFaS{GS$e{*goKe*i(yKdUbOrBt`*3qOB7(PBLfrgLtzSwmo+Q3v;UB6@ zyGzGZI@Z$unyegNrmu(c%^6ap5OpQ@-|w{Ak${Hl7OKayHy z`s3f(%*ir3Ectu9rg0pP{m5&n~)T*KUQ3 zX6X6MhRJD_x1R?*WeyI55+bkUu3tRxr%I)33UKqcZ#iE)2V)K}f3ZE1+7S~F2Up|g z5rY?BTSMBDl)&`uZok&xI^fjGuSWty_cbgAYVNQhGxml$EKnJ3f9DXG!?dfQfwkZ0 zl)u-Mo*YqAS=qKK82B!e%(?t(j(ac0k5*K18O)7t*X9ttC!_AhElW%JE>4~BmS}UQ zI=*0DaEq0-2j}x}*Je&YQepF}x%{?a7^!HmvN!=xMRzhNRuuF@;)K7suy|bb`<|{` z6I@S+9{u|M3A92oZ*2goh$W6QwZ+8!vR^xGWiJJ;fF*=0w{H@?HC0=?p`{NgB)yO} zLiIZ`({^>9y7Dsemzut&*1$^U*CS-|ubLJ6X!pI=s9AxOX?NB00A6R%%q^$!s; za&qix7STcd;j`k2kQ*>yzq7Mp4yF9FXUD=%1P0DSDe^T2eopEB_jUqRIUGzIfptj4 zj)ubnMPGde(Q-LBuUjV*J8pLLLoo!QG;PwDF&pU6S$Y2NKUP?V@_L-pe_xX*nO>1#t-w@?zq9aRJJ zom#y&e$Vm~$Br4QUcAz7u-IRzcwh-MR2?{AfU&Cek|lFg#n#%WR2xOQIY?SVP}8~i zpR_Uaj>g9SV&sH!bWQvokZQ9uYq8L*sYpIJ%&;0z~s; zG&KX9#SLEUfP6J5?dd;R;+@jZYxtVPBD;i$GHL>oElt_bzUBAm+oOuvJVs3v_=J8- zACh`wG0BSKE2zOxwf}pdxO{t;MKgKBsx?0;binbHVu5o_pIXh!2aGl^AbhTh%*C=yA z3eGZS-KsQ*`S3tK`PJWpOA!sB1PJcIqN1!4=>a5BqPyDH7nv**gO?I!$<-7zq4aD) zYu-SweQ2>_*&O1e=Ahoy{kM5bW5?q^?d7Vy37o2XJu!)iE5g5EKz9`$%-og3HJ8%3YYGS`%N zyROu7`Y-z)2qf2*e9GwP6%3_R9mrj?%6zJZw9lLFRf_6@4Nrw9zAK$n5hfiurXSxX$wkg#2_csFM=WG&{$p|Ms^S(8@A{_QgWzKE}8krSf0j~oSpQzAvi*%*yRz`A0+qM;GIT`J$zBM?P00osQ zxHbd#$r4PthQZbkZXp|ur8M(LEd;ts6~xqzmsBVne*Ok-pQFm6o6AK&0M>X_+^VWo zyLZgl?J+6HPf?&#QMo#?*4pC0b&N~JCAme~1^xrywK=)USAVL!@#)WJv9UHcXQd5$ zn@sW|XSICv&D7Kh>n7;WT2SKFZNN}&miFoA-C}(fjJU0_hUt|-VzzWpg=^3NTfv*< z&dflGYbK5Iu5$Zh)C}h`=9{Sv>ns=wq7!7SYuH(ko#Hum^y>|0b^*oi;Cxs}ROVw1|^ z(N9~D!5CYhK1e@Ol~0-8JwEQ>!(m((Q8x?tS+w4M>DP47IRw=MM1^XuC;-?6`=^a; z5J&NK?lYnm$eDCy`sgV0Y92E37RkKv*m2t&e@ceB&dYwM<3}xLAP-cJ(QXD=1h?=_ zh;sEBSRSd>k5lYzY}*nei-GF^mFQIXJXzg_)~3ncOdW_Q*r+;n83zpHu+i=wyOp%G zf*x)`3+3hJrlUKcrqI-mD?h&3NTmwbu zn45thk0;aKt7qQq`9(>1WMOV{`>(>M(U@zdJ$DdEv5AR?&g!n|ZLz;d@x}Ov@tb<= z&3%xNp&<@`VznaW2a+Kql`P^U?zD&fS9(KdPXUDz_ry#zD@90)*y zge&$j>`jl|rYey2>MJ(9;;1A+{7#i?w8a6+ku#sSyh2J@^}rM*Y;=tiNa10_2Z9*DSMGc| zJ<93T{b$b_xeF^dBHcD>Fd2N99MKczo>=)SH%>m;wWRM16u`|GrGzB<+i?BkGJb(DZJeLj>3*Lp47EW`O^vrlMmgilT-qo&gd$zqEA=sb^ z{C?!3)d~p#qx{lQO+YCGIMGBPor zUy`=2U6p(~J-wArYm?Mwf?QdA4Vy1>o7r2MF;4B|S!&%n9GerPr^wUxd1zDY)dhe8 z`U*S{)5rLT6QQAwOdddCfp6mN`;6PSSz?N+s?~JRoQTv)vbAw0xK=GZnYr$_q^}iy~`W@g9Uo--3J)qGc%_)v_OC@7&YNxcJ`TN4aW4qHs4lu z35D4JdeaWM=xlwLONswkj%vc((s=xtQ~CDyN4_KD+Gl?MGGMQTDx6f-`;hX$dNezu zCI}gC-lXxGjc$Y4MaMTS1k`e~7DgX!|NN>q&|G&9XZJVOY{=-E8=Nf&UIQPI#Z#B^ z$$&|)67RKZo7`_^+9o}xd5?8BK@zc^*0izzI2lp?^9Kgcz6lAfT`2*fGT*XMWIm6Z zH|#QhQf66fqx|-_!sMOO2lL3EgYM|$D>?ZizlD-wvtvvmjC6yp@WLrZ%9WM>4Ebjc zf_)QEsaaThe5LK5vXg5i&}JYSs7n6ImaJPp=Ixd1@c8C3#!N8)Nh)i7yLJa~SoAN# zy4CvZ-FHCXr4ECvfJdA?weIJ1pqy z__Q|)c7(i&^gqpeHN8!E zK)};{3M78b+vvFhn4uFS$ozVu(gHj3#R21>+RVWQq|#+Od2qzhT_dnLTqi8z}p`ZlhMm z#^oR-EU=-4bZAos4U)K#&`T&st0?LlE$o;nL=6E&;)@6q!J{; zMnuc!S#5x)w2_6;Au-yb!0{oNn~ScL^xLBM4NdNhv))XyX!M02WtnY&OJZYjcL8X> z>uDb5bJFWi3ReU#(0JA?LPg{WJqjsg*JKTwaHp!CcE%X>wx9iQb#)>x&Jd#nSq=3~ z^^Y7teX6e4TJlmMd>;JCyCp?>AI26?pTH{WT991n_r-d1 z+y3}CURM`Wn)a3XA9ry4+)Sma!eQ4&sO8!sphHJj*G#Iot$-4X=CYKT9ppD<-^o_5 zOzq^G#X>nJu2r8b?s|=D?WN1xyf)*Iz*D;R&CSpeXl-2=3^o!2L;y1?zY+4q7lpm$ z@;xa$yI)>aHC;DPk%()2ESuG`lLP_QFsEP3Uy0L|P0P|fyHvad;X^K`^LRwC5wm_& z*G9lgiV2G`F;Za;5A2u^2G1HPU$V)?#rsoqc}OqMX(*Z!tkX@Z`(IyOazOU!0h3t7 z-ksX+1SBfZz0Wa$xo@?8>!E-hJ2JTo*jwe5m89L%9P@0@sVoWReq||4bI`v|D7E>_ znZw497n|>i3N^sy@s{-^ou#weN=odzXM;lMZEoj+hg8aIJL`fQT0FbY#HCQi(!%c|S@ z(SXTV>suPvw^;Ld7wm*mowWY7;yo^*a)llmC%PfqK!G9!C4XiAJtGERbZGmwbS8qa zWy=)gbG(z{Y*m(9Y}m)e_Z!gBE@yi{{CcDVUkJ0OVx(@xw>e=#DgYMIi;%|&b)Pmd zAb>+Co#Gtmgt;zQ(V-!b9@s@rfR9V|CU$0!yy3PU-rIe>4A@_f!ZQYB>*+WRTr# zh(07Z1zpAp0$|@l)W+Yew0HN1hdHZOIpiN5m!;&2T667;+zgi!%K&;sCCIt?xylrT zMsw%(-EBbMCITOzoi@$X@Tx!OpX-bA!{zKF>`~d}cso%$s;1oG6~X zxqt*dXP)mrDQf3-j#8G>w7X(pG*o%YzJ?GOkzpmd!K;6g+2NbdOx7RhKN9x#5SqPSebq^wTA* zR_z}FVK)N}$;wUhxEV00TB-C3^OwevkLu?>XJL6d2QL)nqz&(+&E9bqXlS(e$qd-H zZzI$(81=@DE!D9nVZJHr<&x}wQf0fu421N#v_R#VD#w;+fS0|Ys;F2--NmiJjesV7 z|CWx&-CkYA7R$&6?~;E%h;=w|iChU{iWl#If>@-^?028nQ&e1ME)1EhmS5JtfdG!Y zyu2;d58IK2S3wAlI2PfyP68A;!(`eDSJ>*Bx7+i^uB-&;590pq*RQ&=65rd2pr9x< zxplW3nsu|$ek?ma?w}gS;SD- ztPdBhtk|wxek?Q;pBiexe*OD1z#-Ek9l#eJLGGrK#mEpmG}*WG7_{}Fs>=7S8nEHJ zLq?(p#X_YQy%GWT@D^04bn9y?9}Exk0?+)r^Qb?zb7ZX}k097++?rO^c?2|bF~$S@FR%Ebe-M&VdR$}pV)EWlZfeV zS;cP%7%MS3yIr2GAsJ4N{*vsY4Ai}?gsHAoW4p@)*axel+@Hhdria$ z9JA!yjT2!#*t>g}PqzO28lk61t^+4%Ga}eKqYsfOJ=Qj>?c|g9S0>rI#DL-wVPU?` z?$ik~DqsLOl7tLONI3Wft&0u#C{HCLO#JIYHKY+1p^FD2I^H}Fg9;Z}bj^w1igb-P^YirVF5MgbYA!_#Bvo*i%Mq!u~ogWi+Mo^74^G@Xy8Y z)wPSd;O@@}VRo>jNe@evUK9yDZeF7K5Q^?Z=J!$eK!eWfr1f-*$ej7vq99yjKl$HP ztuE}J0a^7LrNf{7rbP8Kpy6?e1Hd{(ump9InA$$iP|n z_@pKw4Jch{a0<@suV3ngijoeGTVpf!zR^v8pDFu93BMOb>}Ue?hhnziXlo~Ac+f0c zL2fbM1d%rjg6E1`2w?>l{y)v1Kq+~(REp3_T{h`#ct?KLeLxf@r6=;8YDvFe9UYQ-vlp;oqhSAd;o16=uQR4GBC53MvpWu`g)gccer;-dBqHHw60x%2UL(WBK99xe`?G<=0*U^bqaIBK<#PJH?=fv zU=B$0Y+@}uf4<(rtvlDykPNGoIm*j55#7e^e>F+GTNZWfZ$(GHVkwb2X(7vFVG>9Gcu2nvK}`aK+z zYr>8lWA{N`^$%|&epkFdpQersA@Y9a%$XZ>!ci+Q8CHN-?ELwnKSUn$d(g4`qnC!N zLEeJ!>ES=4Al8iC&eEmoo!|3d4w5Ps2c0uA0++~5WUsN1i@dzVx2><@o0+X4{WW%} zrKRX>gCw5!yBd&0{}i)5=jpzb8jjkt{QdiqvPb_)=@<2$(o*XKW_i2_8AlqbLFOid zHn;Bz_kc?dIK%jFjP)wOOV<<;YX+}+{`@&5@kv-;u52(_1ZHB$J*5!5So{rBLbXw4 z?#<28xC;ywd(zV>?(g~6N55!r04E}mXz^19{7_IUb}7|n9*ME+Kdz#>i?)0r#1wod z=mJzv=5ZSV6kY1#7mPTtXAd#ad~QnbPn;_4BzGhwH2_Fiq0u>V>DtOOP{)CQ%D_8#%7au?l zy%Xsrm9xR`e$zuUv#714ul}XgB_na)9=$nJ>~Z>il%6~+*r>_f^H#k<+J7IQ^$`{p z@O*i4UNkiy(rdyC=G(I~%*dz`Of4<_X2VoG!+E^!%9#rnm~-?Qy0yLI&>s05Lu}_e zO-ux~m3>!JV?BR9Q{ECCiccTCF-H8AW?CJ?34xu!k1uLy(M%ieXA6Nz#81-o>#LHU zbFE<97K;NvTcaajWc2Z5us=CD*4ZI{EB%(uG`h=h)ioA-Mh(!RV;y@b>1~kv~XRb(t=nH-H@O=TdkT+;`C0 zr)p3L{6ab7oYoYBJ6{l{jT1SGI5~NFgl7EW(i0Q5n9QBa*57V?+JzJ|<90@y9=j!d z46*ji9OImoai=mSE>!LuSr2cUS={LzH?Ss+9FnU`0FOV7-XOc$9Je(BfRDXqGbUXi z(}I-Xc*cv+WQeh(iI7dF7huIeb3{NSy*$tbT@^#2$t(r;dKCY94l@YuGOvl*(6fg> zG@=X!X|DhAT(vl2Om}uJxMIv`lI2rtNF>PsT`O|%VG_cj!;ygqTQe)@2fA-lM9Ng zH%e)tnVE#JAZcY0gex${sr64NuDQ~L3_k8?>B4*W;63XICh8M|rRs@MUr#yGkO z9$|jyR<2{E_!m62zeWyMU9FBhwI;dYQa;5Ep)f?WJXL z8{!jK*d^(=-3GtVIV(wL0%h~hW^?a2e(FnU`pM*WUofuG@Ol9BGOo`hFce_qqZgFII}JUG*6dPSbnwXq&sV`S&Y*p#qe0cQlGDX#<|u9_ ztY!5Jt#6-3CYki`fyUlk#ws93jy4#)qsRf@73~dW&k8%coqOw=THc*deLNra1VzH> zvp=04Fi`5#&P56qSBX~5ve77uY3N|=;64-rAA`=>@h=Ki$J-VR(K~nX`PfXaA7AEO z+&u5_@#CwPFHfRj1$8E4gUoeX*%jqIG45 z)2m!{E?uS^*o7MxaEo~~d3muS?i0NhA&FF1h4VqO3JPBiIO^1V}*yxBa#W~6o6-IYn`Ytya_?$BxelLJdewhnG?2|-nqSadon zxMY5I&`fVj>!jB1FFw4CUG_!!6v{11Dk?iNJQkqZM88Q&Iff#jkg^`b!Wl$@gRfoZ zKGO0!-2}p(3HbrzLX-p#$3ckdfjA@%bku~5>uDs;8rLo7GpOg-Q(06&uw{c{0&?H- zl!_uI^quSY&)45@wtgw#!SgjgsHDNG>wd^5fca3*WFeC)EL_bgSGl48ht$pRctm#s zO?R0P5n!ma=iosg%zc=Z=uhju(ap9Iuv~@UB`3wn?h@mKu!2XfjCIz`@%=+xvwq7K z+&|@YZW=o!gPx->$AH~AUlXe%UdtnQ&OUlIN9{rhuy@kGp1m?!=+z&3V9 zGsEdb9b74GJVbzZh#mhOj8^haLOrLpk@TEuBM^Be9yQse6i^I-% zxiwjx%%^cyVcla0dP5VcnV5Y`<7|s5>9K{9OG5u-5~>-^F^9H=$s$x1V0PTDIa|Di zO%@|ldT5^?QslTndwQI^PvJm^mKRUQEheYpG9$P26iuCaf7(>td`;HK_spzWP7jUR zT&+pl=KI8Yz%7rk4qng4JGt82914yCKtusVHQv2B0Zlj}reLDQ@7(b|0@cB#3Y;{F z907VQdgNqad_=|l)8VJ`7RK`eub1y1XBhOLWrTLoMar-f9sk_O zy?vY7hngbG3wC^0;h_oJKDw8LRi~bbnfT=RbEj`{NnZ~=dHu@r`ZkC8X~PfZ71D^Q zt6OZ-LE1x1{UnD6k<8rTDi(9j67HDBWOjDJkRc(UJ~2Ays=fQkt|S!JXKy3P00cIk zs71R;$M83oPh8*y1w=aJzHdOx?yEm&BtAYdABp~Zl1-<{GQGVlKtk$)h zB{Gmgg`LKcNQ>#H^@7zEY_yyMy0#Byn)a zj2d+xxqD7dB`uZRC4y?MQvjl~q;@s3BERT&#lb0{n`nP!rDuqhxJ#m*qNwvFn4cYr zV7Zgf$e$@&n0P@%i|Fy83;8F-0cKy<=-$XZ!gLy+@ z&t1$^j#Jd;grThuJ$@Xm&dd>gfCxnd79u^zVRTsE2+RfGtUx0+R>f3Xh{=C}!%~y_ zM2=W?Q+CSqzygbS17jnf03@08WvX4T{t1f@sqdoVRb>5g+lsxm#~?EF6jAL{t@PZR zTWNWzX_0h6R;HOn(lMzE8zV|AqBW|j6hC|!GO+pWrl=WfEgp}ve8j5}1U{Hs0jC(z zTV*x$*$hD?w6by3zNPkdcIFEg#-VOTJVW=f0h>9yjorW)n+M)LV-rvnA1?yQU#DC& z2oxO!JmSEdoEXh>D$BK1Z>gepK^>~0sW~`JHEiO;O}CxmlN|B1pI(!lf7m9-PFwZ0 zTcE5`(BrL^5kCPtBm|-E0AdAFE{S2D2Rrck_3KabeOeN*^31J9tht;H{)yGrkoqUyGBpEK=3U3z8h7*aI|{YDiizzKe&bWh zniuwT%OqlZ_s?KQ`Lv`YJ(;HJ{{CICogju# z#EP1POdH@*>1@JcQEG}$iOhpLL?K?G# z#&~SeQk`qG*sP~Vke=n1*^6E~nbt2=&9hlgXbl=uY@na+UNhTX37U&2=dD$Q<_e=W z?pERa5D9?;r*kV-_jZDc_H%$Vpd7x+GTrR;R?#`U{x$-Oye!@LQ!0gF*t200ee2$K z+0!FvX6&3$Ng?CqfW{AHbBW9ZGLsVMJ37s+ex5Y!U>-o??YYl^{Oi;Z7Bt$U|37Cn zO3;HruTyiA(Qzphhx0J7vV&*JlO!>FTBs|GEnoatX}VeLHKVI-(EB}yD-~>jOe*I+ zE{RlLWV^}YkX%{8rlG^5ARG;j>ouHa@N`Byv>_4#Rht{84JMHtkXvyl_*}s@OKUkD zq-_*k3~3#F-RDAj%*SZ=HU-uVCD+oPe5|ZAc-7QKe@*Y>S92ZoitOaBJlwR*DmcqY z&w1csOY@mdk1eNrOv<319w6v_+gvu|yjz#?Iyw)k#Zo8REB*J6#eyDB3?BuDKH%JF zX{-x7lfI*GpcFHReT1_NO&Xn+R!Nj?+BpzUR7pWSju)Ii{cWjgX%G(YxKiUiE#hi$ zdZ-0kG%c=6>9w`}n?8R0dB^eQ)Ov=;oOV1=Vi++hPWk7DiVApf*5hdDz5DjoC|AvL93EfdUoT#rM=Y<)AzwI?ub|tFrdQ^u$*eO! z?l5hK$|cY!8q%FA0P*zx{I!%Ig15+ndqEmuBJ%Sy=%-_rf4A3#LY(kt;a6FaAK5hPP31VyzyLm5o~6`tBLA@NyK91)QRSHvlWrhK%0I zjTU48!yuSE@;_(IMHXOXCP(%mjBFX08b}fXijpT!226H$gU8(MsF??LJT7M&rA6@yUj2S4Hx9AM3{59i@WW_8)c{XWd+l;MzwCc zb?tk+Wrrn8r_IS+oV#-EXgSj)xzm4cIQ6n@x9wLz)`i#u^W$FcLg=q>u>G)*sW$gz zHole+>@E?J?kd7V-Q`~SzoSie8UG5DuY=GHkA3KQN&?j0G`282s0_8Vu24f2of>0; z84WFOfLWNL@oLNNX5V9YuRUAW-n<%V`{)Yc@OJ&l8n^`z{^aKYIp;$Rg@|x);Xtwu zOwziUhg63XYK!#b4e!Kpf;fA8GVx}P5EL~vUBzN}{Nr<{zKQow?6Rtbqde|x@jt(= zi(}Sl({{&;{M4i9yYbPiQCK*l&n3s)OpOuR+7>;Fj@x)yF|Pr0H7y9@iQAQJuHVjF zE|t;l12v98zYk75vQt-g2ydEpuhY1x#A~7DH}k&ff4n(!UAP!|U}aTd?J@SadbbWr zJvy#9-^*eqDgb~7hwa8(jba;H^F@@A-V5J7<-nS;i|5Wgm~U@xa+#4s^G=T0KZB_; zMo_G#DmN+d{cK4}#Y3=jCMST%3UG)}WM$dPKWmyf!o_$`LfjtxTjKel3q(f1Ibj2I z7ur}$XZ17>xKJaLFv36Lks=0n(!CED%&_9f>A+-l-91Nvz?DgGZ zZSp<(@Wr(esMLo?mtC++x+dG@>_Wqc3s;&yjTOHA=KZl2$G1BdgA1-u@{Jl>r^^3kOTkYy- z{IwXTQAS1?FbL!sARmAw+8gi!Ud1vb%|PXHV^2JuQ#znaVc?qKWAz5wZ}K^M^~RIZ z4iP3lfBy6W+-5UT!2#ouSM~!?mw|VB{bc;7#kQu83kwW9GwYghS?XACz7|e2wV@~p zdI}F>=MHSFK@*9WkY_ht+P__2m0yjGkALpzvAeC`GH;GPW{=zlAG&*2ItIoL0{E>Y zEGRaWy z)!iL!Of?}Gq(_v!+&R-VyEySfSd_|Rht6l6+5_57peLfUUzY4LHP|lR$#mLdt3z_A z5+nr0FbC*7E-3ph{abr=FHUsrpAVzNh?g!ydUkvC(!ltMRa5iBV-JBArdjFfGv) zRxic?8Gi=5)D{6n(26h(0>wUUB>rrIlfXE@4bWQ%0?Dmffmu3(4}V}i(YOy8=(H%w zsAK){`lg0eZ&i%uO?nunWejeQ@-DH<6m)IK+WWiAEUH_!Izaj79lJ{`*JXs}ff?cF zbxm0!WFTDzL#5v!t?w_wow}{fgQ$tBYjdSO_?-WG^vaDV+3y?ZGs~T2{Kwd2coX74 zRSg+|^HcQVgPgncl@*ASYqz!Rkff`Z-tq@7w>}Zp;GngLK#ae{dp`Z>(j%+|)Ksqd0SC>@pkj@CbTlqowpjIC1$WaVc$3b9!-&Eaod$i$@C#eG zK9cnm@7%kGh<=nijzk+_?kRJUz!i_Kuh!4G{~1$sA`SIcyP^;?AS*hA;#x z+a2Pt?fo4$&pHx{Uv^UBtcEmt78g%R-ChQaAHi)zv@0F;E`3{4ML=w6j}lf4vu+20 zO?U{}rlE%o3C$S0=9wTo1Lv67>17^86_0~2$D;0P5|9-#@|@~;?e6#Gmh^JE5`BKa zV0sbse&GpeQrg9xPRHzg41Lv}=!%J+6>MGVh_bR{NBq94k0%x{`WImULr2P#Q21U* zK9{7WNZXoo*749a^MC5z(!`Q$_o#c%2pFqT_L*MWQ}ozQD|zK;as8eluCq z`sM+ZCzf-wE%)m)9E2Cbm8>D%KyD6m6S{O+p&5!129+UW_*f^%0O*LrP+UW6o3!+Qy+aFLkF$gHHDTD@8$P8%A`gspEA%IFfg!5nJ!nfi}Dcr(UMf ze($WD^?YJxZ8d+fNA9Rp_kWwGZPTY3&6-8B(LQAf)wF#zi_n;pe8_W3{&W~W9%Kxp z^6m@6D*34phmt$IbL}=^2<22X(gc=ys&p?gC8Y`e8OUzq#*N8K@MSXuN#1ORN-wGQ z?T?3su1{Igw{HlHn$Cm?*)t@ZC8??@Y3{ynAc%}G6TPkK$=?T#b{ZU&ob1LTqYs0Y z0nMSff>{ij2`NOXFo)Jes;nV)*4aG6OJ4U9>VEuCUe@8>w%8^5UB<0vnj!XNU@5t= zYh?dK4qrMwZe9iWGHT_oP;##v7p|J ztyWthgJ;#+{ArY30bBml`6aGHpI+C}kRHPz)sN>c2K9JpWbAlWgrm;gUwzK@n#NfY z{$IVCh>ah_nz+cz7HbalIWt;&@v7k$-re}v+p_DnUyBwGFpIx%;&x*K>q%N_h?-ib z>_k?zqyRJSH*=An@@vE@!{!_l^P4|B0`fjMyPbbzujg@cPNhJ|4b#%IVy*Nt=l=chp)mJAM~dk<9C18IHrX zpJ-II7G4Ps`+e}ijMB+QSD!qRE$yAHfyip2sk%=lGx%;_AGQPQ3bV_gCKQ^lu&+d5 zI8|P5tIO1CzU-&%)ia#l$OFvgGlc<}%cXYnPgg?cJ6$u&{P=ggtK6(HTYarp-uje& zjN2Obe-FRhnODYsJRbo_Y9lLE5;tU#H*!ukH2*>+no7ngvA%W13xlFRGab%2pH#bg zym$Zp$I*c`rgBIRKI{O;VIVOwWbx9aSo2ppJCEwIdvnd3FI+olDnW3v4`J@cW>uD* z*gBb13+dE}wL#z!Fi%n!Z>*V!tP#pKJLJZAKy~FqH7{~9s$WfBa)(^HvO>ueEt z1G1_|KlTctrQ~;FxjV8Y#o@R16It<*{rUHiX*-P@&gIWpQhObek9zOnN(}_&gAePK zey5$_(8g)~wXbBJ0@pYyRcvB!8dX+b*u@d>z(LDSHz+u`=&xlxmR^O02_nouYa~1B zaZk9_pt7vY%=ozYKgK#fRJweZjZ=Pau***wxaw<-A-@vninQdu0Mh|I;0}i7vtBgB z;N0!JJe(-sXp1>-HJt1U_2zY1a;CLrIerALJl51=aF-<~=Z#96Z#-2lZlZn{b?c-L zU(VH3TYvoWYiCWw)vMnTqm?@YO#xI5|598~QD>tv3YV--vf5$8NM|yKB|M_&5D38P zw=V8MOh;+yoMAQP$KQ&{66%xur2Cz|cyS6rR|x%ba%5(EmHnp$FyMXmv8=&_11}6| ziK$w?>YIdn^Ef|SDna%03cMI4t@G8J_p}k@X|d>=C?brH4f|IK;DhrRpOe{DGlK|3 z5@by$w!wdN z?btim|GH=g<%_k}lm87$k~ABIT7mVmuNB+h@W6Z^$csx-Bn3r1-aId`6 zUCPI7;lg~oOAK?M(hbZP>&Ja^#CN|uGENj|wQ3&vHy1{-Zi?lFMcPEtw3H%!gqps~ zvS)BN*A``vfo;kXPyE2Z;3CdBV}`Pl(m#|t8Nqb(!0(}RSn0R8(iyrnV2Y>wg(@i4 z04X2$f5X)RS~G1lAoc!SJaFJ1#4a=kfB>7HpkVsQkbx66pU-vffcWCi(EmBw-+B=YUErO;b>)^}YsmFZ7EEa9 zb0jhoHhok&Y=|9QumQCceO23kjufy&*Q4UwIDV3nl7Vs{#NFFL2Sizx*{J{TxwuU{ zF<)(HEYvnjw|)Ee6{@1<}_Hi zE?Q5ywBRQ9PM@3oVd)!xM!Ghv|5qKd2=aCSNoD}~|&v{1lg@o%eyR9D0 z40}m;Z|)5sIYXr|h80E}h)E}6+hCI3e&v7GeoiaXFJiter6LA~bY8PYTKK^FwzE5} zf9lwz?VIV$(OB@_B?M4)_sIy>L^zH!rHz1;I@BZPkYs=Hf@ z0+^>$|4%_dzrRZ+z6bC#CFz3)NCK=9VxppKWHV8JjUHoZGRXU#JWjW98=H;_6|CnH3M$G{x{tt0+kvg}Al zL3@Z8muZUu?!`Rw_EJ(~%{TBX8waCgP`;nE+rtV4F51&*ZtYiSK2+;_TS?4QtZ`fihE;Z&@+w&0yxG;EL*y?$X*jfP(nbR zNXct#U9OnTFHA>uMUH9oBU*uhJ$s5lH7sC1Utd=9N2Kvv)~`Qo6y3?!4*#62-_@L3 z;vsUp-T$3bHXP3gz2_MLbF!CMT!J@E167Hj$61n^`U?SxAoSQB#at)+FkpbOcdsGn z;C`kwrbH5lU4w?9&|_N)0{Do+gw1Oj-k?;_yR!a&`=YKZ^g4Ei099%#HaXzuRJ4B- zqkIA=MT!jA&Rb+Y^EKu6zpJPiga4e17j8DbfC0G8Y-WCYI*mRuLolyOq<{AIIaRKl z5s}OHiPAu$|L@n4-wm$_S6Cawxp(RLXO@=ouwlz(If$hM200@yz*7pB?Ay~18vv%I8%}D0JDuUypRbpC^6qz?vj7v=LgH1Qz0s4s82@K)-&Pzy!f?HE=DjiM zm}y^-6 zQtvu5Y`dyNd~EMPYsFP$P9(g>6Vv*BQYI+Jx;;J7+`+bNX8FxHsknK8fW+*ZslM#g`=&p?w;wiq z>^+-M89<3O*ATQmdc@(sI&86cd3d$EJL&>?e}5IB=k!jQhELdn0AdrOauE|hnD>eH z)3aA|0=i2UD!N(@R*>iR0!SYI)(=&vL_s_GJFk=1F9)IeZc&DUtsiDZdOWN>IA#d| z_4G{R!g}<2uw?OR5mD{MFeVF&;Q4Cp1TuM$U|+7JARx$QHAA#sEKz3;MY&*5Ab$0H0q0>+>K&JAU+L24xH*sxrx9VqMX_=Mq&D-0XEq$a4;|f!rO-#fTL-NdZJ=ll57o#tca!t+e zyigpzRgpNroNcIGU2fP+%9W7)|b*{BiC6B#er_i>CDS z)j@ssqB-J>_|2B3s)g8gJ&u>}`I^m)BtQuTC!aw+u7iKzSiAm2j$~0Ow zKkhp|LV+9WZ*ZVCsJL`CT^kMm28!b3h!Lcy1mxPu&g$Rn`M{iH`t=((etvMO&B8yl zlG}?^vYX-=BIg=!WB_qeFt9S9L@NB7@sk39C;KJPBmg}MoWq+3z47ZH*Mxj|NN*A zzjWC3uXOd4ddyPl!MVe*lhvzB&M)M~a=zR`$XW#XMgmnAj(23{_}#J-xjDQVipJS* zojtn{3ykYFW)5?8N_OYkD{2Y@l zE-sPZ)De!L7?@##0+I<3gJK6_U_iL_y|urqk?)O6Mo?~Dsv8<PI{WCZfe0K8!`D{TR1Cwivzi!3L*{U4b3ajz^^_J4O=jFFK zeXya!DxtjwcgD9yqt0m#jffN|l-$;J>k>jk`@uiq@g|{2E-sjx{If|_LlgG|E$y!E zr$~+3r9bE!<6St0eA0=Ix3#m=?=u-KA4foWa`b#PJeo%gm9RWf8E_pc75@75iyQGo zxB@Psnwq&L$Q;&+a}0s_FGfas=i3)lxhe-fKQpr{&JBmp7na?6!AR+XVSl~N#B5X7 zTv(W-JZ@ay>_jmM;)gJxj3--gU=#oC)iDg=u!Ejg#S@(g3%Kt3_HAEsn$C8%(h=Tr zNN_O6Ex82ib>wv(BF;{AZ*rY{@??0pdwu0Bo!-`J!Gz9ZTR>y4aPWOundyQBL}%vP zXwZnUP3g8de>mB2+l6B|Y92?*h2B~Mv?fFO{=hs8OtkbDgxg;mfh>4(4;DROZWV46fmR9F8h&0{+$yO%Dl2LZr@RAyZ; zth}^~+?L<>>p&d1Z@6rTAE6K;Kp&5Huo1TMXT(=rc86(4lSH5Eo@ui|5Ewzb__BS+ zg*4C6R2h{U(bXB1T9A=6P+q3}jp6b#{wA@anbW-WA6p3a;=eBAlqDlurs|fq5fbqW z(Oy&CZi%0YF>Si47w9Kfooh3ljA|9M0FtUxbhgi~>q9;LZvJ!;@?~ft z#VbRZxdxsML55X+_bfdLZrsCFY^y~U7VYvbu?)+u8}lTdOq6UZc}mwcTVBloU!b^oEZ#l`nc+v*NGRi2qLNaOso+-KI)_AMpIVCYu$OEdPXAClcZ+s-a; zr`VZn>MHBrLEY;(AkvDEq8-Niz zzCTIG?8=qOXiPOUY?7X1FCf3QXeB`W_HEmo-ou0L=n`Dp-S_LjqJ}F0KX&V%GVOYd zh)9%Ik(*+ta+t1IF;xBlQ#aajdngUKM~I3(m|WewXmiq>g$# zc@yyB(BIJAW)OY`k!g;(pZQjYtc%bi#>6BX{iem!Z_sDt{aGHPDa=B-_`24gE^rP#f&L)WLg zqSu7nj_r4%pCDuakBRX)0+WyEtO+g*<7dM^iW<2IlLT*$YHD<1>zRYVk30V9>6Z@C z=Ao=(&h~)=TWiawIdnpnX~L5arhGNx{HmxUaD*|~$BT|KrZz=BTO-SI*XH@1_jYlP zpS(_At<7B_Z@h(C?}?pFJU?Dr`eg2V*Oc10t7BL#(%HzcUF0!ELqA8Gb8g?LpC{G{ zcI(!S49usXC1pkHN2FQoFWXPS*Y-y)Bj9titTZeqOy6Nc`_-#E?=7T2VV~G*awXA> zqE+bJc`rwI&%T4SdeWVP&!0TmwP<;?IU_#I7cB}43v0#=K0e~!$|P=57Gk7rY4q&A z{4K6cvfiXj6rU{K2+Bh5+GF=Flh||f)u>c3oG{Qdi9drlT&{E43)L^x?V1g|Pe@a@ zVx*$FXRywQks}k%p2eBbC2pM)1h@Yx4e2wOpT?} zDI#oz?K2c#^xEZ^S5aq}O04mc4^PkSAO<3C@pdDlPD^^Xxs@33jhgLWmRI3e$+&D?2!2AyjG|4qQmP{8{9a%G;>M

4hh5H=T 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�FIXX2>A#etW=a_Q2t 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 wire-fedcalls.png ### links -- `./example.png` +for users: + +- blog post explaining the technology: https://reasonablypolymorphic.com/blog/abusing-constraints/index.html - https://sketchviz.com/new - https://graphviz.org/doc/info/lang.html -- `/libs/wire-api/src/Wire/API/MakesFederatedCall.hs` + +for developers: + +- `./example.png` +- [MakesFederatedCall.hs (as of 2023-01-16)](https://github.com/wireapp/wire-server/blob/8760b4978ccb039b229d458b7a08136a05e12ff9/libs/wire-api/src/Wire/API/MakesFederatedCall.hs) +- PRs: https://github.com/wireapp/wire-server/pull/2973, https://github.com/wireapp/wire-server/pull/2940, https://github.com/wireapp/wire-server/pull/2950, https://github.com/wireapp/wire-server/pull/2957 ### swagger-ui From 8c589be1840f312c5d9285b3b9052e6d5af3b7f3 Mon Sep 17 00:00:00 2001 From: fisx Date: Mon, 23 Jan 2023 10:02:12 +0100 Subject: [PATCH 20/38] Don't remove global cabal store in 'make full-clean'. (#3000) --- Makefile | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 9b44c21278..affc632be2 100644 --- a/Makefile +++ b/Makefile @@ -52,13 +52,9 @@ install: init .PHONY: full-clean full-clean: clean rm -rf ~/.cache/hie-bios -ifdef CABAL_DIR - rm -rf $(CABAL_DIR)/store -else - rm -rf ~/.cabal/store -endif rm -rf ./dist-newstyle ./.env direnv reload + @echo -e "\n\n*** NOTE: you may want to also 'rm -rf ~/.cabal/store \$$CABAL_DIR/store', not sure.\n" .PHONY: clean clean: From a0f44f6cf189c094934c248ef9ab7c6be46bc044 Mon Sep 17 00:00:00 2001 From: jschaul Date: Mon, 23 Jan 2023 11:54:46 +0100 Subject: [PATCH 21/38] Prekeys investigation: test (#2694) * log randomPrekey state of things * Add an integration test to ensure list of prekeys does not become empty. This test should also run for the dynamoDB case, to ensure our implementation isn't buggy (it probably isn't, but to check for regressions) * add another test for last prekey validation * Add one more test, fix warning from previous test * logging on startup: log using info level, not warning * fixup: expected status code is 201, not 200 * remove commented out code * changelog * linter? make format doesn't work; or local/CI don't agree --- changelog.d/5-internal/pr-2694 | 1 + .../src/Wire/API/User/Client/Prekey.hs | 6 ++ services/brig/src/Brig/App.hs | 8 ++- .../brig/test/integration/API/User/Client.hs | 59 +++++++++++++++++++ services/brig/test/integration/Util.hs | 8 ++- 5 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 changelog.d/5-internal/pr-2694 diff --git a/changelog.d/5-internal/pr-2694 b/changelog.d/5-internal/pr-2694 new file mode 100644 index 0000000000..7bd6984179 --- /dev/null +++ b/changelog.d/5-internal/pr-2694 @@ -0,0 +1 @@ +Add two integration tests arounds last prekeys diff --git a/libs/wire-api/src/Wire/API/User/Client/Prekey.hs b/libs/wire-api/src/Wire/API/User/Client/Prekey.hs index 8b29e54afa..cf084c65d8 100644 --- a/libs/wire-api/src/Wire/API/User/Client/Prekey.hs +++ b/libs/wire-api/src/Wire/API/User/Client/Prekey.hs @@ -25,6 +25,7 @@ module Wire.API.User.Client.Prekey LastPrekey, lastPrekey, unpackLastPrekey, + fakeLastPrekey, lastPrekeyId, PrekeyBundle (..), ClientPrekey (..), @@ -101,6 +102,11 @@ lastPrekeyId = PrekeyId maxBound lastPrekey :: Text -> LastPrekey lastPrekey = LastPrekey . Prekey lastPrekeyId +-- for tests only +-- This fake last prekey has the wrong prekeyId +fakeLastPrekey :: LastPrekey +fakeLastPrekey = LastPrekey $ Prekey (PrekeyId 7) "pQABAQcCoQBYIDXdN8VlKb5lbgPmoDPLPyqNIEyShG4oT/DlW0peRRZUA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=" + -------------------------------------------------------------------------------- -- PrekeyBundle diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index fb600c55cb..50742a9faf 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -232,8 +232,12 @@ newEnv o = do SqsQueue q -> SqsQueue <$> AWS.getQueueUrl (aws ^. AWS.amazonkaEnv) q mSFTEnv <- mapM Calling.mkSFTEnv $ Opt.sft o prekeyLocalLock <- case Opt.randomPrekeys o of - Just True -> Just <$> newMVar () - _ -> pure Nothing + Just True -> do + Log.info lgr $ Log.msg (Log.val "randomPrekeys: active") + Just <$> newMVar () + _ -> do + Log.info lgr $ Log.msg (Log.val "randomPrekeys: not active; using dynamoDB instead.") + pure Nothing kpLock <- newMVar () pure $! Env diff --git a/services/brig/test/integration/API/User/Client.hs b/services/brig/test/integration/API/User/Client.hs index d3171c6ee4..77ccdb1e2f 100644 --- a/services/brig/test/integration/API/User/Client.hs +++ b/services/brig/test/integration/API/User/Client.hs @@ -50,6 +50,7 @@ import Data.Text.Ascii (AsciiChars (validate)) import qualified Data.Vector as Vec import Imports import qualified Network.Wai.Utilities.Error as Error +import qualified System.Logger as Log import Test.QuickCheck (arbitrary, generate) import Test.Tasty hiding (Timeout) import Test.Tasty.Cannon hiding (Cannon) @@ -103,6 +104,9 @@ tests _cl _at opts p db b c g = test p "get /clients - 200" $ testListClients b, test p "get /clients/:client/prekeys - 200" $ testListPrekeyIds b, test p "post /clients - 400" $ testTooManyClients opts b, + test p "client/prekeys not empty" $ testPrekeysNotEmptyRandomPrekeys opts b, + test p "lastprekeys not bogus" $ testRegularPrekeysCannotBeSentAsLastPrekeys b, + test p "lastprekeys not bogus during update" $ testRegularPrekeysCannotBeSentAsLastPrekeysDuringUpdate b, test p "delete /clients/:client - 200 (pwd)" $ testRemoveClient True b c, test p "delete /clients/:client - 200 (no pwd)" $ testRemoveClient False b c, test p "delete /clients/:client - 400 (short pwd)" $ testRemoveClientShortPwd b, @@ -689,6 +693,61 @@ testTooManyClients opts brig = do const (Just "too-many-clients") === fmap Error.label . responseJsonMaybe const (Just "application/json") === getHeader "Content-Type" +-- Ensure that the list of prekeys for a user does not become empty, and the +-- last resort prekey keeps being returned if it's the only key left. +-- Test with featureFlag randomPrekeys=true +testPrekeysNotEmptyRandomPrekeys :: Opt.Opts -> Brig -> Http () +testPrekeysNotEmptyRandomPrekeys opts brig = do + -- Run the test for randomPrekeys (not dynamoDB locking) + let newOpts = opts {Opt.randomPrekeys = Just True} + ensurePrekeysNotEmpty newOpts brig + +ensurePrekeysNotEmpty :: Opt.Opts -> Brig -> Http () +ensurePrekeysNotEmpty opts brig = withSettingsOverrides opts $ do + lgr <- Log.new Log.defSettings + uid <- userId <$> randomUser brig + -- Create a client with 1 regular prekey and 1 last resort prekey + c <- responseJsonError =<< addClient brig uid (defNewClient PermanentClientType [somePrekeys !! 10] (someLastPrekeys !! 10)) + -- Claim the first regular one + _rs1 <- getPreKey brig uid uid (clientId c) responseJsonMaybe rs2 + liftIO $ assertEqual "last prekey rs2" (Just lastPrekeyId) pId2 + liftIO $ Log.warn lgr (Log.msg (Log.val "First claim of last resort successful, claim again...")) + -- Claim again; this should (again) give the last resort one + rs3 <- getPreKey brig uid uid (clientId c) responseJsonMaybe rs3 + liftIO $ assertEqual "last prekey rs3" (Just lastPrekeyId) pId3 + +testRegularPrekeysCannotBeSentAsLastPrekeys :: Brig -> Http () +testRegularPrekeysCannotBeSentAsLastPrekeys brig = do + uid <- userId <$> randomUser brig + -- The parser should reject a normal prekey in the lastPrekey field + addClient brig uid (defNewClient PermanentClientType [head somePrekeys] fakeLastPrekey) !!! const 400 === statusCode + +testRegularPrekeysCannotBeSentAsLastPrekeysDuringUpdate :: Brig -> Http () +testRegularPrekeysCannotBeSentAsLastPrekeysDuringUpdate brig = do + uid <- userId <$> randomUser brig + c <- responseJsonError =<< addClient brig uid (defNewClient PermanentClientType [head somePrekeys] (someLastPrekeys !! 11)) UserId -> UserId -> ClientId -> Http ResponseLBS +getPreKey :: + (MonadIO m, MonadCatch m, MonadFail m, MonadHttp m, HasCallStack) => + Brig -> + UserId -> + UserId -> + ClientId -> + m ResponseLBS getPreKey brig zusr u c = get $ apiVersion "v1" From 82a0a5923774821b4a19549fe4dbf58efa89b733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Tue, 24 Jan 2023 14:10:25 +0100 Subject: [PATCH 22/38] Document a function --- services/galley/src/Galley/API/Message.hs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/galley/src/Galley/API/Message.hs b/services/galley/src/Galley/API/Message.hs index 8f0f2bb178..7484a4c684 100644 --- a/services/galley/src/Galley/API/Message.hs +++ b/services/galley/src/Galley/API/Message.hs @@ -553,6 +553,9 @@ sendLocalMessages loc now sender senderClient mconn qcnv botMap metadata localMe runMessagePush @t loc qcnv (pushes ^. traversed) pure mempty +-- | Send remote messages to the backend given by the domain argument, and +-- return the set of clients for which sending has failed. In case there was no +-- failure, the empty set is returned. sendRemoteMessages :: forall r x. (Members '[FederatorAccess, P.TinyLog] r, CallsFed 'Galley "on-message-sent") => From 804a3b50837b9368c5b861a604daed4bcb2525d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Tue, 24 Jan 2023 14:11:35 +0100 Subject: [PATCH 23/38] Add a changelog --- changelog.d/5-internal/doc-msg-sending | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5-internal/doc-msg-sending diff --git a/changelog.d/5-internal/doc-msg-sending b/changelog.d/5-internal/doc-msg-sending new file mode 100644 index 0000000000..890041edeb --- /dev/null +++ b/changelog.d/5-internal/doc-msg-sending @@ -0,0 +1 @@ +Document in code a function that sends remote Proteus messages From 89b7ce78c9752f507d26c4fed48f158583e832fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Tue, 24 Jan 2023 14:12:07 +0100 Subject: [PATCH 24/38] Revert "Add a changelog" This reverts commit 804a3b50837b9368c5b861a604daed4bcb2525d6. --- changelog.d/5-internal/doc-msg-sending | 1 - 1 file changed, 1 deletion(-) delete mode 100644 changelog.d/5-internal/doc-msg-sending diff --git a/changelog.d/5-internal/doc-msg-sending b/changelog.d/5-internal/doc-msg-sending deleted file mode 100644 index 890041edeb..0000000000 --- a/changelog.d/5-internal/doc-msg-sending +++ /dev/null @@ -1 +0,0 @@ -Document in code a function that sends remote Proteus messages From 7d322f5e4951f383aa79d51586d7bde1301a1522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Tue, 24 Jan 2023 14:12:24 +0100 Subject: [PATCH 25/38] Revert "Document a function" This reverts commit 82a0a5923774821b4a19549fe4dbf58efa89b733. --- services/galley/src/Galley/API/Message.hs | 3 --- 1 file changed, 3 deletions(-) diff --git a/services/galley/src/Galley/API/Message.hs b/services/galley/src/Galley/API/Message.hs index 7484a4c684..8f0f2bb178 100644 --- a/services/galley/src/Galley/API/Message.hs +++ b/services/galley/src/Galley/API/Message.hs @@ -553,9 +553,6 @@ sendLocalMessages loc now sender senderClient mconn qcnv botMap metadata localMe runMessagePush @t loc qcnv (pushes ^. traversed) pure mempty --- | Send remote messages to the backend given by the domain argument, and --- return the set of clients for which sending has failed. In case there was no --- failure, the empty set is returned. sendRemoteMessages :: forall r x. (Members '[FederatorAccess, P.TinyLog] r, CallsFed 'Galley "on-message-sent") => From 30f1d44a171003e2c20d5196d06074a587654868 Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Tue, 24 Jan 2023 14:24:13 +0100 Subject: [PATCH 26/38] Changing `helm_vars` to `values` in deeplink docs Based on current deploy conversation, this is out of date --- docs/src/how-to/associate/deeplink.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/how-to/associate/deeplink.md b/docs/src/how-to/associate/deeplink.md index d3bfc77e27..4c9af0ea21 100644 --- a/docs/src/how-to/associate/deeplink.md +++ b/docs/src/how-to/associate/deeplink.md @@ -100,7 +100,7 @@ As of release `2.117.0` from `2021-10-29` (see `release notes`), ```yaml # override values for wire-server -# (e.g. under ./helm_vars/wire-server/values.yaml) +# (e.g. under ./values/wire-server/values.yaml) nginz: nginx_conf: deeplink: From 4d1f02ef86459b881a8667176b4c6c4095bb6ea2 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 24 Jan 2023 15:40:08 +0100 Subject: [PATCH 27/38] Fix kind setup for running end-to-end federation tests locally (#3008) * Add scripts to load nix-built images into kind * Fix redis-cluster in kind setup * Update how to use kind * Changelog * allow skopeo insecure policy * fetch AWS secrets from Kubernetes Co-authored-by: Stefan Berthold --- Makefile | 8 +++++ changelog.d/5-internal/kind-fix | 1 + docs/src/developer/developer/how-to.md | 20 +++++------ hack/bin/kind-upload-image.sh | 33 +++++++++++++++++++ hack/bin/kind-upload-images.sh | 33 +++++++++++++++++++ .../{values.yaml => values.yaml.gotmpl} | 2 +- hack/helmfile-single.yaml | 2 +- hack/helmfile.yaml | 6 ++-- services/brig/federation-tests.sh | 7 ++++ 9 files changed, 97 insertions(+), 15 deletions(-) create mode 100644 changelog.d/5-internal/kind-fix create mode 100755 hack/bin/kind-upload-image.sh create mode 100755 hack/bin/kind-upload-images.sh rename hack/helm_vars/redis-cluster/{values.yaml => values.yaml.gotmpl} (66%) diff --git a/Makefile b/Makefile index affc632be2..7aa65a0e86 100644 --- a/Makefile +++ b/Makefile @@ -438,6 +438,14 @@ kind-delete: .PHONY: kind-reset kind-reset: kind-delete kind-cluster +.PHONY: kind-upload-images +kind-upload-images: + DOCKER_TAG=$(DOCKER_TAG) KIND_CLUSTER_NAME=$(KIND_CLUSTER_NAME) ./hack/bin/kind-upload-images.sh + +.PHONY: kind-upload-image +kind-upload-image-%: + DOCKER_TAG=$(DOCKER_TAG) KIND_CLUSTER_NAME=$(KIND_CLUSTER_NAME) ./hack/bin/kind-upload-image.sh wireServer.imagesUnoptimizedNoDocs.$(*) + .local/kind-kubeconfig: mkdir -p $(CURDIR)/.local kind get kubeconfig --name $(KIND_CLUSTER_NAME) > $(CURDIR)/.local/kind-kubeconfig diff --git a/changelog.d/5-internal/kind-fix b/changelog.d/5-internal/kind-fix new file mode 100644 index 0000000000..2b7d9f1b3b --- /dev/null +++ b/changelog.d/5-internal/kind-fix @@ -0,0 +1 @@ +Fix kind setup for running end-to-end federation tests locally. \ No newline at end of file diff --git a/docs/src/developer/developer/how-to.md b/docs/src/developer/developer/how-to.md index b900fc4f1f..68250bba21 100644 --- a/docs/src/developer/developer/how-to.md +++ b/docs/src/developer/developer/how-to.md @@ -112,21 +112,19 @@ This can be useful to get quicker feedback while working on multi-backend code o FUTUREWORK: this process is in development (update this section after it's confirmed to work): -##### (i) Build images +##### Run tests in kind -(FUTUREWORK: implement a convenient shortcut to build images without actually uploading them also) -``` -make upload-images-dev -``` +0. Create a local kind cluster with `make kind-cluster` +1. Upload images in docker-daemon running inside kind with `make kind-upload-images` -##### (ii) Run tests in kind + *Note:* First time all the images need to be uploaded. When working on one + service it can be selectively uploaded using `make kind-upload-image-` + (e.g. `make kind-upload-image-brig`). +2. Install wire-server using `make kind-integration-setup`. +3. Run tests using `make kind-integration-test`. +4. Run end2end integration tests: `make kind-integration-e2e`. -0. Create a local kind cluster with `make kind-cluster` -1. Install wire-server using `make kind-integration-setup`. -2. Run tests using `make kind-integration-test`. -3. Run end2end integration tests: `make kind-integration-e2e`. -* Implement re-tagging development tags as your user tag? #### 2.4 Deploy your local code to a kubernetes cluster diff --git a/hack/bin/kind-upload-image.sh b/hack/bin/kind-upload-image.sh new file mode 100755 index 0000000000..61b24c7937 --- /dev/null +++ b/hack/bin/kind-upload-image.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +# This script builds all the images in wireServer.images attribute of +# $ROOT_DIR/nix/default.nix and uploads them to the docker registry using the +# repository name specified in the image derivation and tag specified by +# environment variable "$DOCKER_TAG". +# +# If $DOCKER_USER and $DOCKER_PASSWORD are provided, the script will use them to +# upload the images. +# +# This script is intended to be run by CI/CD pipelines. + +set -euo pipefail + +set -x + +# nix attribute under wireServer from "$ROOT_DIR/nix" containing all the images +readonly IMAGE_ATTR=${1:?$usage} + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) +ROOT_DIR=$(cd -- "$SCRIPT_DIR/../../" &>/dev/null && pwd) +readonly SCRIPT_DIR ROOT_DIR + +tmp_link_store=$(mktemp -d) + +image_stream_file="$tmp_link_store/image-stream" +nix -v --show-trace -L build -f "$ROOT_DIR/nix" "$IMAGE_ATTR" -o "$image_stream_file" +image_file="$tmp_link_store/image" +image_file_tagged="$tmp_link_store/image-tagged" +"$image_stream_file" > "$image_file" +repo=$(skopeo list-tags "docker-archive://$image_file" | jq -r '.Tags[0] | split(":") | .[0]') +skopeo copy --insecure-policy --additional-tag "$repo:$DOCKER_TAG" "docker-archive://$image_file" "docker-archive://$image_file_tagged" +kind load image-archive "$image_file_tagged" --name "$KIND_CLUSTER_NAME" diff --git a/hack/bin/kind-upload-images.sh b/hack/bin/kind-upload-images.sh new file mode 100755 index 0000000000..5ba04b7025 --- /dev/null +++ b/hack/bin/kind-upload-images.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +# This script builds all the images in wireServer.images attribute of +# $ROOT_DIR/nix/default.nix and uploads them to the docker registry using the +# repository name specified in the image derivation and tag specified by +# environment variable "$DOCKER_TAG". +# +# If $DOCKER_USER and $DOCKER_PASSWORD are provided, the script will use them to +# upload the images. +# +# This script is intended to be run by CI/CD pipelines. + +set -euo pipefail + +set -x + +# nix attribute under wireServer from "$ROOT_DIR/nix" containing all the images +readonly IMAGES_ATTR="imagesUnoptimizedNoDocs" + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) +ROOT_DIR=$(cd -- "$SCRIPT_DIR/../../" &>/dev/null && pwd) +readonly SCRIPT_DIR ROOT_DIR + +tmp_link_store=$(mktemp -d) +image_list_file="$tmp_link_store/image-list" +nix -v --show-trace -L build -f "$ROOT_DIR/nix" wireServer.imagesList -o "$image_list_file" + +xargs -I {} -P 10 "$SCRIPT_DIR/kind-upload-image.sh" "wireServer.$IMAGES_ATTR.{}" < "$image_list_file" + +for image_name in nginz nginz-disco; do + printf '*** Unploading image %s\n' "$image_name" + "$SCRIPT_DIR/kind-upload-image.sh" "$image_name" +done diff --git a/hack/helm_vars/redis-cluster/values.yaml b/hack/helm_vars/redis-cluster/values.yaml.gotmpl similarity index 66% rename from hack/helm_vars/redis-cluster/values.yaml rename to hack/helm_vars/redis-cluster/values.yaml.gotmpl index 888fbc0262..5381d26cbd 100644 --- a/hack/helm_vars/redis-cluster/values.yaml +++ b/hack/helm_vars/redis-cluster/values.yaml.gotmpl @@ -1,5 +1,5 @@ global: - storageClass: csi-hostpath-sc + storageClass: {{ .Values.redisStorageClass }} redis-cluster: persistence: diff --git a/hack/helmfile-single.yaml b/hack/helmfile-single.yaml index 790412bf71..3a770ee146 100644 --- a/hack/helmfile-single.yaml +++ b/hack/helmfile-single.yaml @@ -37,7 +37,7 @@ releases: namespace: '{{ .Values.namespace }}' chart: '../.local/charts/redis-cluster' values: - - './helm_vars/redis-cluster/values.yaml' + - './helm_vars/redis-cluster/values.yaml.gotmpl' - name: '{{ .Values.namespace }}-nginx-ingress-controller' namespace: '{{ .Values.namespace }}' diff --git a/hack/helmfile.yaml b/hack/helmfile.yaml index 825e49edff..9dd863334c 100644 --- a/hack/helmfile.yaml +++ b/hack/helmfile.yaml @@ -19,6 +19,7 @@ environments: - namespaceFed2: {{ requiredEnv "NAMESPACE_2" }} - federationDomainFed2: {{ requiredEnv "FEDERATION_DOMAIN_2" }} - imagePullPolicy: Always + - redisStorageClass: csi-hostpath-sc kind: values: - namespace: {{ requiredEnv "NAMESPACE_1" }} @@ -26,6 +27,7 @@ environments: - namespaceFed2: {{ requiredEnv "NAMESPACE_2" }} - federationDomainFed2: {{ requiredEnv "FEDERATION_DOMAIN_2" }} - imagePullPolicy: Never + - redisStorageClass: standard repositories: - name: stable @@ -59,13 +61,13 @@ releases: namespace: '{{ .Values.namespace }}' chart: '../.local/charts/redis-cluster' values: - - './helm_vars/redis-cluster/values.yaml' + - './helm_vars/redis-cluster/values.yaml.gotmpl' - name: '{{ .Values.namespace }}-redis-cluster-2' namespace: '{{ .Values.namespaceFed2 }}' chart: '../.local/charts/redis-cluster' values: - - './helm_vars/redis-cluster/values.yaml' + - './helm_vars/redis-cluster/values.yaml.gotmpl' - name: '{{ .Values.namespace }}-nginx-ingress-controller' namespace: '{{ .Values.namespace }}' diff --git a/services/brig/federation-tests.sh b/services/brig/federation-tests.sh index 6212f75f85..5cea51393d 100755 --- a/services/brig/federation-tests.sh +++ b/services/brig/federation-tests.sh @@ -36,5 +36,12 @@ while read -r ip; do alsoProxyOptions+=("--also-proxy=${ip}") done < <(kubectl get pods -n "$NAMESPACE" -l app=cannon -o json | jq -r '.items[].status.podIPs[].ip') +AWS_ACCESS_KEY_ID="$(kubectl get secret -n "$NAMESPACE" brig -o json | jq -r '.data | map_values(@base64d) | .awsKeyId')" +export AWS_ACCESS_KEY_ID +AWS_SECRET_ACCESS_KEY="$(kubectl get secret -n "$NAMESPACE" brig -o json | jq -r '.data | map_values(@base64d) | .awsSecretKey')" +export AWS_SECRET_ACCESS_KEY +AWS_REGION="$(kubectl get deployment -n "$NAMESPACE" brig -o json | jq -r '.spec.template.spec.containers | map(.env | map(select(.name == "AWS_REGION").value))[0][0]')" +export AWS_REGION + # shellcheck disable=SC2086 telepresence --namespace "$NAMESPACE" --also-proxy=cassandra-ephemeral ${alsoProxyOptions[*]} --run bash -c "export INTEGRATION_FEDERATION_TESTS=1; ./dist/brig-integration -p federation-end2end-user -i i.yaml -s b.yaml" From 2ad081960392ffc90d4eb196e7aba74c8ca12da3 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Wed, 25 Jan 2023 10:27:27 +0100 Subject: [PATCH 28/38] Restructure documentation (#2986) --- changelog.d/5-internal/restructure-docs | 1 + docs/.gitignore | 3 + docs/diagrams/mmdc | 1 - docs/src/changelog/changelog.md | 1 + docs/src/changelog/index.md | 9 + docs/src/conf.py | 1 + .../src/developer/reference/config-options.md | 3 +- .../src/developer/reference/spar-braindump.md | 10 +- docs/src/how-to/install/index.md | 24 +- .../install/infrastructure-configuration.md} | 371 +----------------- docs/src/index.md | 15 +- docs/src/release-notes.md | 13 - .../custom-backend-for-desktop-client.md | 0 .../associate/custom-certificates.md | 0 .../associate/deeplink.md | 0 .../{how-to => understand}/associate/index.md | 0 docs/src/understand/block-user-creation.md | 34 ++ docs/src/understand/classified-domains.md | 40 ++ .../configure-federation.md | 2 +- docs/src/understand/federation/api.md | 2 +- .../img/legalhold-screencast.gif | Bin .../legalhold-step01-click-customization.png | Bin .../img/legalhold-step02-goto-legalhold.png | Bin .../img/legalhold-step03-click-arrow.png | Bin ...hold-step04-click-manage-configuration.png | Bin .../img/legalhold-step05-fill-info.png | Bin docs/src/understand/index.md | 12 +- .../install => understand}/legalhold.md | 0 .../src/{how-to/install => understand}/mls.md | 2 + docs/src/understand/overview.md | 2 +- docs/src/understand/searchability.md | 295 ++++++++++++++ .../single-sign-on/adfs/fig-00.jpg | Bin .../single-sign-on/adfs/fig-01.jpg | Bin .../single-sign-on/adfs/fig-02.jpg | Bin .../single-sign-on/adfs/fig-03.jpg | Bin .../single-sign-on/adfs/fig-04.jpg | Bin .../single-sign-on/adfs/fig-05.jpg | Bin .../single-sign-on/adfs/fig-06.jpg | Bin .../single-sign-on/adfs/fig-07.jpg | Bin .../single-sign-on/adfs/fig-08.jpg | Bin .../single-sign-on/adfs/fig-09.jpg | Bin .../single-sign-on/adfs/fig-10.jpg | Bin .../single-sign-on/adfs/fig-11.jpg | Bin .../single-sign-on/adfs/main.md | 0 .../single-sign-on/azure/01.png | Bin .../single-sign-on/azure/02.png | Bin .../single-sign-on/azure/03.png | Bin .../single-sign-on/azure/04.png | Bin .../single-sign-on/azure/05.png | Bin .../single-sign-on/azure/06.png | Bin .../single-sign-on/azure/07.png | Bin .../single-sign-on/azure/08.png | Bin .../single-sign-on/azure/09.png | Bin .../single-sign-on/azure/10.png | Bin .../single-sign-on/azure/11.png | Bin .../single-sign-on/azure/12.png | Bin .../single-sign-on/azure/13.png | Bin .../single-sign-on/azure/14.png | Bin .../single-sign-on/azure/15.png | Bin .../single-sign-on/azure/main.md | 0 .../single-sign-on/centrify/001.png | Bin .../single-sign-on/centrify/002.png | Bin .../single-sign-on/centrify/003.png | Bin .../single-sign-on/centrify/004.png | Bin .../single-sign-on/centrify/005.png | Bin .../single-sign-on/centrify/006.png | Bin .../single-sign-on/centrify/007.png | Bin .../single-sign-on/centrify/008.png | Bin .../single-sign-on/centrify/009.png | Bin .../single-sign-on/centrify/010.png | Bin .../single-sign-on/centrify/011.png | Bin .../single-sign-on/centrify/main.md | 0 .../single-sign-on/generic-setup.md | 0 .../single-sign-on/index.md | 2 + .../okta/001-applications-screen.png | Bin .../okta/002-add-application.png | Bin .../okta/003-add-application-1.png | Bin .../okta/004-add-application-step1.png | Bin .../okta/005-add-application-step2.png | Bin .../okta/006-add-application-step3.png | Bin .../okta/007-application-sign-on.png | Bin .../single-sign-on/okta/008-assignment.png | Bin .../single-sign-on/okta/main.md | 0 .../single-sign-on/trouble-shooting.md | 0 .../Wire_SAML_Flow (lucidchart).svg | 0 .../understand/Wire_SAML_Flow.png | Bin .../single-sign-on/understand/main.md | 0 .../understand/token-step-01.png | Bin .../understand/token-step-02.png | Bin .../understand/token-step-03.png | Bin .../understand/token-step-04.png | Bin .../understand/token-step-05.png | Bin .../understand/token-step-06.png | Bin .../team-feature-settings.md | 0 nix/default.nix | 4 +- 95 files changed, 428 insertions(+), 419 deletions(-) create mode 100644 changelog.d/5-internal/restructure-docs delete mode 120000 docs/diagrams/mmdc create mode 120000 docs/src/changelog/changelog.md create mode 100644 docs/src/changelog/index.md rename docs/src/{configuration-options.md => how-to/install/infrastructure-configuration.md} (50%) delete mode 100644 docs/src/release-notes.md rename docs/src/{how-to => understand}/associate/custom-backend-for-desktop-client.md (100%) rename docs/src/{how-to => understand}/associate/custom-certificates.md (100%) rename docs/src/{how-to => understand}/associate/deeplink.md (100%) rename docs/src/{how-to => understand}/associate/index.md (100%) create mode 100644 docs/src/understand/block-user-creation.md create mode 100644 docs/src/understand/classified-domains.md rename docs/src/{how-to/install => understand}/configure-federation.md (99%) rename docs/src/{how-to/install => understand}/img/legalhold-screencast.gif (100%) rename docs/src/{how-to/install => understand}/img/legalhold-step01-click-customization.png (100%) rename docs/src/{how-to/install => understand}/img/legalhold-step02-goto-legalhold.png (100%) rename docs/src/{how-to/install => understand}/img/legalhold-step03-click-arrow.png (100%) rename docs/src/{how-to/install => understand}/img/legalhold-step04-click-manage-configuration.png (100%) rename docs/src/{how-to/install => understand}/img/legalhold-step05-fill-info.png (100%) rename docs/src/{how-to/install => understand}/legalhold.md (100%) rename docs/src/{how-to/install => understand}/mls.md (98%) create mode 100644 docs/src/understand/searchability.md rename docs/src/{how-to => understand}/single-sign-on/adfs/fig-00.jpg (100%) rename docs/src/{how-to => understand}/single-sign-on/adfs/fig-01.jpg (100%) rename docs/src/{how-to => understand}/single-sign-on/adfs/fig-02.jpg (100%) rename docs/src/{how-to => understand}/single-sign-on/adfs/fig-03.jpg (100%) rename docs/src/{how-to => understand}/single-sign-on/adfs/fig-04.jpg (100%) rename docs/src/{how-to => understand}/single-sign-on/adfs/fig-05.jpg (100%) rename docs/src/{how-to => understand}/single-sign-on/adfs/fig-06.jpg (100%) rename docs/src/{how-to => understand}/single-sign-on/adfs/fig-07.jpg (100%) rename docs/src/{how-to => understand}/single-sign-on/adfs/fig-08.jpg (100%) rename docs/src/{how-to => understand}/single-sign-on/adfs/fig-09.jpg (100%) rename docs/src/{how-to => understand}/single-sign-on/adfs/fig-10.jpg (100%) rename docs/src/{how-to => understand}/single-sign-on/adfs/fig-11.jpg (100%) rename docs/src/{how-to => understand}/single-sign-on/adfs/main.md (100%) rename docs/src/{how-to => understand}/single-sign-on/azure/01.png (100%) rename docs/src/{how-to => understand}/single-sign-on/azure/02.png (100%) rename docs/src/{how-to => understand}/single-sign-on/azure/03.png (100%) rename docs/src/{how-to => understand}/single-sign-on/azure/04.png (100%) rename docs/src/{how-to => understand}/single-sign-on/azure/05.png (100%) rename docs/src/{how-to => understand}/single-sign-on/azure/06.png (100%) rename docs/src/{how-to => understand}/single-sign-on/azure/07.png (100%) rename docs/src/{how-to => understand}/single-sign-on/azure/08.png (100%) rename docs/src/{how-to => understand}/single-sign-on/azure/09.png (100%) rename docs/src/{how-to => understand}/single-sign-on/azure/10.png (100%) rename docs/src/{how-to => understand}/single-sign-on/azure/11.png (100%) rename docs/src/{how-to => understand}/single-sign-on/azure/12.png (100%) rename docs/src/{how-to => understand}/single-sign-on/azure/13.png (100%) rename docs/src/{how-to => understand}/single-sign-on/azure/14.png (100%) rename docs/src/{how-to => understand}/single-sign-on/azure/15.png (100%) rename docs/src/{how-to => understand}/single-sign-on/azure/main.md (100%) rename docs/src/{how-to => understand}/single-sign-on/centrify/001.png (100%) rename docs/src/{how-to => understand}/single-sign-on/centrify/002.png (100%) rename docs/src/{how-to => understand}/single-sign-on/centrify/003.png (100%) rename docs/src/{how-to => understand}/single-sign-on/centrify/004.png (100%) rename docs/src/{how-to => understand}/single-sign-on/centrify/005.png (100%) rename docs/src/{how-to => understand}/single-sign-on/centrify/006.png (100%) rename docs/src/{how-to => understand}/single-sign-on/centrify/007.png (100%) rename docs/src/{how-to => understand}/single-sign-on/centrify/008.png (100%) rename docs/src/{how-to => understand}/single-sign-on/centrify/009.png (100%) rename docs/src/{how-to => understand}/single-sign-on/centrify/010.png (100%) rename docs/src/{how-to => understand}/single-sign-on/centrify/011.png (100%) rename docs/src/{how-to => understand}/single-sign-on/centrify/main.md (100%) rename docs/src/{how-to => understand}/single-sign-on/generic-setup.md (100%) rename docs/src/{how-to => understand}/single-sign-on/index.md (92%) rename docs/src/{how-to => understand}/single-sign-on/okta/001-applications-screen.png (100%) rename docs/src/{how-to => understand}/single-sign-on/okta/002-add-application.png (100%) rename docs/src/{how-to => understand}/single-sign-on/okta/003-add-application-1.png (100%) rename docs/src/{how-to => understand}/single-sign-on/okta/004-add-application-step1.png (100%) rename docs/src/{how-to => understand}/single-sign-on/okta/005-add-application-step2.png (100%) rename docs/src/{how-to => understand}/single-sign-on/okta/006-add-application-step3.png (100%) rename docs/src/{how-to => understand}/single-sign-on/okta/007-application-sign-on.png (100%) rename docs/src/{how-to => understand}/single-sign-on/okta/008-assignment.png (100%) rename docs/src/{how-to => understand}/single-sign-on/okta/main.md (100%) rename docs/src/{how-to => understand}/single-sign-on/trouble-shooting.md (100%) rename docs/src/{how-to => understand}/single-sign-on/understand/Wire_SAML_Flow (lucidchart).svg (100%) rename docs/src/{how-to => understand}/single-sign-on/understand/Wire_SAML_Flow.png (100%) rename docs/src/{how-to => understand}/single-sign-on/understand/main.md (100%) rename docs/src/{how-to => understand}/single-sign-on/understand/token-step-01.png (100%) rename docs/src/{how-to => understand}/single-sign-on/understand/token-step-02.png (100%) rename docs/src/{how-to => understand}/single-sign-on/understand/token-step-03.png (100%) rename docs/src/{how-to => understand}/single-sign-on/understand/token-step-04.png (100%) rename docs/src/{how-to => understand}/single-sign-on/understand/token-step-05.png (100%) rename docs/src/{how-to => understand}/single-sign-on/understand/token-step-06.png (100%) rename docs/src/{how-to/install => understand}/team-feature-settings.md (100%) diff --git a/changelog.d/5-internal/restructure-docs b/changelog.d/5-internal/restructure-docs new file mode 100644 index 0000000000..f9c770a9cc --- /dev/null +++ b/changelog.d/5-internal/restructure-docs @@ -0,0 +1 @@ +Restructure docs.wire.com diff --git a/docs/.gitignore b/docs/.gitignore index 4ca8ba1127..2e23a54802 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -6,3 +6,6 @@ build # direnv - nix derivation result + +# this is so that the nix build doesn't copy a dangling symlink +src/changelog/changelog.md diff --git a/docs/diagrams/mmdc b/docs/diagrams/mmdc deleted file mode 120000 index df57a81b6b..0000000000 --- a/docs/diagrams/mmdc +++ /dev/null @@ -1 +0,0 @@ -./node_modules/.bin/mmdc \ No newline at end of file diff --git a/docs/src/changelog/changelog.md b/docs/src/changelog/changelog.md new file mode 120000 index 0000000000..79b747aee1 --- /dev/null +++ b/docs/src/changelog/changelog.md @@ -0,0 +1 @@ +../../../CHANGELOG.md \ No newline at end of file diff --git a/docs/src/changelog/index.md b/docs/src/changelog/index.md new file mode 100644 index 0000000000..8eb248cd07 --- /dev/null +++ b/docs/src/changelog/index.md @@ -0,0 +1,9 @@ +# Releases + +```{toctree} +:caption: 'Contents:' +:glob: true +:maxdepth: 1 + +Releases +``` diff --git a/docs/src/conf.py b/docs/src/conf.py index 750a09f904..c91faf4b90 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -136,5 +136,6 @@ "security-responses/log4shell": "2021-12-15_log4shell.html", "security-responses/cve-2021-44521": "2022-02-21_cve-2021-44521.html", "security-responses/2022-05_website_outage": "2022-05-23_website_outage.html", + "how-to/single-sign-on/index": "../../understand/single-sign-on/index.html", "how-to/scim/index": "../../understand/single-sign-on/main.html#user-provisioning" } diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index 98eb07ff17..938ddba131 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -30,8 +30,7 @@ production. ### MLS private key paths -Note: This developer documentation. Documentation for site operators can be found here: -[Messaging Layer Security (MLS)](../../how-to/install/mls.md). +Note: This developer documentation. Documentation for site operators can be found here: {ref}`mls-message-layer-security` The `mlsPrivateKeyPaths` field should contain a mapping from *purposes* and signature schemes to file paths of corresponding x509 private keys in PEM diff --git a/docs/src/developer/reference/spar-braindump.md b/docs/src/developer/reference/spar-braindump.md index dcee5847e9..05735e98c6 100644 --- a/docs/src/developer/reference/spar-braindump.md +++ b/docs/src/developer/reference/spar-braindump.md @@ -1,7 +1,12 @@ # Spar braindump Reference: {#SparBrainDump} - +/home/stefan/repos/wire-server/docs/src/how-to/install/includes/helm_dns-ingress-troubleshooting.inc.rst:147: WARNING: duplicate label trying things out, other instance in /home/stefan/repos/wire-server/docs/src/how-to/install/helm.md +/home/stefan/repos/wire-server/docs/src/how-to/install/includes/helm_dns-ingress-troubleshooting.inc.rst:170: WARNING: duplicate label troubleshooting, other instance in /home/stefan/repos/wire-server/docs/src/how-to/install/helm.md +/home/stefan/repos/wire-server/docs/src/developer/reference/config-options.md:33: WARNING: 'myst' reference target not found: ../../how-to/install/mls.md +/home/stefan/repos/wire-server/docs/src/developer/reference/spar-braindump.md:116: WARNING: 'myst' reference target not found: ../../how-to/single-sign-on/understand/main.rst +/home/stefan/repos/wire-server/docs/src/how-to/install/ansible-VMs.md:97: WARNING: undefined label: 'checks' +/home/stefan/repos/wire-server/docs/src/understand/federation/api.md:162: WARNING: 'myst' reference target not found: ../../how-to/install/mls _Author: Matthias Fischmann_ --- @@ -113,7 +118,8 @@ export IDP_ID=... Copy the new metadata file to one of your spar instances. -Ssh into it. If you can't, [the sso docs](../../how-to/single-sign-on/understand/main.rst) explain how you can create a + +Ssh into it. If you can't, {ref}`the sso docs ` explain how you can create a bearer token if you have the admin's login credentials. If you follow that approach, you need to replace all mentions of `-H'Z-User ...'` with `-H'Authorization: Bearer ...'` in the following, and you won't need diff --git a/docs/src/how-to/install/index.md b/docs/src/how-to/install/index.md index 2758ad819a..b45b694832 100644 --- a/docs/src/how-to/install/index.md +++ b/docs/src/how-to/install/index.md @@ -7,22 +7,22 @@ How to plan an installation Version requirements dependencies -(demo) How to install kubernetes -(demo) How to install wire-server using Helm -(production) Introduction -(production) How to install kubernetes and databases -(production) How to configure AWS services -(production) How to install wire-server using Helm -(production) How to monitor wire-server -(production) How to see centralized logs for wire-server -Server and team feature settings -Messaging Layer Security (MLS) + +How to install kubernetes (Demo) +How to install wire-server using Helm (Demo) + +Introduction +How to install kubernetes and databases +How to configure AWS services +How to install wire-server using Helm +Infrastructure configuration +How to monitor wire-server +How to see centralized logs for wire-server + Web app settings sft restund -configure-federation tls -How to install and set up Legal Hold Managing authentication with ansible Using tinc Troubleshooting during installation diff --git a/docs/src/configuration-options.md b/docs/src/how-to/install/infrastructure-configuration.md similarity index 50% rename from docs/src/configuration-options.md rename to docs/src/how-to/install/infrastructure-configuration.md index 1eeee72383..0e9d9a0029 100644 --- a/docs/src/configuration-options.md +++ b/docs/src/how-to/install/infrastructure-configuration.md @@ -1,6 +1,6 @@ (configuration-options)= -# Part 3 - configuration options in a production setup +# Infrastructure configuration options This contains instructions to configure specific aspects of your production setup depending on your needs. @@ -288,39 +288,6 @@ websockets: enabled: false ``` -## Blocking creation of personal users, new teams - -### In Brig - -There are some unauthenticated end-points that allow arbitrary users on the open internet to do things like create a new team. This is desired in the cloud, but if you run an on-prem setup that is open to the world, you may want to block this. - -Brig has a server option for this: - -```yaml -optSettings: - setRestrictUserCreation: true -``` - -If `setRestrictUserCreation` is `true`, creating new personal users or new teams on your instance from outside your backend installation is impossible. (If you want to be more technical: requests to `/register` that create a new personal account or a new team are answered with `403 forbidden`.) - -On instances with restricted user creation, the site operator with access to the internal REST API can still circumvent the restriction: just log into a brig service pod via ssh and follow the steps in `hack/bin/create_test_team_admins.sh.` - -```{note} -Once the creation of new users and teams has been disabled, it will still be possible to use the [team creation process](https://support.wire.com/hc/en-us/articles/115003858905-Create-a-team) (enter the new team name, email, password, etc), but it will fail/refuse creation late in the creation process (after the «Create team» button is clicked). -``` - -### In the WebApp - -Another way of disabling user registration is by this webapp setting, in `values.yaml`, changing this value from `true` to `false`: - -```yaml -FEATURE_ENABLE_ACCOUNT_REGISTRATION: "false" -``` - -```{note} -If you only disable the creation of users in the webapp, but do not do so in Brig/the backend, a malicious user would be able to use the API to create users, so make sure to disable both. -``` - ## You may want - more server resources to ensure @@ -666,342 +633,6 @@ brig: retryAfter: 86400 ``` -## Configuring searchability - -You can configure how search is limited or not based on user membership in a given team. - -There are two types of searches based on the direction of search: - -- **Inbound** searches mean that somebody is searching for you. Configuring the inbound search visibility means that you (or some admin) can configure whether others can find you or not. -- **Outbound** searches mean that you are searching for somebody. Configuring the outbound search visibility means that some admin can configure whether you can find other users or not. - -There are different types of matches: - -- **Exact handle** search means that the user is found only if the search query is exactly the user handle (e.g. searching for `mc` will find `@mc` but not `@mccaine`). This search returns zero or one results. -- **Full text** search means that the user is found if the search query contains some subset of the user display name and handle. (e.g. the query `mar` will find `Marco C`, `Omar`, `@amaro`) - -### Searching users on the same backend - -Search visibility is controlled by three parameters on the backend: - -- A team outbound configuration flag, `TeamSearchVisibility` with possible values `SearchVisibilityStandard`, `SearchVisibilityNoNameOutsideTeam` - - - `SearchVisibilityStandard` means that the user can find other people outside of the team, if the searched-person inbound search allows it - - `SearchVisibilityNoNameOutsideTeam` means that the user can not find any user outside the team by full text search (but exact handle search still works) - -- A team inbound configuration flag, `SearchVisibilityInbound` with possible values `SearchableByOwnTeam`, `SearchableByAllTeams` - - - `SearchableByOwnTeam` means that the user can be found only by users in their own team. - - `SearchableByAllTeams` means that the user can be found by users in any/all teams. - -- A server configuration flag `searchSameTeamOnly` with possible values true, false. - - - `Note`: For the same backend, this affects inbound and outbound searches (simply because all teams will be subject to this behavior) - - Setting this to `true` means that the all teams on that backend can only find users that belong to their team - -These flag are set on the backend and the clients do not need to be aware of them. - -The flags will influence the behavior of the search API endpoint; clients will only need to parse the results, that are already filtered for them by the backend. - -#### Table of possible outcomes - -```{eval-rst} -+------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -| Is search-er (`uA`) in team (tA)? | Is search-ed (`uB`) in a team? | Backend flag `searchSameTeamOnly` | Team `tA`'s flag `TeamSearchVisibility` | Team tB's flag `SearchVisibilityInbound` | Result of exact search for `uB` | Result of full-text search for `uB` | -+====================================+=================================+====================================+==========================================+===========================================+==================================+======================================+ -| **Search within the same team** | -+------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -| Yes, `tA` | Yes, the same team `tA` | Irrelevant | Irrelevant | Irrelevant | Found | Found | -+------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -| **Outbound search unrestricted** | -+------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -| Yes, `tA` | Yes, another team tB | false | `SearchVisibilityStandard` | `SearchableByAllTeams` | Found | Found | -+------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -| Yes, `tA` | Yes, another team tB | false | `SearchVisibilityStandard` | `SearchableByOwnTeam` | Found | Not found | -+------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -| **Outbound search restricted** | -+------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -| Yes, `tA` | Yes, another team tB | true | Irrelevant | Irrelevant | Not found | Not found | -+------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -| Yes, `tA` | Yes, another team tB | false | `SearchVisibilityNoNameOutsideTeam` | Irrelevant | Found | Not found | -+------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -| Yes, `tA` | No | false | `SearchVisibilityNoNameOutsideTeam` | There’s no team B | Found | Not found | -+------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -``` - -#### Changing the configuration on the server - -To change the `searchSameTeamOnly` setting on the backend, edit the `values.yaml.gotmpl` file for the wire-server chart at this nested level of the configuration: - -```yaml -brig: - # ... - config: - # ... - optSettings: - # ... - setSearchSameTeamOnly: true -``` - -If `setSearchSameTeamOnly` is set to `true` then `TeamSearchVisibility` is forced be in the `SearchVisibilityNoNameOutsideTeam` setting for all teams. - -#### Changing the default configuration for all teams - -If `setSearchSameTeamOnly` is set to `false` (or missing from the configuration) then the default value `TeamSearchVisibility` can be configured at this level of the configuration of the `value.yaml.gotmpl` file of the wire-server chart: - -```yaml -galley: - #... - config: - #... - settings: - #... - featureFlags: - #... - teamSearchVisibility: enabled-by-default -``` - -This default value applies to all teams for which no explicit configuration of the `TeamSearchVisibility` has been set. - -### Searching users on another (federated) backend - -For federated search the table above does not apply, see following table. - -```{note} -Incoming federated searches (i.e. searches from one backend to another) are considered always as being performed from a team user, even if they are performed from a personal user. - -This is because the incoming search request does not carry the information whether the user performing the search was in a team or not. - -So we have to make one assumption, and we assume that they were in a team. -``` - -Allowing search is done at the backend configuration level by the sysadmin: - -- Outbound search restrictions (`searchSameTeamOnly`, `TeamSearchVisibility`) do not apply to federated searches - -- A configuration setting `FederatedUserSearchPolicy` per federating domain with these possible values: - - - `no_search` The federating backend is not allowed to search any users (either by exact handle or full-text). - - `exact_handle_search` The federating backend may only search by exact handle - - `full_search` The federating backend may search users by full text search on display name and handle. The search search results are additionally affected by `SearchVisibilityInbound` setting of each team on the backend. - -- The `SearchVisibilityInbound` setting applies. Since the default value for teams is `SearchableByOwnTeam` this means that for a team to be full-text searchable by users on a federating backend both - - - `FederatedUserSearchPolicy` needs to be set to to full_search for the federating backend - - Any team that wants to be full-text searchable needs to be set to `SearchableByAllTeams` - -The configuration value `FederatedUserSearchPolicy` is per federated domain, e.g. in the values of the wire-server chart: - -```yaml -brig: - config: - optSettings: - setFederationDomainConfigs: - - domain: a.example.com - search_policy: no_search - - domain: a.example.com - search_policy: full_search -``` - -#### Table of possible outcomes - -In the following table, user `uA` on backend A is searching for user `uB` on team `tB` on backend B. - -Any of the flags set for searching users on the same backend are ignored. - -It’s worth nothing that if two users are on two separate backend, they are also guaranteed to be on two separate teams, as teams can not spread across backends. - -| Who is searching | Backend B flag `FederatedUserSearchPolicy` | Team `tB`'s flag `SearchVisibilityInbound` | Result of exact search for `uB` | Result of full-text search for `uB` | -| ---------------------- | ------------------------------------------ | ------------------------------------------ | ------------------------------- | ----------------------------------- | -| user `uA` on backend A | `no_search` | Irrelevant | Not found | Not found | -| user `uA` on backend A | `exact_handle_search` | Irrelevant | Found | Not found | -| user `uA` on backend A | `full_search` | SearchableByOwnTeam | Found | Not found | -| user `uA` on backend A | `full_search` | SearchableByAllTeams | Found | Found | - -### Changing the settings for a given team - -If you need to change searchabilility for a specific team (rather than the entire backend, as above), you need to make specific calls to the API. - -#### Team searchVisibility - -The team flag `searchVisibility` affects the outbound search of user searches. - -If it is set to `no-name-outside-team` for a team then all users of that team will no longer be able to find users that are not part of their team when searching. - -This also includes finding other users by by providing their exact handle. By default it is set to `standard`, which doesn't put any additional restrictions to outbound searches. - -The setting can be changed via endpoint (for more details on how to make the API calls with `curl`, read further): - -``` -GET /teams/{tid}/search-visibility - -- Shows the current TeamSearchVisibility value for the given team - -PUT /teams/{tid}/search-visibility - -- Set specific search visibility for the team - -pull-down-menu "body": - "standard" - "no-name-outside-team" -``` - -The team feature flag `teamSearchVisibility` determines whether it is allowed to change the `searchVisibility` setting or not. - -The default is `disabled-by-default`. - -```{note} -Whenever this feature setting is disabled the `searchVisibility` will be reset to standard. -``` - -The default setting that applies to all teams on the instance can be defined at configuration - -```yaml -settings: - featureFlags: - teamSearchVisibility: disabled-by-default # or enabled-by-default -``` - -#### TeamFeature searchVisibilityInbound - -The team feature flag `searchVisibilityInbound` affects if the team's users are searchable by users from other teams. - -The default setting is `searchable-by-own-team` which hides users from search results by users from other teams. - -If it is set to `searchable-by-all-teams` then users of this team may be included in the results of search queries by other users. - -```{note} -The configuration of this flag does not affect search results when the search query matches the handle exactly. - -If the handle is provdided then any user on the instance can find users. -``` - -This team feature flag can only by toggled by site-administrators with direct access to the galley instance (for more details on how to make the API calls with `curl`, read further): - -``` -PUT /i/teams/{tid}/features/search-visibility-inbound -``` - -With JSON body: - -```json -{"status": "enabled"} -``` - -or - -```json -{"status": "disabled"} -``` - -Where `enabled` is equivalent to `searchable-by-all-teams` and `disabled` is equivalent to `searchable-by-own-team`. - -The default setting that applies to all teams on the instance can be defined at configuration. - -```yaml -searchVisibilityInbound: - defaults: - status: enabled # OR disabled -``` - -Individual teams can overwrite the default setting with API calls as per above. - -#### Making the API calls - -To make API calls to set an explicit configuration for\` TeamSearchVisibilityInbound\` per team, you first need to know the Team ID, which can be found in the team settings app. - -It is an `UUID` which has format like this `dcbedf9a-af2a-4f43-9fd5-525953a919e1`. - -In the following we will be using this Team ID as an example, please replace it with your own team id. - -Next find the name of a `galley` pod by looking at the output of running this command: - -```sh -kubectl -n wire get pods -``` - -The output will look something like this: - -``` -... -galley-5f4787fdc7-9l64n ... -galley-migrate-data-lzz5j ... -... -``` - -Select any of the galley pods, for example we will use `galley-5f4787fdc7-9l64n`. - -Next, set up a port-forwarding from your local machine's port `9000` to the galley's port `8080` by running: - -```sh -kubectl port-forward -n wire galley-5f4787fdc7-9l64n 9000:8080 -``` - -Keep this command running until the end of these instuctions. - -Please run the following commands in a seperate terminal while keeping the terminal which establishes the port-forwarding open. - -To see team's current setting run: - -```sh -curl -XGET http://localhost:9000/i/teams/dcbedf9a-af2a-4f43-9fd5-525953a919e1/features/searchVisibilityInbound - -# {"lockStatus":"unlocked","status":"disabled"} -``` - -Where `disabled` corresponds to `SearchableByOwnTeam` and enabled corresponds to `SearchableByAllTeams`. - -To change the `TeamSearchVisibilityInbound` to `SearchableByAllTeams` for the team run: - -```sh -curl -XPUT -H 'Content-Type: application/json' -d "{\"status\": \"enabled\"}" http://localhost:9000/i/teams/dcbedf9a-af2a-4f43-9fd5-525953a919e1/features/searchVisibilityInbound -``` - -To change the TeamSearchVisibilityInbound to SearchableByOwnTeam for the team run: - -```sh -curl -XPUT -H 'Content-Type: application/json' -d "{\"status\": \"disabled\"}" http://localhost:9000/i/teams/dcbedf9a-af2a-4f43-9fd5-525953a919e1/features/searchVisibilityInbound -``` - -## Configuring classified domains - -As a backend administrator, if you want to control which other backends (identified by their domain) are "classified", - -change the following `galley` configuration in the `value.yaml.gotmpl` file of the wire-server chart: - -```yaml -galley: - replicaCount: 1 - config: - ... - featureFlags: - ... - classifiedDomains: - status: enabled - config: - domains: ["domain-that-is-classified.link"] - ... -``` - -This is not only a `backend` configuration, but also a `team` configuration/feature. - -This means that different combinations of configurations will have different results. - -Here is a table to navigate the possible configurations: - -| Backend Config enabled/disabled | Backend Config Domains | Team Config enabled/disabled | Team Config Domains | User's view | -| ------------------------------- | ---------------------------------------------- | ---------------------------- | ----------------------- | -------------------------------- | -| Enabled | \[domain1.example.com\] | Not configured | Not configured | Enabled, \[domain1.example.com\] | -| Enabled | \[domain1.example.com\]\[domain1.example.com\] | Enabled | Not configured | Enabled, \[domain1.example.com\] | -| Enabled | \[domain1.example.com\] | Enabled | \[domain2.example.com\] | Enabled, Undefined | -| Enabled | \[domain1.example.com\] | Disabled | Anything | Undefined | -| Disabled | Anything | Not configured | Not configured | Disabled, no domains | -| Disabled | Anything | Enabled | \[domain2.example.com\] | Undefined | - -The table assumes the following: - -- When backend level config says that this feature is enabled, it is illegal to not specify domains at the backend level. -- When backend level config says that this feature is disabled, the list of domains is ignored. -- When team level feature is disabled, the accompanying domains are ignored. - ## S3 Addressing Style S3 can either by addressed in path style, i.e. diff --git a/docs/src/index.md b/docs/src/index.md index da0c17e170..bb8d35a63b 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -15,24 +15,19 @@ The targeted audience of this documentation is: If you are a developer, you may want to check out the "Notes for developers" first. -This documentation may be expanded in the future to cover other aspects of Wire. +Release notes of `wire-server` can be found [here](https://github.com/wireapp/wire-server/releases). ```{toctree} :caption: 'Contents:' :glob: true :maxdepth: 1 -Release notes - +Security responses +Release Notes Installation Administration -Connecting Wire Clients -Optional Configuration -Understanding wire-server components -Single-Sign-On and user provisioning -API documentation -Security responses -Notes for developers +Reference +Developers Notes ``` % Overview diff --git a/docs/src/release-notes.md b/docs/src/release-notes.md deleted file mode 100644 index 478db87668..0000000000 --- a/docs/src/release-notes.md +++ /dev/null @@ -1,13 +0,0 @@ -(release-notes)= - -# Release notes - -This page previously contained the release notes for the project, and they were manually updated each time a new release was done, due to limitations in Github's «releases» feature. - -However, Github since updated the feature, making this page un-necessary. - -Go to → [GitHub - wireapp/wire-server: Wire back-end services](https://github.com/wireapp/wire-server/) - -→ Look at releases on right hand side. They are shown by date of release. [Release Notes](https://github.com/wireapp/wire-server/releases) - -→ Open the CHANGELOG.md. This will give you chart version. diff --git a/docs/src/how-to/associate/custom-backend-for-desktop-client.md b/docs/src/understand/associate/custom-backend-for-desktop-client.md similarity index 100% rename from docs/src/how-to/associate/custom-backend-for-desktop-client.md rename to docs/src/understand/associate/custom-backend-for-desktop-client.md diff --git a/docs/src/how-to/associate/custom-certificates.md b/docs/src/understand/associate/custom-certificates.md similarity index 100% rename from docs/src/how-to/associate/custom-certificates.md rename to docs/src/understand/associate/custom-certificates.md diff --git a/docs/src/how-to/associate/deeplink.md b/docs/src/understand/associate/deeplink.md similarity index 100% rename from docs/src/how-to/associate/deeplink.md rename to docs/src/understand/associate/deeplink.md diff --git a/docs/src/how-to/associate/index.md b/docs/src/understand/associate/index.md similarity index 100% rename from docs/src/how-to/associate/index.md rename to docs/src/understand/associate/index.md diff --git a/docs/src/understand/block-user-creation.md b/docs/src/understand/block-user-creation.md new file mode 100644 index 0000000000..5c1e563aab --- /dev/null +++ b/docs/src/understand/block-user-creation.md @@ -0,0 +1,34 @@ +# Block personal user creation + +## In Brig + +There are some unauthenticated end-points that allow arbitrary users on the open internet to do things like create a new team. This is desired in the cloud, but if you run an on-prem setup that is open to the world, you may want to block this. + +Brig has a server option for this: + +```yaml +optSettings: + setRestrictUserCreation: true +``` + +If `setRestrictUserCreation` is `true`, creating new personal users or new teams on your instance from outside your backend installation is impossible. (If you want to be more technical: requests to `/register` that create a new personal account or a new team are answered with `403 forbidden`.) + +On instances with restricted user creation, the site operator with access to the internal REST API can still circumvent the restriction: just log into a brig service pod via ssh and follow the steps in `hack/bin/create_test_team_admins.sh.` + +```{note} +Once the creation of new users and teams has been disabled, it will still be possible to use the [team creation process](https://support.wire.com/hc/en-us/articles/115003858905-Create-a-team) (enter the new team name, email, password, etc), but it will fail/refuse creation late in the creation process (after the «Create team» button is clicked). +``` + +## In the WebApp + +Another way of disabling user registration is by this webapp setting, in `values.yaml`, changing this value from `true` to `false`: + +```yaml +FEATURE_ENABLE_ACCOUNT_REGISTRATION: "false" +``` + +```{note} +If you only disable the creation of users in the webapp, but do not do so in Brig/the backend, a malicious user would be able to use the API to create users, so make sure to disable both. +``` + + diff --git a/docs/src/understand/classified-domains.md b/docs/src/understand/classified-domains.md new file mode 100644 index 0000000000..5d27945abb --- /dev/null +++ b/docs/src/understand/classified-domains.md @@ -0,0 +1,40 @@ +# Classified Domains + +As a backend administrator, if you want to control which other backends (identified by their domain) are "classified", + +change the following `galley` configuration in the `value.yaml.gotmpl` file of the wire-server chart: + +```yaml +galley: + replicaCount: 1 + config: + ... + featureFlags: + ... + classifiedDomains: + status: enabled + config: + domains: ["domain-that-is-classified.link"] + ... +``` + +This is not only a `backend` configuration, but also a `team` configuration/feature. + +This means that different combinations of configurations will have different results. + +Here is a table to navigate the possible configurations: + +| Backend Config enabled/disabled | Backend Config Domains | Team Config enabled/disabled | Team Config Domains | User's view | +| ------------------------------- | ---------------------------------------------- | ---------------------------- | ----------------------- | -------------------------------- | +| Enabled | \[domain1.example.com\] | Not configured | Not configured | Enabled, \[domain1.example.com\] | +| Enabled | \[domain1.example.com\]\[domain1.example.com\] | Enabled | Not configured | Enabled, \[domain1.example.com\] | +| Enabled | \[domain1.example.com\] | Enabled | \[domain2.example.com\] | Enabled, Undefined | +| Enabled | \[domain1.example.com\] | Disabled | Anything | Undefined | +| Disabled | Anything | Not configured | Not configured | Disabled, no domains | +| Disabled | Anything | Enabled | \[domain2.example.com\] | Undefined | + +The table assumes the following: + +- When backend level config says that this feature is enabled, it is illegal to not specify domains at the backend level. +- When backend level config says that this feature is disabled, the list of domains is ignored. +- When team level feature is disabled, the accompanying domains are ignored. diff --git a/docs/src/how-to/install/configure-federation.md b/docs/src/understand/configure-federation.md similarity index 99% rename from docs/src/how-to/install/configure-federation.md rename to docs/src/understand/configure-federation.md index 69396c92b5..6d0042eaad 100644 --- a/docs/src/how-to/install/configure-federation.md +++ b/docs/src/understand/configure-federation.md @@ -1,5 +1,5 @@ (configure-federation)= -# Configure Wire-Server for Federation +# Federation See also {ref}`federation-understand`, which explains the architecture and concepts. diff --git a/docs/src/understand/federation/api.md b/docs/src/understand/federation/api.md index e48e642294..7b576d9234 100644 --- a/docs/src/understand/federation/api.md +++ b/docs/src/understand/federation/api.md @@ -159,7 +159,7 @@ the backend. - `get-user-clients`: Given a list of user ids, return a list of all their clients with public information - `send-connection-action`: Make and also respond to user connection requests - `on-user-deleted-connections`: Notify users that are connected to remote user about that user's deletion -- `get-mls-clients`: Request all [MLS](../../how-to/install/mls)-capable clients for a given user +- `get-mls-clients`: Request all {ref}`MLS `-capable clients for a given user - `claim-key-packages`: Claim a previously-uploaded KeyPackage of a remote user. User for adding users to MLS conversations. See [the brig source diff --git a/docs/src/how-to/install/img/legalhold-screencast.gif b/docs/src/understand/img/legalhold-screencast.gif similarity index 100% rename from docs/src/how-to/install/img/legalhold-screencast.gif rename to docs/src/understand/img/legalhold-screencast.gif diff --git a/docs/src/how-to/install/img/legalhold-step01-click-customization.png b/docs/src/understand/img/legalhold-step01-click-customization.png similarity index 100% rename from docs/src/how-to/install/img/legalhold-step01-click-customization.png rename to docs/src/understand/img/legalhold-step01-click-customization.png diff --git a/docs/src/how-to/install/img/legalhold-step02-goto-legalhold.png b/docs/src/understand/img/legalhold-step02-goto-legalhold.png similarity index 100% rename from docs/src/how-to/install/img/legalhold-step02-goto-legalhold.png rename to docs/src/understand/img/legalhold-step02-goto-legalhold.png diff --git a/docs/src/how-to/install/img/legalhold-step03-click-arrow.png b/docs/src/understand/img/legalhold-step03-click-arrow.png similarity index 100% rename from docs/src/how-to/install/img/legalhold-step03-click-arrow.png rename to docs/src/understand/img/legalhold-step03-click-arrow.png diff --git a/docs/src/how-to/install/img/legalhold-step04-click-manage-configuration.png b/docs/src/understand/img/legalhold-step04-click-manage-configuration.png similarity index 100% rename from docs/src/how-to/install/img/legalhold-step04-click-manage-configuration.png rename to docs/src/understand/img/legalhold-step04-click-manage-configuration.png diff --git a/docs/src/how-to/install/img/legalhold-step05-fill-info.png b/docs/src/understand/img/legalhold-step05-fill-info.png similarity index 100% rename from docs/src/how-to/install/img/legalhold-step05-fill-info.png rename to docs/src/understand/img/legalhold-step05-fill-info.png diff --git a/docs/src/understand/index.md b/docs/src/understand/index.md index f7ca56369a..dd9474bceb 100644 --- a/docs/src/understand/index.md +++ b/docs/src/understand/index.md @@ -1,17 +1,19 @@ (understand)= -# Understanding wire-server components - -This section is almost empty, more documentation will come soon... +# Reference ```{toctree} :glob: true -:maxdepth: 1 +:maxdepth: 2 -Overview +Architecture Overview +Single Sign-On and User Provisioning Audio/video calling, restund servers (TURN/STUN) Conference Calling 2.0 (SFT) Minio Helm Federation +Connecting Wire Clients +Client API documentation +* ``` diff --git a/docs/src/how-to/install/legalhold.md b/docs/src/understand/legalhold.md similarity index 100% rename from docs/src/how-to/install/legalhold.md rename to docs/src/understand/legalhold.md diff --git a/docs/src/how-to/install/mls.md b/docs/src/understand/mls.md similarity index 98% rename from docs/src/how-to/install/mls.md rename to docs/src/understand/mls.md index 9e4543b011..591451a0ab 100644 --- a/docs/src/how-to/install/mls.md +++ b/docs/src/understand/mls.md @@ -1,3 +1,5 @@ +(mls-message-layer-security)= + # Messaging Layer Security (MLS) To enable support for [MLS](https://datatracker.ietf.org/wg/mls/documents/) diff --git a/docs/src/understand/overview.md b/docs/src/understand/overview.md index 56f203f707..6926a81280 100644 --- a/docs/src/understand/overview.md +++ b/docs/src/understand/overview.md @@ -1,6 +1,6 @@ (overview)= -# Overview +# Architecture Overview ## Introduction diff --git a/docs/src/understand/searchability.md b/docs/src/understand/searchability.md new file mode 100644 index 0000000000..083faa030f --- /dev/null +++ b/docs/src/understand/searchability.md @@ -0,0 +1,295 @@ +# User Searchability + +You can configure how search is limited or not based on user membership in a given team. + +There are two types of searches based on the direction of search: + +- **Inbound** searches mean that somebody is searching for you. Configuring the inbound search visibility means that you (or some admin) can configure whether others can find you or not. +- **Outbound** searches mean that you are searching for somebody. Configuring the outbound search visibility means that some admin can configure whether you can find other users or not. + +There are different types of matches: + +- **Exact handle** search means that the user is found only if the search query is exactly the user handle (e.g. searching for `mc` will find `@mc` but not `@mccaine`). This search returns zero or one results. +- **Full text** search means that the user is found if the search query contains some subset of the user display name and handle. (e.g. the query `mar` will find `Marco C`, `Omar`, `@amaro`) + +## Searching users on the same backend + +Search visibility is controlled by three parameters on the backend: + +- A team outbound configuration flag, `TeamSearchVisibility` with possible values `SearchVisibilityStandard`, `SearchVisibilityNoNameOutsideTeam` + + - `SearchVisibilityStandard` means that the user can find other people outside of the team, if the searched-person inbound search allows it + - `SearchVisibilityNoNameOutsideTeam` means that the user can not find any user outside the team by full text search (but exact handle search still works) + +- A team inbound configuration flag, `SearchVisibilityInbound` with possible values `SearchableByOwnTeam`, `SearchableByAllTeams` + + - `SearchableByOwnTeam` means that the user can be found only by users in their own team. + - `SearchableByAllTeams` means that the user can be found by users in any/all teams. + +- A server configuration flag `searchSameTeamOnly` with possible values true, false. + + - `Note`: For the same backend, this affects inbound and outbound searches (simply because all teams will be subject to this behavior) + - Setting this to `true` means that the all teams on that backend can only find users that belong to their team + +These flag are set on the backend and the clients do not need to be aware of them. + +The flags will influence the behavior of the search API endpoint; clients will only need to parse the results, that are already filtered for them by the backend. + +### Table of possible outcomes + +```{eval-rst} ++------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ +| Is search-er (`uA`) in team (tA)? | Is search-ed (`uB`) in a team? | Backend flag `searchSameTeamOnly` | Team `tA`'s flag `TeamSearchVisibility` | Team tB's flag `SearchVisibilityInbound` | Result of exact search for `uB` | Result of full-text search for `uB` | ++====================================+=================================+====================================+==========================================+===========================================+==================================+======================================+ +| **Search within the same team** | ++------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ +| Yes, `tA` | Yes, the same team `tA` | Irrelevant | Irrelevant | Irrelevant | Found | Found | ++------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ +| **Outbound search unrestricted** | ++------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ +| Yes, `tA` | Yes, another team tB | false | `SearchVisibilityStandard` | `SearchableByAllTeams` | Found | Found | ++------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ +| Yes, `tA` | Yes, another team tB | false | `SearchVisibilityStandard` | `SearchableByOwnTeam` | Found | Not found | ++------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ +| **Outbound search restricted** | ++------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ +| Yes, `tA` | Yes, another team tB | true | Irrelevant | Irrelevant | Not found | Not found | ++------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ +| Yes, `tA` | Yes, another team tB | false | `SearchVisibilityNoNameOutsideTeam` | Irrelevant | Found | Not found | ++------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ +| Yes, `tA` | No | false | `SearchVisibilityNoNameOutsideTeam` | There’s no team B | Found | Not found | ++------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ +``` + +### Changing the configuration on the server + +To change the `searchSameTeamOnly` setting on the backend, edit the `values.yaml.gotmpl` file for the wire-server chart at this nested level of the configuration: + +```yaml +brig: + # ... + config: + # ... + optSettings: + # ... + setSearchSameTeamOnly: true +``` + +If `setSearchSameTeamOnly` is set to `true` then `TeamSearchVisibility` is forced be in the `SearchVisibilityNoNameOutsideTeam` setting for all teams. + +### Changing the default configuration for all teams + +If `setSearchSameTeamOnly` is set to `false` (or missing from the configuration) then the default value `TeamSearchVisibility` can be configured at this level of the configuration of the `value.yaml.gotmpl` file of the wire-server chart: + +```yaml +galley: + #... + config: + #... + settings: + #... + featureFlags: + #... + teamSearchVisibility: enabled-by-default +``` + +This default value applies to all teams for which no explicit configuration of the `TeamSearchVisibility` has been set. + +## Searching users on another (federated) backend + +For federated search the table above does not apply, see following table. + +```{note} +Incoming federated searches (i.e. searches from one backend to another) are considered always as being performed from a team user, even if they are performed from a personal user. + +This is because the incoming search request does not carry the information whether the user performing the search was in a team or not. + +So we have to make one assumption, and we assume that they were in a team. +``` + +Allowing search is done at the backend configuration level by the sysadmin: + +- Outbound search restrictions (`searchSameTeamOnly`, `TeamSearchVisibility`) do not apply to federated searches + +- A configuration setting `FederatedUserSearchPolicy` per federating domain with these possible values: + + - `no_search` The federating backend is not allowed to search any users (either by exact handle or full-text). + - `exact_handle_search` The federating backend may only search by exact handle + - `full_search` The federating backend may search users by full text search on display name and handle. The search search results are additionally affected by `SearchVisibilityInbound` setting of each team on the backend. + +- The `SearchVisibilityInbound` setting applies. Since the default value for teams is `SearchableByOwnTeam` this means that for a team to be full-text searchable by users on a federating backend both + + - `FederatedUserSearchPolicy` needs to be set to to full_search for the federating backend + - Any team that wants to be full-text searchable needs to be set to `SearchableByAllTeams` + +The configuration value `FederatedUserSearchPolicy` is per federated domain, e.g. in the values of the wire-server chart: + +```yaml +brig: + config: + optSettings: + setFederationDomainConfigs: + - domain: a.example.com + search_policy: no_search + - domain: a.example.com + search_policy: full_search +``` + +### Table of possible outcomes + +In the following table, user `uA` on backend A is searching for user `uB` on team `tB` on backend B. + +Any of the flags set for searching users on the same backend are ignored. + +It’s worth nothing that if two users are on two separate backend, they are also guaranteed to be on two separate teams, as teams can not spread across backends. + +| Who is searching | Backend B flag `FederatedUserSearchPolicy` | Team `tB`'s flag `SearchVisibilityInbound` | Result of exact search for `uB` | Result of full-text search for `uB` | +| ---------------------- | ------------------------------------------ | ------------------------------------------ | ------------------------------- | ----------------------------------- | +| user `uA` on backend A | `no_search` | Irrelevant | Not found | Not found | +| user `uA` on backend A | `exact_handle_search` | Irrelevant | Found | Not found | +| user `uA` on backend A | `full_search` | SearchableByOwnTeam | Found | Not found | +| user `uA` on backend A | `full_search` | SearchableByAllTeams | Found | Found | + +## Changing the settings for a given team + +If you need to change searchabilility for a specific team (rather than the entire backend, as above), you need to make specific calls to the API. + +### Team searchVisibility + +The team flag `searchVisibility` affects the outbound search of user searches. + +If it is set to `no-name-outside-team` for a team then all users of that team will no longer be able to find users that are not part of their team when searching. + +This also includes finding other users by by providing their exact handle. By default it is set to `standard`, which doesn't put any additional restrictions to outbound searches. + +The setting can be changed via endpoint (for more details on how to make the API calls with `curl`, read further): + +``` +GET /teams/{tid}/search-visibility + -- Shows the current TeamSearchVisibility value for the given team + +PUT /teams/{tid}/search-visibility + -- Set specific search visibility for the team + +pull-down-menu "body": + "standard" + "no-name-outside-team" +``` + +The team feature flag `teamSearchVisibility` determines whether it is allowed to change the `searchVisibility` setting or not. + +The default is `disabled-by-default`. + +```{note} +Whenever this feature setting is disabled the `searchVisibility` will be reset to standard. +``` + +The default setting that applies to all teams on the instance can be defined at configuration + +```yaml +settings: + featureFlags: + teamSearchVisibility: disabled-by-default # or enabled-by-default +``` + +### TeamFeature searchVisibilityInbound + +The team feature flag `searchVisibilityInbound` affects if the team's users are searchable by users from other teams. + +The default setting is `searchable-by-own-team` which hides users from search results by users from other teams. + +If it is set to `searchable-by-all-teams` then users of this team may be included in the results of search queries by other users. + +```{note} +The configuration of this flag does not affect search results when the search query matches the handle exactly. + +If the handle is provdided then any user on the instance can find users. +``` + +This team feature flag can only by toggled by site-administrators with direct access to the galley instance (for more details on how to make the API calls with `curl`, read further): + +``` +PUT /i/teams/{tid}/features/search-visibility-inbound +``` + +With JSON body: + +```json +{"status": "enabled"} +``` + +or + +```json +{"status": "disabled"} +``` + +Where `enabled` is equivalent to `searchable-by-all-teams` and `disabled` is equivalent to `searchable-by-own-team`. + +The default setting that applies to all teams on the instance can be defined at configuration. + +```yaml +searchVisibilityInbound: + defaults: + status: enabled # OR disabled +``` + +Individual teams can overwrite the default setting with API calls as per above. + +### Making the API calls + +To make API calls to set an explicit configuration for\` TeamSearchVisibilityInbound\` per team, you first need to know the Team ID, which can be found in the team settings app. + +It is an `UUID` which has format like this `dcbedf9a-af2a-4f43-9fd5-525953a919e1`. + +In the following we will be using this Team ID as an example, please replace it with your own team id. + +Next find the name of a `galley` pod by looking at the output of running this command: + +```sh +kubectl -n wire get pods +``` + +The output will look something like this: + +``` +... +galley-5f4787fdc7-9l64n ... +galley-migrate-data-lzz5j ... +... +``` + +Select any of the galley pods, for example we will use `galley-5f4787fdc7-9l64n`. + +Next, set up a port-forwarding from your local machine's port `9000` to the galley's port `8080` by running: + +```sh +kubectl port-forward -n wire galley-5f4787fdc7-9l64n 9000:8080 +``` + +Keep this command running until the end of these instuctions. + +Please run the following commands in a seperate terminal while keeping the terminal which establishes the port-forwarding open. + +To see team's current setting run: + +```sh +curl -XGET http://localhost:9000/i/teams/dcbedf9a-af2a-4f43-9fd5-525953a919e1/features/searchVisibilityInbound + +# {"lockStatus":"unlocked","status":"disabled"} +``` + +Where `disabled` corresponds to `SearchableByOwnTeam` and enabled corresponds to `SearchableByAllTeams`. + +To change the `TeamSearchVisibilityInbound` to `SearchableByAllTeams` for the team run: + +```sh +curl -XPUT -H 'Content-Type: application/json' -d "{\"status\": \"enabled\"}" http://localhost:9000/i/teams/dcbedf9a-af2a-4f43-9fd5-525953a919e1/features/searchVisibilityInbound +``` + +To change the TeamSearchVisibilityInbound to SearchableByOwnTeam for the team run: + +```sh +curl -XPUT -H 'Content-Type: application/json' -d "{\"status\": \"disabled\"}" http://localhost:9000/i/teams/dcbedf9a-af2a-4f43-9fd5-525953a919e1/features/searchVisibilityInbound +``` + diff --git a/docs/src/how-to/single-sign-on/adfs/fig-00.jpg b/docs/src/understand/single-sign-on/adfs/fig-00.jpg similarity index 100% rename from docs/src/how-to/single-sign-on/adfs/fig-00.jpg rename to docs/src/understand/single-sign-on/adfs/fig-00.jpg diff --git a/docs/src/how-to/single-sign-on/adfs/fig-01.jpg b/docs/src/understand/single-sign-on/adfs/fig-01.jpg similarity index 100% rename from docs/src/how-to/single-sign-on/adfs/fig-01.jpg rename to docs/src/understand/single-sign-on/adfs/fig-01.jpg diff --git a/docs/src/how-to/single-sign-on/adfs/fig-02.jpg b/docs/src/understand/single-sign-on/adfs/fig-02.jpg similarity index 100% rename from docs/src/how-to/single-sign-on/adfs/fig-02.jpg rename to docs/src/understand/single-sign-on/adfs/fig-02.jpg diff --git a/docs/src/how-to/single-sign-on/adfs/fig-03.jpg b/docs/src/understand/single-sign-on/adfs/fig-03.jpg similarity index 100% rename from docs/src/how-to/single-sign-on/adfs/fig-03.jpg rename to docs/src/understand/single-sign-on/adfs/fig-03.jpg diff --git a/docs/src/how-to/single-sign-on/adfs/fig-04.jpg b/docs/src/understand/single-sign-on/adfs/fig-04.jpg similarity index 100% rename from docs/src/how-to/single-sign-on/adfs/fig-04.jpg rename to docs/src/understand/single-sign-on/adfs/fig-04.jpg diff --git a/docs/src/how-to/single-sign-on/adfs/fig-05.jpg b/docs/src/understand/single-sign-on/adfs/fig-05.jpg similarity index 100% rename from docs/src/how-to/single-sign-on/adfs/fig-05.jpg rename to docs/src/understand/single-sign-on/adfs/fig-05.jpg diff --git a/docs/src/how-to/single-sign-on/adfs/fig-06.jpg b/docs/src/understand/single-sign-on/adfs/fig-06.jpg similarity index 100% rename from docs/src/how-to/single-sign-on/adfs/fig-06.jpg rename to docs/src/understand/single-sign-on/adfs/fig-06.jpg diff --git a/docs/src/how-to/single-sign-on/adfs/fig-07.jpg b/docs/src/understand/single-sign-on/adfs/fig-07.jpg similarity index 100% rename from docs/src/how-to/single-sign-on/adfs/fig-07.jpg rename to docs/src/understand/single-sign-on/adfs/fig-07.jpg diff --git a/docs/src/how-to/single-sign-on/adfs/fig-08.jpg b/docs/src/understand/single-sign-on/adfs/fig-08.jpg similarity index 100% rename from docs/src/how-to/single-sign-on/adfs/fig-08.jpg rename to docs/src/understand/single-sign-on/adfs/fig-08.jpg diff --git a/docs/src/how-to/single-sign-on/adfs/fig-09.jpg b/docs/src/understand/single-sign-on/adfs/fig-09.jpg similarity index 100% rename from docs/src/how-to/single-sign-on/adfs/fig-09.jpg rename to docs/src/understand/single-sign-on/adfs/fig-09.jpg diff --git a/docs/src/how-to/single-sign-on/adfs/fig-10.jpg b/docs/src/understand/single-sign-on/adfs/fig-10.jpg similarity index 100% rename from docs/src/how-to/single-sign-on/adfs/fig-10.jpg rename to docs/src/understand/single-sign-on/adfs/fig-10.jpg diff --git a/docs/src/how-to/single-sign-on/adfs/fig-11.jpg b/docs/src/understand/single-sign-on/adfs/fig-11.jpg similarity index 100% rename from docs/src/how-to/single-sign-on/adfs/fig-11.jpg rename to docs/src/understand/single-sign-on/adfs/fig-11.jpg diff --git a/docs/src/how-to/single-sign-on/adfs/main.md b/docs/src/understand/single-sign-on/adfs/main.md similarity index 100% rename from docs/src/how-to/single-sign-on/adfs/main.md rename to docs/src/understand/single-sign-on/adfs/main.md diff --git a/docs/src/how-to/single-sign-on/azure/01.png b/docs/src/understand/single-sign-on/azure/01.png similarity index 100% rename from docs/src/how-to/single-sign-on/azure/01.png rename to docs/src/understand/single-sign-on/azure/01.png diff --git a/docs/src/how-to/single-sign-on/azure/02.png b/docs/src/understand/single-sign-on/azure/02.png similarity index 100% rename from docs/src/how-to/single-sign-on/azure/02.png rename to docs/src/understand/single-sign-on/azure/02.png diff --git a/docs/src/how-to/single-sign-on/azure/03.png b/docs/src/understand/single-sign-on/azure/03.png similarity index 100% rename from docs/src/how-to/single-sign-on/azure/03.png rename to docs/src/understand/single-sign-on/azure/03.png diff --git a/docs/src/how-to/single-sign-on/azure/04.png b/docs/src/understand/single-sign-on/azure/04.png similarity index 100% rename from docs/src/how-to/single-sign-on/azure/04.png rename to docs/src/understand/single-sign-on/azure/04.png diff --git a/docs/src/how-to/single-sign-on/azure/05.png b/docs/src/understand/single-sign-on/azure/05.png similarity index 100% rename from docs/src/how-to/single-sign-on/azure/05.png rename to docs/src/understand/single-sign-on/azure/05.png diff --git a/docs/src/how-to/single-sign-on/azure/06.png b/docs/src/understand/single-sign-on/azure/06.png similarity index 100% rename from docs/src/how-to/single-sign-on/azure/06.png rename to docs/src/understand/single-sign-on/azure/06.png diff --git a/docs/src/how-to/single-sign-on/azure/07.png b/docs/src/understand/single-sign-on/azure/07.png similarity index 100% rename from docs/src/how-to/single-sign-on/azure/07.png rename to docs/src/understand/single-sign-on/azure/07.png diff --git a/docs/src/how-to/single-sign-on/azure/08.png b/docs/src/understand/single-sign-on/azure/08.png similarity index 100% rename from docs/src/how-to/single-sign-on/azure/08.png rename to docs/src/understand/single-sign-on/azure/08.png diff --git a/docs/src/how-to/single-sign-on/azure/09.png b/docs/src/understand/single-sign-on/azure/09.png similarity index 100% rename from docs/src/how-to/single-sign-on/azure/09.png rename to docs/src/understand/single-sign-on/azure/09.png diff --git a/docs/src/how-to/single-sign-on/azure/10.png b/docs/src/understand/single-sign-on/azure/10.png similarity index 100% rename from docs/src/how-to/single-sign-on/azure/10.png rename to docs/src/understand/single-sign-on/azure/10.png diff --git a/docs/src/how-to/single-sign-on/azure/11.png b/docs/src/understand/single-sign-on/azure/11.png similarity index 100% rename from docs/src/how-to/single-sign-on/azure/11.png rename to docs/src/understand/single-sign-on/azure/11.png diff --git a/docs/src/how-to/single-sign-on/azure/12.png b/docs/src/understand/single-sign-on/azure/12.png similarity index 100% rename from docs/src/how-to/single-sign-on/azure/12.png rename to docs/src/understand/single-sign-on/azure/12.png diff --git a/docs/src/how-to/single-sign-on/azure/13.png b/docs/src/understand/single-sign-on/azure/13.png similarity index 100% rename from docs/src/how-to/single-sign-on/azure/13.png rename to docs/src/understand/single-sign-on/azure/13.png diff --git a/docs/src/how-to/single-sign-on/azure/14.png b/docs/src/understand/single-sign-on/azure/14.png similarity index 100% rename from docs/src/how-to/single-sign-on/azure/14.png rename to docs/src/understand/single-sign-on/azure/14.png diff --git a/docs/src/how-to/single-sign-on/azure/15.png b/docs/src/understand/single-sign-on/azure/15.png similarity index 100% rename from docs/src/how-to/single-sign-on/azure/15.png rename to docs/src/understand/single-sign-on/azure/15.png diff --git a/docs/src/how-to/single-sign-on/azure/main.md b/docs/src/understand/single-sign-on/azure/main.md similarity index 100% rename from docs/src/how-to/single-sign-on/azure/main.md rename to docs/src/understand/single-sign-on/azure/main.md diff --git a/docs/src/how-to/single-sign-on/centrify/001.png b/docs/src/understand/single-sign-on/centrify/001.png similarity index 100% rename from docs/src/how-to/single-sign-on/centrify/001.png rename to docs/src/understand/single-sign-on/centrify/001.png diff --git a/docs/src/how-to/single-sign-on/centrify/002.png b/docs/src/understand/single-sign-on/centrify/002.png similarity index 100% rename from docs/src/how-to/single-sign-on/centrify/002.png rename to docs/src/understand/single-sign-on/centrify/002.png diff --git a/docs/src/how-to/single-sign-on/centrify/003.png b/docs/src/understand/single-sign-on/centrify/003.png similarity index 100% rename from docs/src/how-to/single-sign-on/centrify/003.png rename to docs/src/understand/single-sign-on/centrify/003.png diff --git a/docs/src/how-to/single-sign-on/centrify/004.png b/docs/src/understand/single-sign-on/centrify/004.png similarity index 100% rename from docs/src/how-to/single-sign-on/centrify/004.png rename to docs/src/understand/single-sign-on/centrify/004.png diff --git a/docs/src/how-to/single-sign-on/centrify/005.png b/docs/src/understand/single-sign-on/centrify/005.png similarity index 100% rename from docs/src/how-to/single-sign-on/centrify/005.png rename to docs/src/understand/single-sign-on/centrify/005.png diff --git a/docs/src/how-to/single-sign-on/centrify/006.png b/docs/src/understand/single-sign-on/centrify/006.png similarity index 100% rename from docs/src/how-to/single-sign-on/centrify/006.png rename to docs/src/understand/single-sign-on/centrify/006.png diff --git a/docs/src/how-to/single-sign-on/centrify/007.png b/docs/src/understand/single-sign-on/centrify/007.png similarity index 100% rename from docs/src/how-to/single-sign-on/centrify/007.png rename to docs/src/understand/single-sign-on/centrify/007.png diff --git a/docs/src/how-to/single-sign-on/centrify/008.png b/docs/src/understand/single-sign-on/centrify/008.png similarity index 100% rename from docs/src/how-to/single-sign-on/centrify/008.png rename to docs/src/understand/single-sign-on/centrify/008.png diff --git a/docs/src/how-to/single-sign-on/centrify/009.png b/docs/src/understand/single-sign-on/centrify/009.png similarity index 100% rename from docs/src/how-to/single-sign-on/centrify/009.png rename to docs/src/understand/single-sign-on/centrify/009.png diff --git a/docs/src/how-to/single-sign-on/centrify/010.png b/docs/src/understand/single-sign-on/centrify/010.png similarity index 100% rename from docs/src/how-to/single-sign-on/centrify/010.png rename to docs/src/understand/single-sign-on/centrify/010.png diff --git a/docs/src/how-to/single-sign-on/centrify/011.png b/docs/src/understand/single-sign-on/centrify/011.png similarity index 100% rename from docs/src/how-to/single-sign-on/centrify/011.png rename to docs/src/understand/single-sign-on/centrify/011.png diff --git a/docs/src/how-to/single-sign-on/centrify/main.md b/docs/src/understand/single-sign-on/centrify/main.md similarity index 100% rename from docs/src/how-to/single-sign-on/centrify/main.md rename to docs/src/understand/single-sign-on/centrify/main.md diff --git a/docs/src/how-to/single-sign-on/generic-setup.md b/docs/src/understand/single-sign-on/generic-setup.md similarity index 100% rename from docs/src/how-to/single-sign-on/generic-setup.md rename to docs/src/understand/single-sign-on/generic-setup.md diff --git a/docs/src/how-to/single-sign-on/index.md b/docs/src/understand/single-sign-on/index.md similarity index 92% rename from docs/src/how-to/single-sign-on/index.md rename to docs/src/understand/single-sign-on/index.md index 2cdb939676..01317c99b5 100644 --- a/docs/src/how-to/single-sign-on/index.md +++ b/docs/src/understand/single-sign-on/index.md @@ -1,3 +1,5 @@ +(sso-main-documentation)= + # Single Sign-On and User Provisioning ```{toctree} diff --git a/docs/src/how-to/single-sign-on/okta/001-applications-screen.png b/docs/src/understand/single-sign-on/okta/001-applications-screen.png similarity index 100% rename from docs/src/how-to/single-sign-on/okta/001-applications-screen.png rename to docs/src/understand/single-sign-on/okta/001-applications-screen.png diff --git a/docs/src/how-to/single-sign-on/okta/002-add-application.png b/docs/src/understand/single-sign-on/okta/002-add-application.png similarity index 100% rename from docs/src/how-to/single-sign-on/okta/002-add-application.png rename to docs/src/understand/single-sign-on/okta/002-add-application.png diff --git a/docs/src/how-to/single-sign-on/okta/003-add-application-1.png b/docs/src/understand/single-sign-on/okta/003-add-application-1.png similarity index 100% rename from docs/src/how-to/single-sign-on/okta/003-add-application-1.png rename to docs/src/understand/single-sign-on/okta/003-add-application-1.png diff --git a/docs/src/how-to/single-sign-on/okta/004-add-application-step1.png b/docs/src/understand/single-sign-on/okta/004-add-application-step1.png similarity index 100% rename from docs/src/how-to/single-sign-on/okta/004-add-application-step1.png rename to docs/src/understand/single-sign-on/okta/004-add-application-step1.png diff --git a/docs/src/how-to/single-sign-on/okta/005-add-application-step2.png b/docs/src/understand/single-sign-on/okta/005-add-application-step2.png similarity index 100% rename from docs/src/how-to/single-sign-on/okta/005-add-application-step2.png rename to docs/src/understand/single-sign-on/okta/005-add-application-step2.png diff --git a/docs/src/how-to/single-sign-on/okta/006-add-application-step3.png b/docs/src/understand/single-sign-on/okta/006-add-application-step3.png similarity index 100% rename from docs/src/how-to/single-sign-on/okta/006-add-application-step3.png rename to docs/src/understand/single-sign-on/okta/006-add-application-step3.png diff --git a/docs/src/how-to/single-sign-on/okta/007-application-sign-on.png b/docs/src/understand/single-sign-on/okta/007-application-sign-on.png similarity index 100% rename from docs/src/how-to/single-sign-on/okta/007-application-sign-on.png rename to docs/src/understand/single-sign-on/okta/007-application-sign-on.png diff --git a/docs/src/how-to/single-sign-on/okta/008-assignment.png b/docs/src/understand/single-sign-on/okta/008-assignment.png similarity index 100% rename from docs/src/how-to/single-sign-on/okta/008-assignment.png rename to docs/src/understand/single-sign-on/okta/008-assignment.png diff --git a/docs/src/how-to/single-sign-on/okta/main.md b/docs/src/understand/single-sign-on/okta/main.md similarity index 100% rename from docs/src/how-to/single-sign-on/okta/main.md rename to docs/src/understand/single-sign-on/okta/main.md diff --git a/docs/src/how-to/single-sign-on/trouble-shooting.md b/docs/src/understand/single-sign-on/trouble-shooting.md similarity index 100% rename from docs/src/how-to/single-sign-on/trouble-shooting.md rename to docs/src/understand/single-sign-on/trouble-shooting.md diff --git a/docs/src/how-to/single-sign-on/understand/Wire_SAML_Flow (lucidchart).svg b/docs/src/understand/single-sign-on/understand/Wire_SAML_Flow (lucidchart).svg similarity index 100% rename from docs/src/how-to/single-sign-on/understand/Wire_SAML_Flow (lucidchart).svg rename to docs/src/understand/single-sign-on/understand/Wire_SAML_Flow (lucidchart).svg diff --git a/docs/src/how-to/single-sign-on/understand/Wire_SAML_Flow.png b/docs/src/understand/single-sign-on/understand/Wire_SAML_Flow.png similarity index 100% rename from docs/src/how-to/single-sign-on/understand/Wire_SAML_Flow.png rename to docs/src/understand/single-sign-on/understand/Wire_SAML_Flow.png diff --git a/docs/src/how-to/single-sign-on/understand/main.md b/docs/src/understand/single-sign-on/understand/main.md similarity index 100% rename from docs/src/how-to/single-sign-on/understand/main.md rename to docs/src/understand/single-sign-on/understand/main.md diff --git a/docs/src/how-to/single-sign-on/understand/token-step-01.png b/docs/src/understand/single-sign-on/understand/token-step-01.png similarity index 100% rename from docs/src/how-to/single-sign-on/understand/token-step-01.png rename to docs/src/understand/single-sign-on/understand/token-step-01.png diff --git a/docs/src/how-to/single-sign-on/understand/token-step-02.png b/docs/src/understand/single-sign-on/understand/token-step-02.png similarity index 100% rename from docs/src/how-to/single-sign-on/understand/token-step-02.png rename to docs/src/understand/single-sign-on/understand/token-step-02.png diff --git a/docs/src/how-to/single-sign-on/understand/token-step-03.png b/docs/src/understand/single-sign-on/understand/token-step-03.png similarity index 100% rename from docs/src/how-to/single-sign-on/understand/token-step-03.png rename to docs/src/understand/single-sign-on/understand/token-step-03.png diff --git a/docs/src/how-to/single-sign-on/understand/token-step-04.png b/docs/src/understand/single-sign-on/understand/token-step-04.png similarity index 100% rename from docs/src/how-to/single-sign-on/understand/token-step-04.png rename to docs/src/understand/single-sign-on/understand/token-step-04.png diff --git a/docs/src/how-to/single-sign-on/understand/token-step-05.png b/docs/src/understand/single-sign-on/understand/token-step-05.png similarity index 100% rename from docs/src/how-to/single-sign-on/understand/token-step-05.png rename to docs/src/understand/single-sign-on/understand/token-step-05.png diff --git a/docs/src/how-to/single-sign-on/understand/token-step-06.png b/docs/src/understand/single-sign-on/understand/token-step-06.png similarity index 100% rename from docs/src/how-to/single-sign-on/understand/token-step-06.png rename to docs/src/understand/single-sign-on/understand/token-step-06.png diff --git a/docs/src/how-to/install/team-feature-settings.md b/docs/src/understand/team-feature-settings.md similarity index 100% rename from docs/src/how-to/install/team-feature-settings.md rename to docs/src/understand/team-feature-settings.md diff --git a/nix/default.nix b/nix/default.nix index bd35e4aaf2..c377bf7102 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -49,7 +49,9 @@ let nativeBuildInputs = docsPkgs ++ [ pkgs.gnumake ]; } '' - cp -r ${pkgs.nix-gitignore.gitignoreSource [] ../docs}/* . + cp -rH ${pkgs.nix-gitignore.gitignoreSource [] ../docs}/* . + chmod -R +w ./src + cp ${../CHANGELOG.md} ./src/changelog/changelog.md make docs-all mkdir $out cp -r build/* $out/ From 0bd7425bae382a630120af2660bfc2fa395a6fa3 Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Wed, 25 Jan 2023 11:26:22 +0100 Subject: [PATCH 29/38] fix Makefile kind-restart-all target (#3015) --- Makefile | 2 +- changelog.d/5-internal/makefile-kind-restart-all | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/5-internal/makefile-kind-restart-all diff --git a/Makefile b/Makefile index 7aa65a0e86..28ba398972 100644 --- a/Makefile +++ b/Makefile @@ -488,7 +488,7 @@ kind-integration-e2e: .local/kind-kubeconfig kind-restart-all: .local/kind-kubeconfig export KUBECONFIG=$(CURDIR)/.local/kind-kubeconfig && \ kubectl delete pod -n $(NAMESPACE) -l release=$(NAMESPACE)-wire-server && \ - kubectl delete pod -n $(NAMESPACE)-fed2 -l release=$(NAMESPACE)-fed2-wire-server + kubectl delete pod -n $(NAMESPACE)-fed2 -l release=$(NAMESPACE)-wire-server-2 kind-restart-nginx-ingress: .local/kind-kubeconfig export KUBECONFIG=$(CURDIR)/.local/kind-kubeconfig && \ diff --git a/changelog.d/5-internal/makefile-kind-restart-all b/changelog.d/5-internal/makefile-kind-restart-all new file mode 100644 index 0000000000..899efa0d8e --- /dev/null +++ b/changelog.d/5-internal/makefile-kind-restart-all @@ -0,0 +1 @@ +Fix Makefile target kind-restart-all. From 376f4697127b20c2d95d56872eaeca4fd5b7950d Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 25 Jan 2023 14:42:24 +0100 Subject: [PATCH 30/38] Removed unused old swagger code (#3017) --- changelog.d/5-internal/pr-3017 | 1 + libs/wire-api/src/Wire/API/Team/Feature.hs | 119 --------------------- tools/stern/src/Stern/Swagger.hs | 96 ----------------- tools/stern/stern.cabal | 1 - 4 files changed, 1 insertion(+), 216 deletions(-) create mode 100644 changelog.d/5-internal/pr-3017 delete mode 100644 tools/stern/src/Stern/Swagger.hs diff --git a/changelog.d/5-internal/pr-3017 b/changelog.d/5-internal/pr-3017 new file mode 100644 index 0000000000..7d9245d71d --- /dev/null +++ b/changelog.d/5-internal/pr-3017 @@ -0,0 +1 @@ +Unused old swagger code removed from stern and team features diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index a7d74ad034..e9bbd67aab 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -77,10 +77,6 @@ module Wire.API.Team.Feature MLSConfig (..), AllFeatureConfigs (..), typeFeatureTTL, - withStatusModel, - withStatusNoLockModel, - allFeatureModels, - typeFeatureStatus, unImplicitLockStatus, ImplicitLockStatus (..), ) @@ -159,10 +155,6 @@ class IsFeatureConfig cfg where type FeatureSymbol cfg :: Symbol defFeatureStatus :: WithStatus cfg - -- | Swagger 1.2 model for stern and wai routes - configModel :: Maybe Doc.Model - configModel = Nothing - objectSchema :: -- | Should be "pure MyFeatureConfig" if the feature doesn't have config, -- which results in a trivial empty schema and the "config" field being @@ -250,20 +242,6 @@ instance (ToSchema cfg, IsFeatureConfig cfg) => ToSchema (WithStatus cfg) where instance (Arbitrary cfg, IsFeatureConfig cfg) => Arbitrary (WithStatus cfg) where arbitrary = WithStatusBase <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary -withStatusModel :: forall cfg. (IsFeatureConfig cfg, KnownSymbol (FeatureSymbol cfg)) => Doc.Model -withStatusModel = - let name = featureName @cfg - mbModelCfg = configModel @cfg - in Doc.defineModel ("WithStatus." <> name) $ do - case mbModelCfg of - Nothing -> Doc.description $ "Team feature " <> name <> " that has no configuration beyond the boolean on/off switch." - Just modelCfg -> do - Doc.description $ "Status and config of " <> name - Doc.property "config" (Doc.ref modelCfg) $ Doc.description "config" - - Doc.property "status" typeFeatureStatus $ Doc.description "status" - Doc.property "lockStatus" typeLockStatusValue $ Doc.description "" - ---------------------------------------------------------------------- -- WithStatusPatch @@ -359,19 +337,6 @@ instance (ToSchema cfg, IsFeatureConfig cfg) => ToSchema (WithStatusNoLock cfg) inner = schema @cfg name = fromMaybe "" (getName (schemaDoc inner)) <> ".WithStatusNoLock" -withStatusNoLockModel :: forall cfg. (IsFeatureConfig cfg, KnownSymbol (FeatureSymbol cfg)) => Doc.Model -withStatusNoLockModel = - let name = featureName @cfg - mbModelCfg = configModel @cfg - in Doc.defineModel ("WithStatusNoLock." <> name) $ do - case mbModelCfg of - Nothing -> Doc.description $ "Team feature " <> name <> " that has no configuration beyond the boolean on/off switch." - Just modelCfg -> do - Doc.description $ "Status and config of " <> name - Doc.property "config" (Doc.ref modelCfg) $ Doc.description "config" - - Doc.property "status" typeFeatureStatus $ Doc.description "status" - ---------------------------------------------------------------------- -- FeatureTTL @@ -491,14 +456,6 @@ data LockStatus = LockStatusLocked | LockStatusUnlocked instance FromHttpApiData LockStatus where parseUrlPiece = maybeToEither "Invalid lock status" . fromByteString . cs -typeLockStatusValue :: Doc.DataType -typeLockStatusValue = - Doc.string $ - Doc.enum - [ "locked", - "unlocked" - ] - instance ToSchema LockStatus where schema = enum @Text "LockStatus" $ @@ -565,57 +522,6 @@ computeFeatureConfigForTeamUser mStatusDb mLockStatusDb defStatus = where lockStatus = fromMaybe (wsLockStatus defStatus) mLockStatusDb -allFeatureModels :: [Doc.Model] -allFeatureModels = - [ withStatusNoLockModel @LegalholdConfig, - withStatusNoLockModel @SSOConfig, - withStatusNoLockModel @SearchVisibilityAvailableConfig, - withStatusNoLockModel @ValidateSAMLEmailsConfig, - withStatusNoLockModel @DigitalSignaturesConfig, - withStatusNoLockModel @AppLockConfig, - withStatusNoLockModel @FileSharingConfig, - withStatusNoLockModel @ClassifiedDomainsConfig, - withStatusNoLockModel @ConferenceCallingConfig, - withStatusNoLockModel @SelfDeletingMessagesConfig, - withStatusNoLockModel @GuestLinksConfig, - withStatusNoLockModel @SndFactorPasswordChallengeConfig, - withStatusNoLockModel @SearchVisibilityInboundConfig, - withStatusNoLockModel @MLSConfig, - withStatusNoLockModel @ExposeInvitationURLsToTeamAdminConfig, - withStatusModel @LegalholdConfig, - withStatusModel @SSOConfig, - withStatusModel @SearchVisibilityAvailableConfig, - withStatusModel @ValidateSAMLEmailsConfig, - withStatusModel @DigitalSignaturesConfig, - withStatusModel @AppLockConfig, - withStatusModel @FileSharingConfig, - withStatusModel @ClassifiedDomainsConfig, - withStatusModel @ConferenceCallingConfig, - withStatusModel @SelfDeletingMessagesConfig, - withStatusModel @GuestLinksConfig, - withStatusModel @SndFactorPasswordChallengeConfig, - withStatusModel @SearchVisibilityInboundConfig, - withStatusModel @MLSConfig, - withStatusModel @ExposeInvitationURLsToTeamAdminConfig - ] - <> catMaybes - [ configModel @LegalholdConfig, - configModel @SSOConfig, - configModel @SearchVisibilityAvailableConfig, - configModel @ValidateSAMLEmailsConfig, - configModel @DigitalSignaturesConfig, - configModel @AppLockConfig, - configModel @FileSharingConfig, - configModel @ClassifiedDomainsConfig, - configModel @ConferenceCallingConfig, - configModel @SelfDeletingMessagesConfig, - configModel @GuestLinksConfig, - configModel @SndFactorPasswordChallengeConfig, - configModel @SearchVisibilityInboundConfig, - configModel @MLSConfig, - configModel @ExposeInvitationURLsToTeamAdminConfig - ] - -------------------------------------------------------------------------------- -- GuestLinks feature @@ -816,9 +722,6 @@ instance IsFeatureConfig ClassifiedDomainsConfig where LockStatusUnlocked (ClassifiedDomainsConfig []) FeatureTTLUnlimited - configModel = Just $ - Doc.defineModel "ClassifiedDomainsConfig" $ do - Doc.property "domains" (Doc.array Doc.string') $ Doc.description "domains" objectSchema = field "config" schema ---------------------------------------------------------------------- @@ -848,10 +751,6 @@ instance IsFeatureConfig AppLockConfig where LockStatusUnlocked (AppLockConfig (EnforceAppLock False) 60) FeatureTTLUnlimited - configModel = Just $ - Doc.defineModel "AppLockConfig" $ do - Doc.property "enforceAppLock" Doc.bool' $ Doc.description "enforceAppLock" - Doc.property "inactivityTimeoutSecs" Doc.int32' $ Doc.description "" objectSchema = field "config" schema newtype EnforceAppLock = EnforceAppLock Bool @@ -904,9 +803,6 @@ instance IsFeatureConfig SelfDeletingMessagesConfig where LockStatusUnlocked (SelfDeletingMessagesConfig 0) FeatureTTLUnlimited - configModel = Just $ - Doc.defineModel "SelfDeletingMessagesConfig" $ do - Doc.property "enforcedTimeoutSeconds" Doc.int32' $ Doc.description "optional; default: `0` (no enforcement)" objectSchema = field "config" schema ---------------------------------------------------------------------- @@ -937,13 +833,6 @@ instance IsFeatureConfig MLSConfig where in withStatus FeatureStatusDisabled LockStatusUnlocked config FeatureTTLUnlimited objectSchema = field "config" schema - configModel = Just $ - Doc.defineModel "MLSConfig" $ do - Doc.property "protocolToggleUsers" (Doc.array Doc.string') $ Doc.description "allowlist of users that may change protocols" - Doc.property "defaultProtocol" Doc.string' $ Doc.description "default protocol, either \"proteus\" or \"mls\"" - Doc.property "allowedCipherSuites" (Doc.array Doc.int32') $ Doc.description "cipher suite numbers, See https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol.html#table-5" - Doc.property "defaultCipherSuite" Doc.int32' $ Doc.description "cipher suite number. See https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol.html#table-5" - ---------------------------------------------------------------------- -- ExposeInvitationURLsToTeamAdminConfig @@ -985,14 +874,6 @@ instance FromHttpApiData FeatureStatus where instance ToHttpApiData FeatureStatus where toUrlPiece = cs . toByteString' -typeFeatureStatus :: Doc.DataType -typeFeatureStatus = - Doc.string $ - Doc.enum - [ "enabled", - "disabled" - ] - instance ToSchema FeatureStatus where schema = enum @Text "FeatureStatus" $ diff --git a/tools/stern/src/Stern/Swagger.hs b/tools/stern/src/Stern/Swagger.hs deleted file mode 100644 index 97a047c961..0000000000 --- a/tools/stern/src/Stern/Swagger.hs +++ /dev/null @@ -1,96 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Stern.Swagger where - -import Data.String.Conversions -import Data.Swagger.Build.Api -import Imports -import qualified Wire.API.Team.Feature as Feature -import Wire.API.Team.SearchVisibility (modelTeamSearchVisibility) - -sternModels :: [Model] -sternModels = - [ emailUpdate, - phoneUpdate, - modelTeamSearchVisibility, - teamBillingInfo, - teamBillingInfoUpdate - ] - <> Feature.allFeatureModels - -emailUpdate :: Model -emailUpdate = defineModel "EmailUpdate" $ do - description "Email Update Data" - property "email" string' $ - description "Email" - -phoneUpdate :: Model -phoneUpdate = defineModel "PhoneUpdate" $ do - description "Phone Update Data" - property "phone" string' $ - description "E.164 phone number" - -teamBillingInfo :: Model -teamBillingInfo = defineModel "teamBillingInfo" $ do - property "firstname" string' $ - description "First name of the team owner" - property "lastname" string' $ - description "Last name of the team owner" - property "street" string' $ - description "Street of the company address" - property "zip" string' $ - description "ZIP code of the company address" - property "city" string' $ - description "City of the company address" - property "country" string' $ - description "Country of the company address" - property "company" string' $ do - description "Name of the company" - optional - property "state" string' $ do - description "State of the company address" - optional - -teamBillingInfoUpdate :: Model -teamBillingInfoUpdate = defineModel "teamBillingInfoUpdate" $ do - property "firstname" string' $ do - description "First name of the team owner (1 - 256 characters)" - optional - property "lastname" string' $ do - description "Last name of the team owner (1 - 256 characters)" - optional - property "street" string' $ do - description "Street of the company address (1 - 256 characters)" - optional - property "zip" string' $ do - description "ZIP code of the company address (1 - 16 characters)" - optional - property "city" string' $ do - description "City of the company address (1 - 256 characters)" - optional - property "country" string' $ do - description "Country of the company address (1 - 256 characters)" - optional - property "company" string' $ do - description "Name of the company (1 - 256 characters)" - optional - property "state" string' $ do - description "State of the company address (1 - 256 characters)" - optional diff --git a/tools/stern/stern.cabal b/tools/stern/stern.cabal index f7831bfa65..7101c73a9e 100644 --- a/tools/stern/stern.cabal +++ b/tools/stern/stern.cabal @@ -23,7 +23,6 @@ library Stern.App Stern.Intra Stern.Options - Stern.Swagger Stern.Types other-modules: Paths_stern From 246470f7684b5c6a93be4feb0a9e82769b39a605 Mon Sep 17 00:00:00 2001 From: zebot Date: Wed, 25 Jan 2023 15:08:54 +0100 Subject: [PATCH 31/38] chore: [charts] Update team-settings version (#3016) Co-authored-by: Zebot --- changelog.d/0-release-notes/team-settings-upgrade | 1 + charts/team-settings/values.yaml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/0-release-notes/team-settings-upgrade diff --git a/changelog.d/0-release-notes/team-settings-upgrade b/changelog.d/0-release-notes/team-settings-upgrade new file mode 100644 index 0000000000..1168c3ea44 --- /dev/null +++ b/changelog.d/0-release-notes/team-settings-upgrade @@ -0,0 +1 @@ +Upgrade team-settings version to 4.14.0-v0.31.9-0-bf82b46 diff --git a/charts/team-settings/values.yaml b/charts/team-settings/values.yaml index 200a9e9683..60ccd89059 100644 --- a/charts/team-settings/values.yaml +++ b/charts/team-settings/values.yaml @@ -9,7 +9,7 @@ resources: cpu: "1" image: repository: quay.io/wire/team-settings - tag: "4.13.0-v0.31.5-0-4754212" + tag: "4.14.0-v0.31.9-0-bf82b46" service: https: externalPort: 443 From f6b2f932d85c0b6b3701160daf3deb483c9627f8 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Wed, 25 Jan 2023 15:28:36 +0100 Subject: [PATCH 32/38] Mock combinators (#3014) * Add mock creation utilities * Remove Servant-based mocks * Regenerate nix packages Co-authored-by: Igor Ranieri Elland <54423+elland@users.noreply.github.com> * Fold guardComponent * Move MLS mocks to a separate module Co-authored-by: Igor Ranieri Elland <54423+elland@users.noreply.github.com> --- changelog.d/5-internal/mock-utilities | 1 + services/federator/default.nix | 1 + services/federator/federator.cabal | 1 + .../federator/src/Federator/MockServer.hs | 86 ++++- services/galley/galley.cabal | 1 + services/galley/test/integration/API.hs | 328 ++++++++---------- .../galley/test/integration/API/Federation.hs | 59 ++-- services/galley/test/integration/API/MLS.hs | 165 +-------- .../galley/test/integration/API/MLS/Mocks.hs | 71 ++++ .../test/integration/API/MessageTimer.hs | 4 +- services/galley/test/integration/API/Roles.hs | 8 +- services/galley/test/integration/API/Util.hs | 76 +--- 12 files changed, 375 insertions(+), 426 deletions(-) create mode 100644 changelog.d/5-internal/mock-utilities create mode 100644 services/galley/test/integration/API/MLS/Mocks.hs diff --git a/changelog.d/5-internal/mock-utilities b/changelog.d/5-internal/mock-utilities new file mode 100644 index 0000000000..1c08e75164 --- /dev/null +++ b/changelog.d/5-internal/mock-utilities @@ -0,0 +1 @@ +Add combinators for creating mocked federator responses in integration tests diff --git a/services/federator/default.nix b/services/federator/default.nix index 2704a157dc..f274290059 100644 --- a/services/federator/default.nix +++ b/services/federator/default.nix @@ -129,6 +129,7 @@ mkDerivation { time-manager tinylog tls + transformers types-common unix uri-bytestring diff --git a/services/federator/federator.cabal b/services/federator/federator.cabal index d832332a96..547d3d7536 100644 --- a/services/federator/federator.cabal +++ b/services/federator/federator.cabal @@ -140,6 +140,7 @@ library , time-manager , tinylog , tls + , transformers , types-common , unix , uri-bytestring diff --git a/services/federator/src/Federator/MockServer.hs b/services/federator/src/Federator/MockServer.hs index 38bf0fe8ca..f8f88e5310 100644 --- a/services/federator/src/Federator/MockServer.hs +++ b/services/federator/src/Federator/MockServer.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE RecordWildCards #-} -- This file is part of the Wire Server implementation. @@ -18,17 +19,33 @@ -- with this program. If not, see . module Federator.MockServer - ( MockException (..), + ( -- * Federator mock server + MockException (..), withTempMockFederator, FederatedRequest (..), + + -- * Mock utilities + Mock, + runMock, + mockReply, + mockFail, + guardRPC, + guardComponent, + (~>), + getRequest, + getRequestRPC, + getRequestBody, ) where import qualified Control.Exception as Exception import Control.Exception.Base (throw) import Control.Monad.Catch hiding (fromException) +import Control.Monad.Trans.Except +import Control.Monad.Trans.Maybe import qualified Data.Aeson as Aeson import Data.Domain (Domain) +import qualified Data.Text as Text import qualified Data.Text.Lazy as LText import Federator.Error import Federator.Error.ServerError @@ -129,3 +146,70 @@ withTempMockFederator headers resp action = do (\(_close, port) -> action port) calls <- readIORef remoteCalls pure (result, calls) + +-------------------------------------------------------------------------------- +-- Mock creation utilities + +-- | This is a monad that can be used to create mocked responses. It is a very +-- minimalistic web framework. One can expect a certain request (using +-- 'guardRPC') and reply accordingly (using 'mockReply'). Multiple possible +-- requests and responses can be combined using the 'Alternative' instance. In +-- simple cases, one can also use the infix '(~>)' combinator, which concisely +-- binds a request to a hardcoded pure response. +newtype Mock a = Mock {unMock :: ReaderT FederatedRequest (MaybeT (ExceptT Text IO)) a} + deriving newtype (Functor, Applicative, Alternative, Monad, MonadIO) + +-- | Convert a mocked response to a function which can be used as input to +-- 'tempMockFederator'. +runMock :: (Text -> IO a) -> Mock a -> FederatedRequest -> IO a +runMock err m req = + runExceptT (runMaybeT (runReaderT (unMock m) req)) >>= \case + Right Nothing -> err ("unmocked endpoint called: " <> frRPC req) + Right (Just x) -> pure x + Left e -> err e + +-- | Retrieve the current request. +getRequest :: Mock FederatedRequest +getRequest = Mock $ ReaderT pure + +-- | Retrieve the RPC of the current request. +getRequestRPC :: Mock Text +getRequestRPC = frRPC <$> getRequest + +-- | Retrieve and deserialise the body of the current request. +getRequestBody :: Aeson.FromJSON a => Mock a +getRequestBody = do + b <- frBody <$> getRequest + case Aeson.eitherDecode b of + Left e -> do + rpc <- getRequestRPC + mockFail ("Parse failure in " <> rpc <> ": " <> Text.pack e) + Right x -> pure x + +-- | Expect a given RPC. If the current request does not match, the whole +-- action fails. This can be used in combination with the 'Alternative' +-- instance to provide responses for multiple requests. +guardRPC :: Text -> Mock () +guardRPC rpc = do + rpc' <- getRequestRPC + guard (rpc' == rpc) + +guardComponent :: Component -> Mock () +guardComponent c = do + c' <- frComponent <$> getRequest + guard (c == c') + +-- | Serialise and return a response. +mockReply :: Aeson.ToJSON a => a -> Mock LByteString +mockReply = pure . Aeson.encode + +-- | Abort the mock with an error. +mockFail :: Text -> Mock a +mockFail = Mock . lift . lift . throwE + +infixl 5 ~> + +-- | Expect a given RPC and simply return a pure response when the current +-- request matches. +(~>) :: Aeson.ToJSON a => Text -> a -> Mock LByteString +(~>) rpc x = guardRPC rpc *> mockReply x diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 955c3ac159..c7c42b7a87 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -375,6 +375,7 @@ executable galley-integration API.Federation.Util API.MessageTimer API.MLS + API.MLS.Mocks API.MLS.Util API.Roles API.SQS diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 7c001655ca..ce291f1ba4 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -24,7 +24,6 @@ where import qualified API.CustomBackend as CustomBackend import qualified API.Federation as Federation -import API.Federation.Util import qualified API.MLS import qualified API.MessageTimer as MessageTimer import qualified API.Roles as Roles @@ -64,15 +63,14 @@ import qualified Data.Text as T import qualified Data.Text.Ascii as Ascii import Data.Time.Clock (getCurrentTime) import Federator.Discovery (DiscoveryFailure (..)) -import Federator.MockServer (FederatedRequest (..), MockException (..)) +import Federator.MockServer import Galley.API.Mapping import Galley.Options (optFederator) import Galley.Types.Conversations.Intra import Galley.Types.Conversations.Members import Imports -import qualified Network.HTTP.Types as HTTP +import qualified Network.HTTP.Types.Status as HTTP import Network.Wai.Utilities.Error -import Servant hiding (respond) import Test.QuickCheck (arbitrary, generate) import Test.Tasty import Test.Tasty.Cannon (TimeoutUnit (..), (#)) @@ -97,7 +95,6 @@ import Wire.API.Internal.Notification import Wire.API.Message import qualified Wire.API.Message as Message import Wire.API.Routes.MultiTablePaging -import Wire.API.Routes.Named import Wire.API.Routes.Version import Wire.API.Routes.Versioned import qualified Wire.API.Team.Feature as Public @@ -340,7 +337,7 @@ postConvWithRemoteUsersOk = do let nameMaxSize = T.replicate 256 "a" WS.bracketR3 c alice alex amy $ \(wsAlice, wsAlex, wsAmy) -> do (rsp, federatedRequests) <- - withTempMockFederator (const ()) $ + withTempMockFederator' (mockReply ()) $ postConvQualified alice defNewProteusConv {newConvName = checked nameMaxSize, newConvQualifiedUsers = [qAlex, qAmy, qChad, qCharlie, qDee]} - pure $ - if - | d == bDomain -> - UserMap . Map.fromList $ - [ (qUnqualified bob, Set.singleton (mkPubClient bobClient)), - (qUnqualified bart, Set.fromList (map mkPubClient [bartClient1, bartClient2])) - ] - | d == cDomain -> UserMap (Map.singleton (qUnqualified carl) (Set.singleton (PubClient carlClient Nothing))) - | otherwise -> mempty - - galleyApi _ = - mkHandler @(FedApi 'Galley) $ Named @"on-message-sent" $ \_ _ -> pure () - - (resp2, requests) <- postProteusMessageQualifiedWithMockFederator aliceU aliceClient convId message "data" Message.MismatchReportAll brigApi galleyApi + brigMock = do + guardRPC "get-user-clients" + d <- frTargetDomain <$> getRequest + asum + [ do + guard (d == bDomain) + + mockReply $ + UserMap . Map.fromList $ + [ (qUnqualified bob, Set.singleton (mkPubClient bobClient)), + (qUnqualified bart, Set.fromList (map mkPubClient [bartClient1, bartClient2])) + ], + do + guard (d == cDomain) + mockReply $ + UserMap + ( Map.singleton + (qUnqualified carl) + (Set.singleton (PubClient carlClient Nothing)) + ) + ] + galleyMock = "on-message-sent" ~> () + + (resp2, requests) <- postProteusMessageQualifiedWithMockFederator aliceU aliceClient convId message "data" Message.MismatchReportAll (brigMock <|> galleyMock) pure resp2 !!! do const 201 === statusCode assertMismatchQualified mempty mempty mempty mempty @@ -835,13 +839,8 @@ postMessageQualifiedLocalOwningBackendMissingClients = do let message = [(chadOwningDomain, chadClient, "text-for-chad")] -- FUTUREWORK: Mock federator and ensure that message is not propagated to remotes WS.bracketR2 cannon bobUnqualified chadUnqualified $ \(wsBob, wsChad) -> do - let brigApi _ = - mkHandler @(FedApi 'Brig) $ - Named @"get-user-clients" $ \_ _ -> - pure $ UserMap (Map.singleton (qUnqualified deeRemote) (Set.singleton (PubClient deeClient Nothing))) - galleyApi _ = mkHandler @(FedApi 'Galley) EmptyAPI - - (resp2, _requests) <- postProteusMessageQualifiedWithMockFederator aliceUnqualified aliceClient convId message "data" Message.MismatchReportAll brigApi galleyApi + let mock = "get-user-clients" ~> UserMap (Map.singleton (qUnqualified deeRemote) (Set.singleton (PubClient deeClient Nothing))) + (resp2, _requests) <- postProteusMessageQualifiedWithMockFederator aliceUnqualified aliceClient convId message "data" Message.MismatchReportAll mock pure resp2 !!! do const 412 === statusCode @@ -913,20 +912,17 @@ postMessageQualifiedLocalOwningBackendRedundantAndDeletedClients = do ] -- FUTUREWORK: Mock federator and ensure that a message to Dee is sent - let brigApi _ = - mkHandler @(FedApi 'Brig) $ - Named @"get-user-clients" $ \_ getUserClients -> - let lookupClients uid - | uid == deeRemoteUnqualified = Just (uid, Set.fromList [PubClient deeClient Nothing]) - | uid == nonMemberRemoteUnqualified = Just (uid, Set.fromList [PubClient nonMemberRemoteClient Nothing]) - | otherwise = Nothing - in pure $ UserMap . Map.fromList . mapMaybe lookupClients $ F.gucUsers getUserClients - galleyApi _ = - mkHandler @(FedApi 'Galley) $ - Named @"on-message-sent" $ - \_ _ -> pure () - - (resp2, _requests) <- postProteusMessageQualifiedWithMockFederator aliceUnqualified aliceClient convId message "data" Message.MismatchReportAll brigApi galleyApi + let brigMock = do + guardRPC "get-user-clients" + getUserClients <- getRequestBody + let lookupClients uid + | uid == deeRemoteUnqualified = Just (uid, Set.fromList [PubClient deeClient Nothing]) + | uid == nonMemberRemoteUnqualified = Just (uid, Set.fromList [PubClient nonMemberRemoteClient Nothing]) + | otherwise = Nothing + mockReply $ UserMap . Map.fromList . mapMaybe lookupClients $ F.gucUsers getUserClients + galleyMock = "on-message-sent" ~> () + + (resp2, _requests) <- postProteusMessageQualifiedWithMockFederator aliceUnqualified aliceClient convId message "data" Message.MismatchReportAll (brigMock <|> galleyMock) pure resp2 !!! do const 201 === statusCode let expectedRedundant = @@ -993,18 +989,16 @@ postMessageQualifiedLocalOwningBackendIgnoreMissingClients = do defNewProteusConv {newConvQualifiedUsers = [bobOwningDomain, chadOwningDomain, deeRemote]} let convId = (`Qualified` owningDomain) . decodeConvId $ resp - let brigApi _ = - mkHandler @(FedApi 'Brig) $ - Named @"get-user-clients" $ \_ _ -> - pure $ UserMap (Map.singleton (qUnqualified deeRemote) (Set.singleton (PubClient deeClient Nothing))) - galleyApi _ = mkHandler @(FedApi 'Galley) EmptyAPI + let mock = + "get-user-clients" ~> + UserMap (Map.singleton (qUnqualified deeRemote) (Set.singleton (PubClient deeClient Nothing))) -- Missing Bob, chadClient2 and Dee let message = [(chadOwningDomain, chadClient, "text-for-chad")] -- FUTUREWORK: Mock federator and ensure that clients of Dee are checked. Also -- ensure that message is not propagated to remotes WS.bracketR2 cannon bobUnqualified chadUnqualified $ \(wsBob, wsChad) -> do - (resp2, _requests) <- postProteusMessageQualifiedWithMockFederator aliceUnqualified aliceClient convId message "data" Message.MismatchIgnoreAll brigApi galleyApi + (resp2, _requests) <- postProteusMessageQualifiedWithMockFederator aliceUnqualified aliceClient convId message "data" Message.MismatchIgnoreAll mock pure resp2 !!! do const 201 === statusCode assertMismatchQualified mempty mempty mempty mempty @@ -1015,7 +1009,7 @@ postMessageQualifiedLocalOwningBackendIgnoreMissingClients = do -- Another way to ignore all is to report nobody WS.bracketR2 cannon bobUnqualified chadUnqualified $ \(wsBob, wsChad) -> do - (resp2, _requests) <- postProteusMessageQualifiedWithMockFederator aliceUnqualified aliceClient convId message "data" (Message.MismatchReportOnly mempty) brigApi galleyApi + (resp2, _requests) <- postProteusMessageQualifiedWithMockFederator aliceUnqualified aliceClient convId message "data" (Message.MismatchReportOnly mempty) mock pure resp2 !!! do const 201 === statusCode assertMismatchQualified mempty mempty mempty mempty @@ -1034,8 +1028,7 @@ postMessageQualifiedLocalOwningBackendIgnoreMissingClients = do message "data" (Message.MismatchIgnoreOnly (Set.fromList [bobOwningDomain, chadOwningDomain, deeRemote])) - brigApi - galleyApi + mock pure resp2 !!! do const 201 === statusCode assertMismatchQualified mempty mempty mempty mempty @@ -1055,8 +1048,7 @@ postMessageQualifiedLocalOwningBackendIgnoreMissingClients = do message "data" (Message.MismatchReportOnly (Set.fromList [chadOwningDomain])) - brigApi - galleyApi + mock pure resp2 !!! do const 412 === statusCode let expectedMissing = @@ -1076,8 +1068,7 @@ postMessageQualifiedLocalOwningBackendIgnoreMissingClients = do message "data" (Message.MismatchReportOnly (Set.fromList [deeRemote])) - brigApi - galleyApi + mock pure resp2 !!! do const 412 === statusCode let expectedMissing = @@ -1128,16 +1119,15 @@ postMessageQualifiedLocalOwningBackendFailedToSendClients = do (deeRemote, deeClient, "text-for-dee") ] - let brigApi _ = - mkHandler @(FedApi 'Brig) $ - Named @"get-user-clients" $ \_ _ -> - pure $ UserMap (Map.singleton (qUnqualified deeRemote) (Set.singleton (PubClient deeClient Nothing))) - galleyApi _ = - mkHandler @(FedApi 'Galley) $ - Named @"on-message-sent" $ \_ _ -> - throwError err503 {errBody = "Down for maintenance."} + let mock = + ( "get-user-clients" ~> + UserMap (Map.singleton (qUnqualified deeRemote) (Set.singleton (PubClient deeClient Nothing))) + ) + <|> ( guardRPC "on-message-sent" + *> throw (MockErrorResponse HTTP.status503 "Down for maintenance.") + ) - (resp2, _requests) <- postProteusMessageQualifiedWithMockFederator aliceUnqualified aliceClient convId message "data" Message.MismatchReportAll brigApi galleyApi + (resp2, _requests) <- postProteusMessageQualifiedWithMockFederator aliceUnqualified aliceClient convId message "data" Message.MismatchReportAll mock let expectedFailedToSend = QualifiedUserClients . Map.fromList $ @@ -1166,16 +1156,12 @@ postMessageQualifiedRemoteOwningBackendFailure = do let remoteDomain = Domain "far-away.example.com" convId = Qualified convIdUnqualified remoteDomain - let brigApi _ = mkHandler @(FedApi 'Brig) EmptyAPI - let galleyApi _ = - mkHandler @(FedApi 'Galley) $ - Named @"send-message" $ - callsFed $ - callsFed $ \_ _ -> - throwError err503 {errBody = "Down for maintenance."} + let mock = + guardRPC "send-message" + *> throw (MockErrorResponse HTTP.status503 "Down for maintenance.") (resp2, _requests) <- - postProteusMessageQualifiedWithMockFederator aliceUnqualified aliceClient convId [] "data" Message.MismatchReportAll brigApi galleyApi + postProteusMessageQualifiedWithMockFederator aliceUnqualified aliceClient convId [] "data" Message.MismatchReportAll mock pure resp2 !!! do const 503 === statusCode @@ -1209,15 +1195,9 @@ postMessageQualifiedRemoteOwningBackendSuccess = do Message.mssFailedToSend = mempty } message = [(bobOwningDomain, bobClient, "text-for-bob"), (deeRemote, deeClient, "text-for-dee")] - brigApi _ = mkHandler @(FedApi 'Brig) EmptyAPI - galleyApi _ = mkHandler @(FedApi 'Galley) $ - Named @"send-message" $ - callsFed $ - callsFed $ \_ _ -> - pure (F.MessageSendResponse (Right mss)) - + mock = "send-message" ~> F.MessageSendResponse (Right mss) (resp2, _requests) <- - postProteusMessageQualifiedWithMockFederator aliceUnqualified aliceClient convId message "data" Message.MismatchReportAll brigApi galleyApi + postProteusMessageQualifiedWithMockFederator aliceUnqualified aliceClient convId message "data" Message.MismatchReportAll mock pure resp2 !!! do const 201 === statusCode @@ -1567,7 +1547,7 @@ testAccessUpdateGuestRemoved = do c <- view tsCannon WS.bracketRN c (map qUnqualified [alice, bob, charlie]) $ \[wsA, wsB, wsC] -> do -- conversation access role changes to team only - (_, reqs) <- withTempMockFederator (const ()) $ do + (_, reqs) <- withTempMockFederator' (mockReply ()) $ do putQualifiedAccessUpdate (qUnqualified alice) (cnvQualifiedId conv) @@ -2418,7 +2398,7 @@ testAddRemoteMember = do connectWithRemoteUser alice remoteBob (resp, reqs) <- - withTempMockFederator (respond remoteBob) $ + withTempMockFederator' (respond remoteBob) $ postQualifiedMembers alice (remoteBob :| []) convId FederatedRequest -> Value - respond bob req - | frComponent req == Brig = - toJSON [mkProfile bob (Name "bob")] - | frRPC req == "on-new-remote-conversation" = - toJSON EmptyResponse - | otherwise = toJSON () + respond :: Qualified UserId -> Mock LByteString + respond bob = + asum + [ guardComponent Brig *> mockReply [mkProfile bob (Name "bob")], + "on-new-remote-conversation" ~> EmptyResponse, + "on-conversation-updated" ~> () + ] testDeleteTeamConversationWithRemoteMembers :: TestM () testDeleteTeamConversationWithRemoteMembers = do @@ -2461,13 +2441,10 @@ testDeleteTeamConversationWithRemoteMembers = do connectWithRemoteUser alice remoteBob - let brigApi _ = mkHandler @(FedApi 'Brig) EmptyAPI - galleyApi _ = - mkHandler @(FedApi 'Galley) $ - (Named @"on-new-remote-conversation" $ \_ _ -> pure EmptyResponse) - :<|> (Named @"on-conversation-updated" $ \_ _ -> pure ()) - - (_, received) <- withTempServantMockFederator brigApi galleyApi localDomain $ do + let mock = + ("on-new-remote-conversation" ~> EmptyResponse) + <|> ("on-conversation-updated" ~> ()) + (_, received) <- withTempMockFederator' mock $ do postQualifiedMembers alice (remoteBob :| []) convId !!! const 200 === statusCode @@ -2539,8 +2516,8 @@ testGetQualifiedRemoteConv = do ProtocolProteus (respAll, _) <- - withTempMockFederator - (const remoteConversationResponse) + withTempMockFederator' + (mockReply remoteConversationResponse) (getConvQualified aliceId remoteConvId) conv <- responseJsonUnsafe <$> (pure respAll do - let success = pure . encode - case frTargetDomain fedReq of - d | d == remoteDomainA -> success $ GetConversationsResponse [mockConversationA] - d | d == remoteDomainB -> success $ GetConversationsResponse [mockConversationB] - d | d == remoteDomainC -> throw (DiscoveryFailureSrvNotAvailable "domainC") - _ -> assertFailure $ "Unrecognized domain: " <> show fedReq - ) - (listConvs alice req) + (respAll, receivedRequests) <- do + let mock = do + d <- frTargetDomain <$> getRequest + asum + [ guard (d == remoteDomainA) *> mockReply (GetConversationsResponse [mockConversationA]), + guard (d == remoteDomainB) *> mockReply (GetConversationsResponse [mockConversationB]), + guard (d == remoteDomainC) *> liftIO (throw (DiscoveryFailureSrvNotAvailable "domainC")), + do + r <- getRequest + liftIO . assertFailure $ "Unrecognized domain: " <> show r + ] + withTempMockFederator' mock (listConvs alice req) convs <- responseJsonUnsafe <$> (pure respAll mockReply () (respDel, fedRequests) <- - withTempMockFederator mockReturnEve $ + withTempMockFederator' mockReturnEve $ deleteMemberQualified alice qBob qconvId let [galleyFederatedRequest] = fedRequestsForDomain remoteDomain Galley fedRequests assertRemoveUpdate galleyFederatedRequest qconvId qAlice [qUnqualified qEve] qBob @@ -2947,21 +2927,17 @@ deleteRemoteMemberConvLocalQualifiedOk = do connectUsers alice (singleton bob) mapM_ (connectWithRemoteUser alice) [qChad, qDee, qEve] - let mockedResponse fedReq = do - let success :: ToJSON a => a -> IO LByteString - success = pure . encode - getUsersRPC = "get-users-by-ids" - case (frTargetDomain fedReq, frRPC fedReq) of - (d, mp) - | d == remoteDomain1 && mp == getUsersRPC -> - success [mkProfile qChad (Name "Chad"), mkProfile qDee (Name "Dee")] - (d, mp) - | d == remoteDomain2 && mp == getUsersRPC -> - success [mkProfile qEve (Name "Eve")] - _ -> success () - + let mockedResponse = do + guardRPC "get-users-by-ids" + d <- frTargetDomain <$> getRequest + asum + [ guard (d == remoteDomain1) + *> mockReply [mkProfile qChad (Name "Chad"), mkProfile qDee (Name "Dee")], + guard (d == remoteDomain2) + *> mockReply [mkProfile qEve (Name "Eve")] + ] (convId, _) <- - withTempMockFederator' mockedResponse $ + withTempMockFederator' (mockedResponse <|> mockReply ()) $ fmap decodeConvId $ postConvQualified alice @@ -2970,7 +2946,7 @@ deleteRemoteMemberConvLocalQualifiedOk = do let qconvId = Qualified convId localDomain (respDel, federatedRequests) <- - withTempMockFederator' mockedResponse $ + withTempMockFederator' (mockedResponse <|> mockReply ()) $ deleteMemberQualified alice qChad qconvId liftIO $ do statusCode respDel @?= 200 @@ -3002,18 +2978,15 @@ leaveRemoteConvQualifiedOk = do let remoteDomain = Domain "faraway.example.com" qconv = Qualified conv remoteDomain qBob = Qualified bob remoteDomain - let mockedFederatedGalleyResponse :: FederatedRequest -> Maybe Value - mockedFederatedGalleyResponse req - | frComponent req == Galley = - Just . toJSON . F.LeaveConversationResponse . Right $ () - | otherwise = Nothing + let mockedFederatedGalleyResponse = do + guardComponent Galley + mockReply (F.LeaveConversationResponse (Right ())) mockResponses = - joinMockedFederatedResponses - (mockedFederatedBrigResponse [(qBob, "Bob")]) - mockedFederatedGalleyResponse + mockedFederatedBrigResponse [(qBob, "Bob")] + <|> mockedFederatedGalleyResponse (resp, fedRequests) <- - withTempMockFederator mockResponses $ + withTempMockFederator' mockResponses $ deleteMemberQualified alice qAlice qconv let leaveRequest = fromJust . decode . frBody . Imports.head $ @@ -3033,15 +3006,13 @@ leaveNonExistentRemoteConv = do let remoteDomain = Domain "faraway.example.com" conv <- randomQualifiedId remoteDomain - let mockResponses :: FederatedRequest -> Maybe Value - mockResponses req - | frComponent req == Galley = - Just . toJSON . F.LeaveConversationResponse $ - Left F.RemoveFromConversationErrorNotFound - | otherwise = Nothing + let mockResponses = do + guardComponent Galley + mockReply $ + F.LeaveConversationResponse (Left F.RemoveFromConversationErrorNotFound) (resp, fedRequests) <- - withTempMockFederator mockResponses $ + withTempMockFederator' mockResponses $ responseJsonError =<< deleteMemberQualified (qUnqualified alice) alice conv Maybe Value - mockResponses req - | frComponent req == Galley = - Just . toJSON . F.LeaveConversationResponse $ - Left F.RemoveFromConversationErrorRemovalNotAllowed - | otherwise = Nothing + let mockResponses = do + guardComponent Galley + mockReply $ + F.LeaveConversationResponse + ( Left F.RemoveFromConversationErrorRemovalNotAllowed + ) (resp, fedRequests) <- - withTempMockFederator mockResponses $ + withTempMockFederator' mockResponses $ responseJsonError =<< deleteMemberQualified (qUnqualified alice) alice conv do (_, requests) <- - withTempMockFederator (const ()) $ + withTempMockFederator' (mockReply ()) $ putQualifiedConversationName bob qconv "gossip++" !!! const 200 === statusCode req <- assertOne requests @@ -3497,8 +3468,8 @@ putRemoteConvMemberOk update = do [localMemberToOther remoteDomain bobAsLocal] remoteConversationResponse = GetConversationsResponse [mockConversation] (rs, _) <- - withTempMockFederator - (const remoteConversationResponse) + withTempMockFederator' + (mockReply remoteConversationResponse) $ getConvQualified alice qconv do - (res, federatedRequests) <- withTempMockFederator mockResponse $ do + (res, federatedRequests) <- withTempMockFederator' mockResponse $ do putQualifiedReceiptMode alice qconv newReceiptMode do (_, requests) <- - withTempMockFederator (const ()) $ + withTempMockFederator' (mockReply ()) $ putQualifiedReceiptMode bob qconv (ReceiptMode 43) !!! const 200 === statusCode req <- assertOne requests @@ -3887,16 +3858,19 @@ removeUser = do runFedClient @"on-conversation-created" fedGalleyClient dDomain $ nc convD1 dory [alexDel] WS.bracketR3 c alice' alexDel' amy' $ \(wsAlice, wsAlexDel, wsAmy) -> do - let handler :: FederatedRequest -> IO LByteString - handler freq - | frTargetDomain freq == dDomain = - throw $ DiscoveryFailureSrvNotAvailable "dDomain" - | frTargetDomain freq `elem` [bDomain, cDomain] = - case frRPC freq of - "leave-conversation" -> pure (encode (F.LeaveConversationResponse (Right ()))) - "on-conversation-updated" -> pure (encode ()) - _ -> throw $ MockErrorResponse HTTP.status404 "invalid rpc" - | otherwise = throw $ MockErrorResponse HTTP.status500 "unmocked domain" + let handler = do + d <- frTargetDomain <$> getRequest + asum + [ do + guard (d == dDomain) + throw (DiscoveryFailureSrvNotAvailable "dDomain"), + do + guard (d `elem` [bDomain, cDomain]) + asum + [ "leave-conversation" ~> F.LeaveConversationResponse (Right ()), + "on-conversation-updated" ~> () + ] + ] (_, fedRequests) <- withTempMockFederator' handler $ deleteUser alexDel' !!! const 200 === statusCode @@ -4037,7 +4011,7 @@ testOne2OneConversationRequest shouldBeLocal actor desired = do found <- do let rconv = mkProteusConv (qUnqualified convId) (tUnqualified bob) roleNameWireAdmin [] (resp, _) <- - withTempMockFederator (const (F.GetConversationsResponse [rconv])) $ + withTempMockFederator' (mockReply (F.GetConversationsResponse [rconv])) $ getConvQualified (tUnqualified alice) convId pure $ statusCode resp == 200 liftIO $ found @?= ((actor, desired) == (LocalActor, Included)) @@ -4083,15 +4057,15 @@ updateTypingIndicatorToRemoteUserRemoteConv = do [localMemberToOther remoteDomain bobAsLocal] remoteConversationResponse = GetConversationsResponse [mockConversation] void - $ withTempMockFederator - (const remoteConversationResponse) + $ withTempMockFederator' + (mockReply remoteConversationResponse) $ getConvQualified alice qconv do -- Started void $ - withTempMockFederator (const ()) $ do + withTempMockFederator' (mockReply ()) $ do -- post typing indicator from bob to alice let tcReq = TypingDataUpdateRequest @@ -4109,7 +4083,7 @@ updateTypingIndicatorToRemoteUserRemoteConv = do -- stopped void $ - withTempMockFederator (const ()) $ do + withTempMockFederator' (mockReply ()) $ do -- post typing indicator from bob to alice let tcReq = TypingDataUpdateRequest @@ -4145,7 +4119,7 @@ updateTypingIndicatorFromRemoteUser = do WS.bracketR c alice $ \wsAlice -> do -- Started void $ - withTempMockFederator (const ()) $ do + withTempMockFederator' (mockReply ()) $ do -- post typing indicator from bob to alice let tcReq = TypingDataUpdateRequest @@ -4164,7 +4138,7 @@ updateTypingIndicatorFromRemoteUser = do -- stopped void $ - withTempMockFederator (const ()) $ do + withTempMockFederator' (mockReply ()) $ do -- post typing indicator from bob to alice let tcReq = TypingDataUpdateRequest @@ -4200,7 +4174,7 @@ updateTypingIndicatorToRemoteUser = do WS.bracketR c bob $ \wsBob -> do -- started void $ - withTempMockFederator (const ()) $ do + withTempMockFederator' (mockReply ()) $ do -- post typing indicator from alice to bob let tcReq = TypingDataUpdateRequest @@ -4219,7 +4193,7 @@ updateTypingIndicatorToRemoteUser = do -- stopped void $ - withTempMockFederator (const ()) $ do + withTempMockFederator' (mockReply ()) $ do -- post typing indicator from alice to bob let tcReq = TypingDataUpdateRequest diff --git a/services/galley/test/integration/API/Federation.hs b/services/galley/test/integration/API/Federation.hs index e33915ce83..99e90443ba 100644 --- a/services/galley/test/integration/API/Federation.hs +++ b/services/galley/test/integration/API/Federation.hs @@ -22,7 +22,6 @@ import API.Util import Bilge hiding (head) import Bilge.Assert import Control.Lens hiding ((#)) -import Data.Aeson (ToJSON (..)) import qualified Data.Aeson as A import Data.ByteString.Conversion (toByteString') import Data.Domain @@ -41,7 +40,7 @@ import Data.String.Conversions import Data.Time.Clock import Data.Timeout (TimeoutUnit (..), (#)) import Data.UUID.V4 (nextRandom) -import Federator.MockServer (FederatedRequest (..)) +import Federator.MockServer import Galley.Types.Conversations.Intra import Imports import Test.QuickCheck (arbitrary, generate) @@ -643,21 +642,18 @@ leaveConversationSuccess = do connectWithRemoteUser alice qDee connectWithRemoteUser alice qEve - let mockedResponse fedReq = do - let success :: ToJSON a => a -> IO LByteString - success = pure . A.encode - getUsersRPC = "get-users-by-ids" - case (frTargetDomain fedReq, frRPC fedReq) of - (d, mp) - | d == remoteDomain1 && mp == getUsersRPC -> - success [mkProfile qChad (Name "Chad"), mkProfile qDee (Name "Dee")] - (d, mp) - | d == remoteDomain2 && mp == getUsersRPC -> - success [mkProfile qEve (Name "Eve")] - _ -> success () + let mock = do + guardRPC "get-users-by-ids" + d <- frTargetDomain <$> getRequest + asum + [ guard (d == remoteDomain1) + *> mockReply [mkProfile qChad (Name "Chad"), mkProfile qDee (Name "Dee")], + guard (d == remoteDomain2) + *> mockReply [mkProfile qEve (Name "Eve")] + ] (convId, _) <- - withTempMockFederator' mockedResponse $ + withTempMockFederator' (mock <|> mockReply ()) $ decodeConvId <$> postConvQualified alice @@ -668,7 +664,7 @@ leaveConversationSuccess = do (_, federatedRequests) <- WS.bracketR2 c alice bob $ \(wsAlice, wsBob) -> do - withTempMockFederator' mockedResponse $ do + withTempMockFederator' (mock <|> mockReply ()) $ do g <- viewGalley let leaveRequest = FedGalley.LeaveConversationRequest convId (qUnqualified qChad) respBS <- @@ -842,12 +838,9 @@ sendMessage = do connectWithRemoteUser aliceId bob connectWithRemoteUser aliceId chad -- conversation - let responses1 req - | frComponent req == Brig = - toJSON [bobProfile, chadProfile] - | otherwise = toJSON () + let responses1 = guardComponent Brig *> mockReply [bobProfile, chadProfile] (convId, requests1) <- - withTempMockFederator responses1 $ + withTempMockFederator' (responses1 <|> mockReply ()) $ fmap decodeConvId $ postConvQualified aliceId @@ -878,16 +871,14 @@ sendMessage = do FedGalley.pmsrSender = bobId, FedGalley.pmsrRawMessage = Base64ByteString (Protolens.encodeMessage msg) } - let responses2 req - | frComponent req == Brig = - toJSON - ( Map.fromList - [ (chadId, Set.singleton (PubClient chadClient Nothing)), - (bobId, Set.singleton (PubClient bobClient Nothing)) - ] - ) - | otherwise = toJSON () - (_, requests2) <- withTempMockFederator responses2 $ do + let mock = do + guardComponent Brig + mockReply $ + Map.fromList + [ (chadId, Set.singleton (PubClient chadClient Nothing)), + (bobId, Set.singleton (PubClient bobClient Nothing)) + ] + (_, requests2) <- withTempMockFederator' (mock <|> mockReply ()) $ do WS.bracketR cannon aliceId $ \ws -> do g <- viewGalley msresp <- @@ -982,7 +973,7 @@ onUserDeleted = do do - (resp, rpcCalls) <- withTempMockFederator (const ()) $ do + (resp, rpcCalls) <- withTempMockFederator' (mockReply ()) $ do let udcn = FedGalley.UserDeletedConversationsNotification { FedGalley.udcvUser = tUnqualified bob, @@ -1060,7 +1051,7 @@ updateConversationByRemoteAdmin = do let convName = "Test Conv" WS.bracketR c alice $ \wsAlice -> do (rsp, _federatedRequests) <- - withTempMockFederator (const ()) $ do + withTempMockFederator' (mockReply ()) $ do postConvQualified alice defNewProteusConv {newConvName = checked convName, newConvQualifiedUsers = [qbob, qcharlie]} TestTree tests s = @@ -313,11 +312,6 @@ testRemoteWelcome :: TestM () testRemoteWelcome = do [alice, bob] <- createAndConnectUsers [Nothing, Just "bob.example.com"] - let mockedResponse fedReq = - case frRPC fedReq of - "mls-welcome" -> pure (Aeson.encode MLSWelcomeSent) - ms -> assertFailure ("unmocked endpoint called: " <> cs ms) - runMLSTest $ do alice1 <- createMLSClient alice _bob1 <- createFakeMLSClient bob @@ -328,7 +322,7 @@ testRemoteWelcome = do Nothing -> assertFailure "Expected welcome message" Just w -> pure w (_, reqs) <- - withTempMockFederator' mockedResponse $ + withTempMockFederator' welcomeMock $ postWelcome (ciUser (mpSender commit)) welcome !!! const 201 === statusCode consumeWelcome welcome @@ -630,21 +624,9 @@ testAddRemoteUser = do [alice1, bob1] <- traverse createMLSClient users (_, qcnv) <- setupMLSGroup alice1 - let mock req = case frRPC req of - "on-conversation-updated" -> pure (Aeson.encode ()) - "on-new-remote-conversation" -> pure (Aeson.encode EmptyResponse) - "get-mls-clients" -> - pure - . Aeson.encode - . Set.fromList - . map (flip ClientInfo True . ciClient) - $ [bob1] - "mls-welcome" -> pure (Aeson.encode MLSWelcomeSent) - ms -> assertFailure ("unmocked endpoint called: " <> cs ms) - commit <- createAddCommit alice1 [bob] (events, reqs) <- - withTempMockFederator' mock $ + withTempMockFederator' (receiveCommitMock [bob1] <|> welcomeMock) $ sendAndConsumeCommit commit pure (events, reqs, qcnv) @@ -820,17 +802,7 @@ testRemoteAppMessage = do (_, qcnv) <- setupMLSGroup alice1 - let mock req = case frRPC req of - "on-conversation-updated" -> pure (Aeson.encode ()) - "on-new-remote-conversation" -> pure (Aeson.encode EmptyResponse) - "on-mls-message-sent" -> pure (Aeson.encode RemoteMLSMessageOk) - "mls-welcome" -> pure (Aeson.encode MLSWelcomeSent) - "get-mls-clients" -> - pure - . Aeson.encode - . Set.singleton - $ ClientInfo (ciClient bob1) True - ms -> assertFailure ("unmocked endpoint called: " <> cs ms) + let mock = receiveCommitMock [bob1] <|> messageSentMock <|> welcomeMock ((message, events), reqs) <- withTempMockFederator' mock $ do void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit @@ -905,12 +877,8 @@ testLocalToRemote = do -- A notifies B about bob being in the conversation (Join event): step 5 receiveOnConvUpdated qcnv alice bob - let mock req = case frRPC req of - "send-mls-message" -> pure (Aeson.encode (MLSMessageResponseUpdates [])) - rpc -> assertFailure $ "unmocked RPC called: " <> T.unpack rpc - (_, reqs) <- - withTempMockFederator' mock $ + withTempMockFederator' sendMessageMock $ -- bob sends a message: step 12 sendAndConsumeMessage message @@ -949,12 +917,8 @@ testLocalToRemoteNonMember = do -- register remote conversation: step 4 receiveNewRemoteConv qcnv groupId - let mock req = case frRPC req of - "send-mls-message" -> pure (Aeson.encode (MLSMessageResponseUpdates [])) - rpc -> assertFailure $ "unmocked RPC called: " <> T.unpack rpc - void $ - withTempMockFederator' mock $ do + withTempMockFederator' sendMessageMock $ do galley <- viewGalley -- bob sends a message: step 12 @@ -1208,20 +1172,8 @@ testRemoteToLocal = do kpb <- claimKeyPackages alice1 bob mp <- createAddCommit alice1 [bob] - let mockedResponse fedReq = - case frRPC fedReq of - "mls-welcome" -> pure (Aeson.encode MLSWelcomeSent) - "on-new-remote-conversation" -> pure (Aeson.encode EmptyResponse) - "on-conversation-updated" -> pure (Aeson.encode ()) - "get-mls-clients" -> - pure - . Aeson.encode - . Set.singleton - $ ClientInfo (ciClient bob1) True - "claim-key-packages" -> pure . Aeson.encode $ kpb - ms -> assertFailure ("unmocked endpoint called: " <> cs ms) - - void . withTempMockFederator' mockedResponse $ + let mock = receiveCommitMock [bob1] <|> welcomeMock <|> claimKeyPackagesMock kpb + void . withTempMockFederator' mock $ sendAndConsumeCommit mp traverse_ consumeWelcome (mpWelcome mp) @@ -1266,19 +1218,8 @@ testRemoteToLocalWrongConversation = do void $ setupMLSGroup alice1 mp <- createAddCommit alice1 [bob] - let mockedResponse fedReq = - case frRPC fedReq of - "mls-welcome" -> pure (Aeson.encode MLSWelcomeSent) - "on-new-remote-conversation" -> pure (Aeson.encode EmptyResponse) - "on-conversation-updated" -> pure (Aeson.encode ()) - "get-mls-clients" -> - pure - . Aeson.encode - . Set.singleton - $ ClientInfo (ciClient bob1) True - ms -> assertFailure ("unmocked endpoint called: " <> cs ms) - - void . withTempMockFederator' mockedResponse $ sendAndConsumeCommit mp + let mock = receiveCommitMock [bob1] <|> welcomeMock + void . withTempMockFederator' mock $ sendAndConsumeCommit mp traverse_ consumeWelcome (mpWelcome mp) message <- createApplicationMessage bob1 "hello from another backend" @@ -1636,19 +1577,7 @@ testBackendRemoveProposalLocalConvRemoteUser = do (_, qcnv) <- setupMLSGroup alice1 commit <- createAddCommit alice1 [bob] - let mock req = case frRPC req of - "on-conversation-updated" -> pure (Aeson.encode ()) - "on-new-remote-conversation" -> pure (Aeson.encode EmptyResponse) - "on-mls-message-sent" -> pure (Aeson.encode RemoteMLSMessageOk) - "mls-welcome" -> pure (Aeson.encode MLSWelcomeSent) - "get-mls-clients" -> - pure - . Aeson.encode - . Set.fromList - . map (flip ClientInfo True . ciClient) - $ [bob1, bob2] - ms -> assertFailure ("unmocked endpoint called: " <> cs ms) - + let mock = receiveCommitMock [bob1, bob2] <|> welcomeMock <|> messageSentMock void . withTempMockFederator' mock $ do mlsBracket [alice1] $ \[wsA] -> do void $ sendAndConsumeCommit commit @@ -1813,19 +1742,7 @@ testBackendRemoveProposalLocalConvRemoteLeaver = do (_, qcnv) <- setupMLSGroup alice1 commit <- createAddCommit alice1 [bob] - let mock req = case frRPC req of - "on-conversation-updated" -> pure (Aeson.encode ()) - "on-new-remote-conversation" -> pure (Aeson.encode EmptyResponse) - "on-mls-message-sent" -> pure (Aeson.encode RemoteMLSMessageOk) - "mls-welcome" -> pure (Aeson.encode MLSWelcomeSent) - "get-mls-clients" -> - pure - . Aeson.encode - . Set.fromList - . map (flip ClientInfo True . ciClient) - $ [bob1, bob2] - ms -> assertFailure ("unmocked endpoint called: " <> cs ms) - + let mock = receiveCommitMock [bob1, bob2] <|> welcomeMock <|> messageSentMock bobClients <- getClientsFromGroupState alice1 bob void . withTempMockFederator' mock $ do mlsBracket [alice1] $ \[wsA] -> void $ do @@ -1889,21 +1806,8 @@ testBackendRemoveProposalLocalConvRemoteClient = do (_, qcnv) <- setupMLSGroup alice1 commit <- createAddCommit alice1 [bob] - let mock req = case frRPC req of - "on-conversation-updated" -> pure (Aeson.encode ()) - "on-new-remote-conversation" -> pure (Aeson.encode EmptyResponse) - "mls-welcome" -> pure (Aeson.encode MLSWelcomeSent) - "on-mls-message-sent" -> pure (Aeson.encode RemoteMLSMessageOk) - "get-mls-clients" -> - pure - . Aeson.encode - . Set.fromList - . map (flip ClientInfo True) - $ [ciClient bob1] - ms -> assertFailure ("unmocked endpoint called: " <> cs ms) - [(_, bob1KP)] <- getClientsFromGroupState alice1 bob - + let mock = receiveCommitMock [bob1] <|> welcomeMock <|> messageSentMock void . withTempMockFederator' mock $ do mlsBracket [alice1] $ \[wsA] -> void $ do void $ sendAndConsumeCommit commit @@ -1957,16 +1861,7 @@ testGetGroupInfoOfRemoteConv = do receiveOnConvUpdated qcnv alice bob let fakeGroupState = "\xde\xad\xbe\xef" - let mock req = case frRPC req of - "query-group-info" -> do - request <- either (assertFailure . ("Parse failure in query-group-info " <>)) pure (Aeson.eitherDecode (frBody req)) - let uid = ggireqSender request - pure . Aeson.encode $ - if uid == qUnqualified bob - then GetGroupInfoResponseState (Base64ByteString fakeGroupState) - else GetGroupInfoResponseError ConvNotFound - s -> error ("unmocked: " <> T.unpack s) - + let mock = queryGroupStateMock fakeGroupState bob (_, reqs) <- withTempMockFederator' mock $ do res <- fmap responseBody $ @@ -1994,18 +1889,7 @@ testFederatedGetGroupInfo = do commit <- createAddCommit alice1 [bob] groupState <- assertJust (mpPublicGroupState commit) - let mock req = case frRPC req of - "on-new-remote-conversation" -> pure (Aeson.encode EmptyResponse) - "on-conversation-updated" -> pure (Aeson.encode ()) - "mls-welcome" -> pure (Aeson.encode MLSWelcomeSent) - "get-mls-clients" -> - pure - . Aeson.encode - . Set.fromList - . map (flip ClientInfo True) - $ [ciClient bob1] - ms -> assertFailure ("unmocked endpoint called: " <> cs ms) - + let mock = receiveCommitMock [bob1] <|> welcomeMock void . withTempMockFederator' mock $ do void $ sendAndConsumeCommitBundle commit @@ -2080,9 +1964,7 @@ testAddUserToRemoteConvWithBundle = do commit <- createAddCommit bob1 [charlie] commitBundle <- createBundle commit - let mock req = case frRPC req of - "send-mls-commit-bundle" -> pure (Aeson.encode (MLSMessageResponseUpdates [])) - s -> error ("unmocked: " <> T.unpack s) + let mock = "send-mls-commit-bundle" ~> MLSMessageResponseUpdates [] (_, reqs) <- withTempMockFederator' mock $ do void $ sendAndConsumeCommitBundle commit @@ -2109,20 +1991,9 @@ testRemoteUserPostsCommitBundle = do [alice1, bob1] <- traverse createMLSClient [alice, bob] (_, qcnv) <- setupMLSGroup alice1 - let mock req = case frRPC req of - "on-conversation-updated" -> pure (Aeson.encode ()) - "on-new-remote-conversation" -> pure (Aeson.encode EmptyResponse) - "get-mls-clients" -> - pure - . Aeson.encode - . Set.fromList - . map (flip ClientInfo True . ciClient) - $ [bob1] - "mls-welcome" -> pure (Aeson.encode MLSWelcomeSent) - ms -> assertFailure ("unmocked endpoint called: " <> cs ms) - commit <- createAddCommit alice1 [bob] - void $ + void $ do + let mock = receiveCommitMock [bob1] <|> welcomeMock withTempMockFederator' mock $ do void $ sendAndConsumeCommit commit putOtherMemberQualified (qUnqualified alice) bob (OtherMemberUpdate (Just roleNameWireAdmin)) qcnv diff --git a/services/galley/test/integration/API/MLS/Mocks.hs b/services/galley/test/integration/API/MLS/Mocks.hs new file mode 100644 index 0000000000..71d1dd6a7e --- /dev/null +++ b/services/galley/test/integration/API/MLS/Mocks.hs @@ -0,0 +1,71 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module API.MLS.Mocks + ( receiveCommitMock, + messageSentMock, + welcomeMock, + sendMessageMock, + claimKeyPackagesMock, + queryGroupStateMock, + ) +where + +import Data.Id +import Data.Json.Util +import Data.Qualified +import qualified Data.Set as Set +import Federator.MockServer +import Imports +import Wire.API.Error.Galley +import Wire.API.Federation.API.Common +import Wire.API.Federation.API.Galley +import Wire.API.MLS.Credential +import Wire.API.MLS.KeyPackage +import Wire.API.User.Client + +receiveCommitMock :: [ClientIdentity] -> Mock LByteString +receiveCommitMock clients = + asum + [ "on-conversation-updated" ~> (), + "on-new-remote-conversation" ~> EmptyResponse, + "get-mls-clients" ~> + Set.fromList + ( map (flip ClientInfo True . ciClient) clients + ) + ] + +messageSentMock :: Mock LByteString +messageSentMock = "on-mls-message-sent" ~> RemoteMLSMessageOk + +welcomeMock :: Mock LByteString +welcomeMock = "mls-welcome" ~> MLSWelcomeSent + +sendMessageMock :: Mock LByteString +sendMessageMock = "send-mls-message" ~> MLSMessageResponseUpdates [] + +claimKeyPackagesMock :: KeyPackageBundle -> Mock LByteString +claimKeyPackagesMock kpb = "claim-key-packages" ~> kpb + +queryGroupStateMock :: ByteString -> Qualified UserId -> Mock LByteString +queryGroupStateMock gs qusr = do + guardRPC "query-group-info" + uid <- ggireqSender <$> getRequestBody + mockReply $ + if uid == qUnqualified qusr + then GetGroupInfoResponseState (Base64ByteString gs) + else GetGroupInfoResponseError ConvNotFound diff --git a/services/galley/test/integration/API/MessageTimer.hs b/services/galley/test/integration/API/MessageTimer.hs index b464c0b0d8..4bc37d317f 100644 --- a/services/galley/test/integration/API/MessageTimer.hs +++ b/services/galley/test/integration/API/MessageTimer.hs @@ -32,7 +32,7 @@ import qualified Data.List1 as List1 import Data.Misc import Data.Qualified import Data.Singletons -import Federator.MockServer (FederatedRequest (..)) +import Federator.MockServer import Imports hiding (head) import Network.Wai.Utilities.Error import Test.Tasty @@ -156,7 +156,7 @@ messageTimerChangeWithRemotes = do WS.bracketR c bob $ \wsB -> do (_, requests) <- - withTempMockFederator (const ()) $ + withTempMockFederator' (mockReply ()) $ putMessageTimerUpdateQualified bob qconv (ConversationMessageTimerUpdate timer1sec) !!! const 200 === statusCode diff --git a/services/galley/test/integration/API/Roles.hs b/services/galley/test/integration/API/Roles.hs index b5ed4cb27c..72cb40b494 100644 --- a/services/galley/test/integration/API/Roles.hs +++ b/services/galley/test/integration/API/Roles.hs @@ -30,7 +30,7 @@ import qualified Data.List1 as List1 import Data.Qualified import qualified Data.Set as Set import Data.Singletons -import Federator.MockServer (FederatedRequest (..)) +import Federator.MockServer import Imports import Network.Wai.Utilities.Error import Test.Tasty @@ -175,7 +175,7 @@ roleUpdateRemoteMember = do WS.bracketR c bob $ \wsB -> do (_, requests) <- - withTempMockFederator (const ()) $ + withTempMockFederator' (mockReply ()) $ putOtherMemberQualified bob qcharlie @@ -244,7 +244,7 @@ roleUpdateWithRemotes = do WS.bracketR2 c bob charlie $ \(wsB, wsC) -> do (_, requests) <- - withTempMockFederator (const ()) $ + withTempMockFederator' (mockReply ()) $ putOtherMemberQualified bob qcharlie @@ -303,7 +303,7 @@ accessUpdateWithRemotes = do let access = ConversationAccessData (Set.singleton CodeAccess) (Set.fromList [TeamMemberAccessRole, NonTeamMemberAccessRole, GuestAccessRole, ServiceAccessRole]) WS.bracketR2 c bob charlie $ \(wsB, wsC) -> do (_, requests) <- - withTempMockFederator (const ()) $ + withTempMockFederator' (mockReply ()) $ putQualifiedAccessUpdate bob qconv access !!! const 200 === statusCode diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 11255a97e0..12a928e19d 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -24,6 +24,7 @@ import Bilge.Assert import Bilge.TestSession import Brig.Types.Connection import Brig.Types.Intra (UserAccount (..)) +import Control.Applicative import Control.Concurrent.Async import Control.Exception (throw) import Control.Lens hiding (from, to, uncons, (#), (.=)) @@ -69,7 +70,7 @@ import Data.Time (getCurrentTime) import Data.Tuple.Extra import qualified Data.UUID as UUID import Data.UUID.V4 -import Federator.MockServer (FederatedRequest (..)) +import Federator.MockServer import qualified Federator.MockServer as Mock import GHC.TypeLits (KnownSymbol) import Galley.Intra.User (chunkify) @@ -719,7 +720,7 @@ postConvWithRemoteUsers :: TestM (Response (Maybe LByteString)) postConvWithRemoteUsers u n = fmap fst $ - withTempMockFederator (const ()) $ + withTempMockFederator' (mockReply ()) $ postConvQualified u n {newConvName = setName (newConvName n)} ByteString -> ClientMismatchStrategy -> - (Domain -> ServerT (FedApi 'Brig) Handler) -> - (Domain -> ServerT (FedApi 'Galley) Handler) -> + Mock LByteString -> TestM (ResponseLBS, [FederatedRequest]) -postProteusMessageQualifiedWithMockFederator senderUser senderClient convId recipients dat strat brigApi galleyApi = do - localDomain <- viewFederationDomain - withTempServantMockFederator brigApi galleyApi localDomain $ +postProteusMessageQualifiedWithMockFederator senderUser senderClient convId recipients dat strat mock = + withTempMockFederator' mock $ postProteusMessageQualified senderUser senderClient convId recipients dat strat postProteusMessageQualified :: @@ -2572,48 +2571,21 @@ mkProfile quid name = -- | Run the given action on a temporary galley instance with access to a mock -- federator. --- --- The `resp :: FederatedRequest -> a` argument can be used to provide a fake --- federator response (of an arbitrary JSON-serialisable type a) for every --- expected request. -withTempMockFederator :: - ToJSON a => - (FederatedRequest -> a) -> - TestM b -> - TestM (b, [FederatedRequest]) -withTempMockFederator resp = withTempMockFederator' $ pure . encode . resp - withTempMockFederator' :: (MonadIO m, MonadMask m, HasSettingsOverrides m) => - (FederatedRequest -> IO LByteString) -> + Mock LByteString -> m b -> m (b, [FederatedRequest]) withTempMockFederator' resp action = do + let mock = runMock (assertFailure . Text.unpack) $ do + r <- resp + pure ("application" // "json", r) Mock.withTempMockFederator [("Content-Type", "application/json")] - ((\r -> pure ("application" // "json", r)) <=< resp) + mock $ \mockPort -> do withSettingsOverrides (\opts -> opts & Opts.optFederator ?~ Endpoint "127.0.0.1" (fromIntegral mockPort)) action --- Start a mock federator. Use provided Servant handler for the mocking function. -withTempServantMockFederator :: - (Domain -> ServerT (FedApi 'Brig) Handler) -> - (Domain -> ServerT (FedApi 'Galley) Handler) -> - Domain -> - TestM b -> - TestM (b, [FederatedRequest]) -withTempServantMockFederator brigApi galleyApi originDomain = - withTempMockFederator' mock - where - server :: Domain -> ServerT CombinedBrigAndGalleyAPI Handler - server d = brigApi d :<|> galleyApi d - - mock :: FederatedRequest -> IO LByteString - mock req = - makeFedRequestToServant @CombinedBrigAndGalleyAPI originDomain (server (frTargetDomain req)) req - -type CombinedBrigAndGalleyAPI = FedApi 'Brig :<|> FedApi 'Galley - -- Starts a servant Application in Network.Wai.Test session and runs the -- FederatedRequest against it. makeFedRequestToServant :: @@ -2772,28 +2744,10 @@ checkTimeout = 3 # Second -- | The function is used in conjuction with 'withTempMockFederator' to mock -- responses by Brig on the mocked side of federation. -mockedFederatedBrigResponse :: [(Qualified UserId, Text)] -> FederatedRequest -> Maybe Value -mockedFederatedBrigResponse users req - | frComponent req == Brig = - Just . toJSON $ [mkProfile mem (Name name) | (mem, name) <- users] - | otherwise = Nothing - --- | Combine two mocked services such that for a given request a JSON response --- is produced. -joinMockedFederatedResponses :: - (FederatedRequest -> Maybe Value) -> - (FederatedRequest -> Maybe Value) -> - FederatedRequest -> - Value -joinMockedFederatedResponses service1 service2 req = - fromMaybe (toJSON ()) (service1 req <|> service2 req) - --- | Only Brig is mocked. -onlyMockedFederatedBrigResponse :: [(Qualified UserId, Text)] -> FederatedRequest -> Value -onlyMockedFederatedBrigResponse users = - joinMockedFederatedResponses - (mockedFederatedBrigResponse users) - (const Nothing) +mockedFederatedBrigResponse :: [(Qualified UserId, Text)] -> Mock LByteString +mockedFederatedBrigResponse users = do + guardComponent Brig + mockReply [mkProfile mem (Name name) | (mem, name) <- users] fedRequestsForDomain :: HasCallStack => Domain -> Component -> [FederatedRequest] -> [FederatedRequest] fedRequestsForDomain domain component = From 7922ccce9fbe6588f49e82b5645d0aaea9110038 Mon Sep 17 00:00:00 2001 From: zebot Date: Wed, 25 Jan 2023 15:40:23 +0100 Subject: [PATCH 33/38] chore: [charts] Update webapp version (#3011) Co-authored-by: Zebot --- changelog.d/0-release-notes/webapp-upgrade | 1 + charts/webapp/values.yaml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/0-release-notes/webapp-upgrade diff --git a/changelog.d/0-release-notes/webapp-upgrade b/changelog.d/0-release-notes/webapp-upgrade new file mode 100644 index 0000000000..60904d9252 --- /dev/null +++ b/changelog.d/0-release-notes/webapp-upgrade @@ -0,0 +1 @@ +Upgrade webapp version to 2023-01-24-production.0-v0.31.9-0-17b742f diff --git a/charts/webapp/values.yaml b/charts/webapp/values.yaml index 8fd730fb42..015b109ada 100644 --- a/charts/webapp/values.yaml +++ b/charts/webapp/values.yaml @@ -9,7 +9,7 @@ resources: cpu: "1" image: repository: quay.io/wire/webapp - tag: "2022-12-19-production.0-v0.31.9-0-6b2f2bf" + tag: "2023-01-24-production.0-v0.31.9-0-17b742f" service: https: externalPort: 443 From 589a0274b03c5075ac3a3973fd962356ed1a9ceb Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Thu, 26 Jan 2023 11:32:01 +0100 Subject: [PATCH 34/38] Github Workflow: dont use environment for secrets (#3021) --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e1b40c398d..707b353c19 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,6 @@ on: jobs: treefmt: name: Run treefmt - environment: cachix # for secrets runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -25,7 +24,6 @@ jobs: build-docs: name: Build docs - environment: cachix runs-on: ubuntu-latest permissions: id-token: write From e345ca3f093fa84d2ecef9e7a6b3095628b5e544 Mon Sep 17 00:00:00 2001 From: Lucendio Date: Thu, 26 Jan 2023 12:58:34 +0100 Subject: [PATCH 35/38] charts/galley/configmap: remove duplicate feature flag searchVisibilityInbound (#3026) Accidentally introduced in https://github.com/wireapp/wire-server/pull/2494 --- charts/galley/templates/configmap.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/charts/galley/templates/configmap.yaml b/charts/galley/templates/configmap.yaml index a761fb24fd..9962452b0c 100644 --- a/charts/galley/templates/configmap.yaml +++ b/charts/galley/templates/configmap.yaml @@ -111,10 +111,6 @@ data: conversationGuestLinks: {{- toYaml .settings.featureFlags.conversationGuestLinks | nindent 10 }} {{- end }} - {{- if .settings.featureFlags.searchVisibilityInbound }} - searchVisibilityInbound: - {{- toYaml .settings.featureFlags.searchVisibilityInbound | nindent 10 }} - {{- end }} {{- if .settings.featureFlags.mls }} mls: {{- toYaml .settings.featureFlags.mls | nindent 10 }} From 0e16249f5b0cc8869afdf37a984295e4c6b0df6e Mon Sep 17 00:00:00 2001 From: jschaul Date: Thu, 26 Jan 2023 14:29:19 +0100 Subject: [PATCH 36/38] document helm chart upgrades after k8s version upgrades (#3019) * document helm chart upgrades after k8s version upgrades * Update docs/src/how-to/administrate/kubernetes/upgrade-cluster/index.md Co-authored-by: Leif Battermann --- .../0-release-notes/chart-compatibility | 17 ++++++++++++++- .../kubernetes/upgrade-cluster/index.md | 21 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/changelog.d/0-release-notes/chart-compatibility b/changelog.d/0-release-notes/chart-compatibility index 54bf8bf8d0..bd4607c00d 100644 --- a/changelog.d/0-release-notes/chart-compatibility +++ b/changelog.d/0-release-notes/chart-compatibility @@ -1 +1,16 @@ -wire-server helm charts using Ingress resources are now compatible with kubernetes versions 1.22, 1.23 and 1.24 (but keep being compatible with older versions of kubernetes). +wire-server helm charts using Ingress resources are now compatible with kubernetes versions 1.22, 1.23 and 1.24 (but remain compatible with older versions of kubernetes). + +If you upgrade to this version of helm charts and/or you upgrade your version of kubernetes while wire-server is deployed, you may find that `helm update` or `helmfile apply/sync` gives an error like this: + +> Error: UPGRADE FAILED: current release manifest contains removed kubernetes api(s) for this kubernetes version and it is therefore unable to build the kubernetes objects for performing the diff. error from kubernetes: unable to recognize "": no matches for kind "Ingress" in version "extensions/v1beta1" + +In which case you can use the [helm mapkubeapis plugin](https://github.com/helm/helm-mapkubeapis) to upgrade an existing release with the following command: + +```sh +# install plugin version 0.1.0 (more recent may not work) +helm plugin install --version v0.1.0 https://github.com/helm/helm-mapkubeapis +# adjust helm release name and namespace as required +helm mapkubeapis --namespace wire nginx-ingress-services +``` + +Alternatively, if a few minutes of downtime are not a problem; you can `helm delete` a release and re-install it again, which will work without the above plugin. diff --git a/docs/src/how-to/administrate/kubernetes/upgrade-cluster/index.md b/docs/src/how-to/administrate/kubernetes/upgrade-cluster/index.md index 739ae7ee2c..062e99ceb8 100644 --- a/docs/src/how-to/administrate/kubernetes/upgrade-cluster/index.md +++ b/docs/src/how-to/administrate/kubernetes/upgrade-cluster/index.md @@ -73,3 +73,24 @@ the components, and draining/uncordon nodes. ## Kubeadm Please refer to the *official documentation:* [Upgrading kubeadm clusters](https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/kubeadm-upgrade/) + +# Troubleshooting problems arising after kubernetes cluster upgrades + +## Helm and kubernetes API changes + +If you upgrade to new versions of kubernetes while wire-server is deployed, you may find that after that version update, deploying a new version of wire-server or nginx-ingress-services (or another helm chart we provide) using `helm update` or `helmfile apply/sync` gives an error like this: + +> Error: UPGRADE FAILED: current release manifest contains removed kubernetes api(s) for this kubernetes version and it is therefore unable to build the kubernetes objects for performing the diff. error from kubernetes: unable to recognize "": no matches for kind "Ingress" in version "extensions/v1beta1" + +What's happening here is that some [deprecated](https://kubernetes.io/docs/reference/using-api/deprecation-guide/) kubernetes API versions may potentially have been removed. While we strive to keep maximum compatibility of kubernetes versions in our helm charts, that's not sufficient when doing k8s upgrades while wire-server helm charts are in use: you need to tell a helm release about the difference in API version. + +In which case you can use the [helm mapkubeapis plugin](https://github.com/helm/helm-mapkubeapis) to upgrade an existing release with the following command: + +```sh +# install plugin version 0.1.0 (more recent may not work) +helm plugin install --version v0.1.0 https://github.com/helm/helm-mapkubeapis +# adjust helm release name and namespace as required +helm mapkubeapis --namespace wire nginx-ingress-services +``` + +Alternatively, if a few minutes of downtime are not a problem; you can `helm delete` a release and re-install it again, which will work without the above plugin. From 24d754cd641f6df36e9c4438347149b81ef18f95 Mon Sep 17 00:00:00 2001 From: Zebot Date: Thu, 26 Jan 2023 14:27:01 +0000 Subject: [PATCH 37/38] Add changelog for Release 2023-01-26 --- CHANGELOG.md | 77 +++++++++++++++++++ .../0-release-notes/chart-compatibility | 16 ---- .../0-release-notes/team-settings-upgrade | 1 - changelog.d/0-release-notes/webapp-upgrade | 1 - .../1-api-changes/get-conversation-v3-leak | 1 - changelog.d/3-bug-fixes/pr-2968 | 1 - .../update-inbucket-chart-dependency | 1 - changelog.d/4-docs/copybutton | 1 - .../4-docs/hook-fedcalls-into-api-docs | 1 - changelog.d/4-docs/pr-2973 | 1 - .../5-internal/k8ssandra-cluster-helm-chart | 1 - changelog.d/5-internal/kind-fix | 1 - .../5-internal/makefile-kind-restart-all | 1 - changelog.d/5-internal/mock-utilities | 1 - changelog.d/5-internal/pr-2694 | 1 - changelog.d/5-internal/pr-2965 | 1 - changelog.d/5-internal/pr-2991 | 1 - changelog.d/5-internal/pr-3017 | 1 - changelog.d/5-internal/refactor-writetime | 1 - changelog.d/5-internal/restructure-docs | 1 - changelog.d/5-internal/sqservices-1848 | 1 - 21 files changed, 77 insertions(+), 35 deletions(-) delete mode 100644 changelog.d/0-release-notes/chart-compatibility delete mode 100644 changelog.d/0-release-notes/team-settings-upgrade delete mode 100644 changelog.d/0-release-notes/webapp-upgrade delete mode 100644 changelog.d/1-api-changes/get-conversation-v3-leak delete mode 100644 changelog.d/3-bug-fixes/pr-2968 delete mode 100644 changelog.d/3-bug-fixes/update-inbucket-chart-dependency delete mode 100644 changelog.d/4-docs/copybutton delete mode 100644 changelog.d/4-docs/hook-fedcalls-into-api-docs delete mode 100644 changelog.d/4-docs/pr-2973 delete mode 100644 changelog.d/5-internal/k8ssandra-cluster-helm-chart delete mode 100644 changelog.d/5-internal/kind-fix delete mode 100644 changelog.d/5-internal/makefile-kind-restart-all delete mode 100644 changelog.d/5-internal/mock-utilities delete mode 100644 changelog.d/5-internal/pr-2694 delete mode 100644 changelog.d/5-internal/pr-2965 delete mode 100644 changelog.d/5-internal/pr-2991 delete mode 100644 changelog.d/5-internal/pr-3017 delete mode 100644 changelog.d/5-internal/refactor-writetime delete mode 100644 changelog.d/5-internal/restructure-docs delete mode 100644 changelog.d/5-internal/sqservices-1848 diff --git a/CHANGELOG.md b/CHANGELOG.md index c35bd5ca66..236cd9db33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,80 @@ +# [2023-01-26] (Chart Release 4.31.0) + +## Release notes + + +* wire-server helm charts using Ingress resources are now compatible with kubernetes versions 1.22, 1.23 and 1.24 (but remain compatible with older versions of kubernetes). + + If you upgrade to this version of helm charts and/or you upgrade your version of kubernetes while wire-server is deployed, you may find that `helm update` or `helmfile apply/sync` gives an error like this: + + > Error: UPGRADE FAILED: current release manifest contains removed kubernetes api(s) for this kubernetes version and it is therefore unable to build the kubernetes objects for performing the diff. error from kubernetes: unable to recognize "": no matches for kind "Ingress" in version "extensions/v1beta1" + + In which case you can use the [helm mapkubeapis plugin](https://github.com/helm/helm-mapkubeapis) to upgrade an existing release with the following command: + + ```sh + # install plugin version 0.1.0 (more recent may not work) + helm plugin install --version v0.1.0 https://github.com/helm/helm-mapkubeapis + # adjust helm release name and namespace as required + helm mapkubeapis --namespace wire nginx-ingress-services + ``` + + Alternatively, if a few minutes of downtime are not a problem; you can `helm delete` a release and re-install it again, which will work without the above plugin. (#3002) + +* Upgrade team-settings version to 4.14.0-v0.31.9-0-bf82b46 (#2180) + +* Upgrade webapp version to 2023-01-24-production.0-v0.31.9-0-17b742f (#2302) + + +## API changes + + +* The unqualified `GET /conversations/:id` endpoint has been removed from API v3, and is restored to the previous behaviour of returning a Conversation using the v2 schema. Similarly, its qualified counterpart `GET /conversations/:domain/:id` now returns a v2 Conversation when accessed through API v2. (#2992) + + +## Bug fixes and other updates + + +* Fix pagination in team user search (make search key unique) (#2968) + +* Update `inbucket` (fake smtp server) chart dependency: The prior version relied on an image that has been removed from docker hub. Thus, our own `inbucket` chart could not be deployed anymore. (#2998) + + +## Documentation + + +* Add sphinx-copybutton plugin to make copying snippets of code from docs.wire.com easier. (#PR_NOT_FOUND) + +* Hook federated API call documentation into docs.wire.com (manually). (#2988) + +* Tool for dumping fed call graphs (dot/graphviz and csv); see README for details (#2973) + + +## Internal changes + + +* Add Helm chart to configure clusters managed by k8ssandra-operator for test environments. (#2981) + +* Fix kind setup for running end-to-end federation tests locally. (#3008) + +* Fix Makefile target kind-restart-all. (#3015) + +* Add combinators for creating mocked federator responses in integration tests (#3014) + +* Add two integration tests arounds last prekeys (#2694) + +* Fix `make clean` (#2965, #2978) + +* Make ID tags more readable by expanding abbreviations to full names. (#2991) + +* Unused old swagger code removed from stern and team features (#3017) + +* Refactor Writetime from Int64 to wrapper of UTCTime (#2994) + +* Restructure docs.wire.com (#2986) + +* Fixed flaky team user search integration test (#2996) + + # [2023-01-12] (Chart Release 4.30.0) ## Release notes diff --git a/changelog.d/0-release-notes/chart-compatibility b/changelog.d/0-release-notes/chart-compatibility deleted file mode 100644 index bd4607c00d..0000000000 --- a/changelog.d/0-release-notes/chart-compatibility +++ /dev/null @@ -1,16 +0,0 @@ -wire-server helm charts using Ingress resources are now compatible with kubernetes versions 1.22, 1.23 and 1.24 (but remain compatible with older versions of kubernetes). - -If you upgrade to this version of helm charts and/or you upgrade your version of kubernetes while wire-server is deployed, you may find that `helm update` or `helmfile apply/sync` gives an error like this: - -> Error: UPGRADE FAILED: current release manifest contains removed kubernetes api(s) for this kubernetes version and it is therefore unable to build the kubernetes objects for performing the diff. error from kubernetes: unable to recognize "": no matches for kind "Ingress" in version "extensions/v1beta1" - -In which case you can use the [helm mapkubeapis plugin](https://github.com/helm/helm-mapkubeapis) to upgrade an existing release with the following command: - -```sh -# install plugin version 0.1.0 (more recent may not work) -helm plugin install --version v0.1.0 https://github.com/helm/helm-mapkubeapis -# adjust helm release name and namespace as required -helm mapkubeapis --namespace wire nginx-ingress-services -``` - -Alternatively, if a few minutes of downtime are not a problem; you can `helm delete` a release and re-install it again, which will work without the above plugin. diff --git a/changelog.d/0-release-notes/team-settings-upgrade b/changelog.d/0-release-notes/team-settings-upgrade deleted file mode 100644 index 1168c3ea44..0000000000 --- a/changelog.d/0-release-notes/team-settings-upgrade +++ /dev/null @@ -1 +0,0 @@ -Upgrade team-settings version to 4.14.0-v0.31.9-0-bf82b46 diff --git a/changelog.d/0-release-notes/webapp-upgrade b/changelog.d/0-release-notes/webapp-upgrade deleted file mode 100644 index 60904d9252..0000000000 --- a/changelog.d/0-release-notes/webapp-upgrade +++ /dev/null @@ -1 +0,0 @@ -Upgrade webapp version to 2023-01-24-production.0-v0.31.9-0-17b742f diff --git a/changelog.d/1-api-changes/get-conversation-v3-leak b/changelog.d/1-api-changes/get-conversation-v3-leak deleted file mode 100644 index 8f5f313314..0000000000 --- a/changelog.d/1-api-changes/get-conversation-v3-leak +++ /dev/null @@ -1 +0,0 @@ -The unqualified `GET /conversations/:id` endpoint has been removed from API v3, and is restored to the previous behaviour of returning a Conversation using the v2 schema. Similarly, its qualified counterpart `GET /conversations/:domain/:id` now returns a v2 Conversation when accessed through API v2. diff --git a/changelog.d/3-bug-fixes/pr-2968 b/changelog.d/3-bug-fixes/pr-2968 deleted file mode 100644 index e32c978a07..0000000000 --- a/changelog.d/3-bug-fixes/pr-2968 +++ /dev/null @@ -1 +0,0 @@ -Fix pagination in team user search (make search key unique) diff --git a/changelog.d/3-bug-fixes/update-inbucket-chart-dependency b/changelog.d/3-bug-fixes/update-inbucket-chart-dependency deleted file mode 100644 index 63ae2c40a9..0000000000 --- a/changelog.d/3-bug-fixes/update-inbucket-chart-dependency +++ /dev/null @@ -1 +0,0 @@ -Update `inbucket` (fake smtp server) chart dependency: The prior version relied on an image that has been removed from docker hub. Thus, our own `inbucket` chart could not be deployed anymore. diff --git a/changelog.d/4-docs/copybutton b/changelog.d/4-docs/copybutton deleted file mode 100644 index cef39536e5..0000000000 --- a/changelog.d/4-docs/copybutton +++ /dev/null @@ -1 +0,0 @@ -Add sphinx-copybutton plugin to make copying snippets of code from docs.wire.com easier. diff --git a/changelog.d/4-docs/hook-fedcalls-into-api-docs b/changelog.d/4-docs/hook-fedcalls-into-api-docs deleted file mode 100644 index 2a6d02a8a3..0000000000 --- a/changelog.d/4-docs/hook-fedcalls-into-api-docs +++ /dev/null @@ -1 +0,0 @@ -Hook federated API call documentation into docs.wire.com (manually). diff --git a/changelog.d/4-docs/pr-2973 b/changelog.d/4-docs/pr-2973 deleted file mode 100644 index 89fbeb8be6..0000000000 --- a/changelog.d/4-docs/pr-2973 +++ /dev/null @@ -1 +0,0 @@ -Tool for dumping fed call graphs (dot/graphviz and csv); see README for details \ No newline at end of file diff --git a/changelog.d/5-internal/k8ssandra-cluster-helm-chart b/changelog.d/5-internal/k8ssandra-cluster-helm-chart deleted file mode 100644 index ce269c8ef6..0000000000 --- a/changelog.d/5-internal/k8ssandra-cluster-helm-chart +++ /dev/null @@ -1 +0,0 @@ -Add Helm chart to configure clusters managed by k8ssandra-operator for test environments. diff --git a/changelog.d/5-internal/kind-fix b/changelog.d/5-internal/kind-fix deleted file mode 100644 index 2b7d9f1b3b..0000000000 --- a/changelog.d/5-internal/kind-fix +++ /dev/null @@ -1 +0,0 @@ -Fix kind setup for running end-to-end federation tests locally. \ No newline at end of file diff --git a/changelog.d/5-internal/makefile-kind-restart-all b/changelog.d/5-internal/makefile-kind-restart-all deleted file mode 100644 index 899efa0d8e..0000000000 --- a/changelog.d/5-internal/makefile-kind-restart-all +++ /dev/null @@ -1 +0,0 @@ -Fix Makefile target kind-restart-all. diff --git a/changelog.d/5-internal/mock-utilities b/changelog.d/5-internal/mock-utilities deleted file mode 100644 index 1c08e75164..0000000000 --- a/changelog.d/5-internal/mock-utilities +++ /dev/null @@ -1 +0,0 @@ -Add combinators for creating mocked federator responses in integration tests diff --git a/changelog.d/5-internal/pr-2694 b/changelog.d/5-internal/pr-2694 deleted file mode 100644 index 7bd6984179..0000000000 --- a/changelog.d/5-internal/pr-2694 +++ /dev/null @@ -1 +0,0 @@ -Add two integration tests arounds last prekeys diff --git a/changelog.d/5-internal/pr-2965 b/changelog.d/5-internal/pr-2965 deleted file mode 100644 index 9dffecdd84..0000000000 --- a/changelog.d/5-internal/pr-2965 +++ /dev/null @@ -1 +0,0 @@ -Fix `make clean` (#2965, #2978) diff --git a/changelog.d/5-internal/pr-2991 b/changelog.d/5-internal/pr-2991 deleted file mode 100644 index 150d50cf30..0000000000 --- a/changelog.d/5-internal/pr-2991 +++ /dev/null @@ -1 +0,0 @@ -Make ID tags more readable by expanding abbreviations to full names. diff --git a/changelog.d/5-internal/pr-3017 b/changelog.d/5-internal/pr-3017 deleted file mode 100644 index 7d9245d71d..0000000000 --- a/changelog.d/5-internal/pr-3017 +++ /dev/null @@ -1 +0,0 @@ -Unused old swagger code removed from stern and team features diff --git a/changelog.d/5-internal/refactor-writetime b/changelog.d/5-internal/refactor-writetime deleted file mode 100644 index fb0f680472..0000000000 --- a/changelog.d/5-internal/refactor-writetime +++ /dev/null @@ -1 +0,0 @@ -Refactor Writetime from Int64 to wrapper of UTCTime diff --git a/changelog.d/5-internal/restructure-docs b/changelog.d/5-internal/restructure-docs deleted file mode 100644 index f9c770a9cc..0000000000 --- a/changelog.d/5-internal/restructure-docs +++ /dev/null @@ -1 +0,0 @@ -Restructure docs.wire.com diff --git a/changelog.d/5-internal/sqservices-1848 b/changelog.d/5-internal/sqservices-1848 deleted file mode 100644 index ed89235a80..0000000000 --- a/changelog.d/5-internal/sqservices-1848 +++ /dev/null @@ -1 +0,0 @@ -Fixed flaky team user search integration test From 13640b5d89a114ac8172a078b9dd3a01680abdb0 Mon Sep 17 00:00:00 2001 From: Sven Tennie Date: Thu, 26 Jan 2023 15:38:43 +0100 Subject: [PATCH 38/38] Fix CHANGELOG.md Add missing pull request (PR) id. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 236cd9db33..f5014c421e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,7 +42,7 @@ ## Documentation -* Add sphinx-copybutton plugin to make copying snippets of code from docs.wire.com easier. (#PR_NOT_FOUND) +* Add sphinx-copybutton plugin to make copying snippets of code from docs.wire.com easier. (#2900) * Hook federated API call documentation into docs.wire.com (manually). (#2988)