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 diff --git a/CHANGELOG.md b/CHANGELOG.md index c35bd5ca66..f5014c421e 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. (#2900) + +* 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/Makefile b/Makefile index df625369b5..28ba398972 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 @@ -47,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: @@ -437,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 @@ -479,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/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/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/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 }} 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 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/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 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 }} 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 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 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/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..2016f223c2 --- /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/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 e7c36d04e6..c91faf4b90 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 @@ -113,6 +114,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)$' @@ -128,6 +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/main.html#setting-up-sso-externally", - "how-to/scim/index": "../../understand/single-sign-on/main.html#user-provisioning", + "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/developer/how-to.md b/docs/src/developer/developer/how-to.md index 14c0e278d9..68250bba21 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 @@ -113,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/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/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/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..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](../../understand/single-sign-on/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/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..79a04fa649 --- /dev/null +++ b/docs/src/how-to/administrate/index.md @@ -0,0 +1,12 @@ +# Administration + +```{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..062e99ceb8 --- /dev/null +++ b/docs/src/how-to/administrate/kubernetes/upgrade-cluster/index.md @@ -0,0 +1,96 @@ +# 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/) + +# 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. 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.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/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.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.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.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..b45b694832 --- /dev/null +++ b/docs/src/how-to/install/index.md @@ -0,0 +1,30 @@ +# Installation + +```{toctree} +:glob: true +:maxdepth: 2 + +How to plan an installation +Version requirements +dependencies + +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 +tls +Managing authentication with ansible +Using tinc +Troubleshooting during installation +Verifying your 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/infrastructure-configuration.md b/docs/src/how-to/install/infrastructure-configuration.md new file mode 100644 index 0000000000..0e9d9a0029 --- /dev/null +++ b/docs/src/how-to/install/infrastructure-configuration.md @@ -0,0 +1,679 @@ +(configuration-options)= + +# Infrastructure configuration options + +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 +``` + +## 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 *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 +``` + +## 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/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/post-install.md b/docs/src/how-to/install/post-install.md new file mode 100644 index 0000000000..6a513f0ece --- /dev/null +++ b/docs/src/how-to/install/post-install.md @@ -0,0 +1,132 @@ +# 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 + +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/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.rst deleted file mode 100644 index 4a7420aa23..0000000000 --- a/docs/src/how-to/post-install/index.rst +++ /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:: - :maxdepth: 1 - :glob: - - Verifying NTP - Verifying data retention for logs don't exceed 72 hours 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.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.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.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/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/index.md b/docs/src/index.md new file mode 100644 index 0000000000..bb8d35a63b --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,45 @@ +% 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. + +Release notes of `wire-server` can be found [here](https://github.com/wireapp/wire-server/releases). + +```{toctree} +:caption: 'Contents:' +:glob: true +:maxdepth: 1 + +Security responses +Release Notes +Installation +Administration +Reference +Developers Notes +``` + +% 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.rst deleted file mode 100644 index 497af3cca3..0000000000 --- a/docs/src/release-notes.rst +++ /dev/null @@ -1,14 +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 `_ - -→ Look at releases on right hand side. They are shown by date of release. `Release Notes `_ - -→ Open the CHANGELOG.md. This will give you chart version. \ No newline at end of file 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..e86c0de1f2 --- /dev/null +++ b/docs/src/understand/api-client-perspective/index.md @@ -0,0 +1,11 @@ +# 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. + +```{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.md b/docs/src/understand/api-client-perspective/swagger.md new file mode 100644 index 0000000000..5722c95aed --- /dev/null +++ b/docs/src/understand/api-client-perspective/swagger.md @@ -0,0 +1,24 @@ +# Swagger API documentation (all public endpoints) + +Our staging system provides swagger documentation of our public rest +API. + +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 (swagger 2.0) + +[new staging swagger page](https://staging-nginz-https.zinfra.io/api/swagger-ui/) + +## Old docs (swagger 1.2) + +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. + +```{image} img/swagger.png +``` diff --git a/docs/src/understand/api-client-perspective/swagger.rst b/docs/src/understand/api-client-perspective/swagger.rst deleted file mode 100644 index 5dd3d29e36..0000000000 --- a/docs/src/understand/api-client-perspective/swagger.rst +++ /dev/null @@ -1,31 +0,0 @@ -Swagger API documentation (all public endpoints) -================================================ - -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. - -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 staging swagger page `_ - - -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. - -.. image:: img/swagger.png diff --git a/docs/src/understand/associate/custom-backend-for-desktop-client.md b/docs/src/understand/associate/custom-backend-for-desktop-client.md new file mode 100644 index 0000000000..ad66ca975e --- /dev/null +++ b/docs/src/understand/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-certificates.rst b/docs/src/understand/associate/custom-certificates.md similarity index 80% rename from docs/src/how-to/associate/custom-certificates.rst rename to docs/src/understand/associate/custom-certificates.md index 3a5c15b852..2c52391570 100644 --- a/docs/src/how-to/associate/custom-certificates.rst +++ b/docs/src/understand/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/understand/associate/deeplink.md b/docs/src/understand/associate/deeplink.md new file mode 100644 index 0000000000..4c9af0ea21 --- /dev/null +++ b/docs/src/understand/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 ./values/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/understand/associate/index.md b/docs/src/understand/associate/index.md new file mode 100644 index 0000000000..3dba99c8f2 --- /dev/null +++ b/docs/src/understand/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/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 83% rename from docs/src/how-to/install/configure-federation.md rename to docs/src/understand/configure-federation.md index 38d53af30f..6d0042eaad 100644 --- a/docs/src/how-to/install/configure-federation.md +++ b/docs/src/understand/configure-federation.md @@ -1,20 +1,12 @@ (configure-federation)= -# Configure Wire-Server for Federation +# 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 @@ -26,28 +18,28 @@ detailed in the sections below: - 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, `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 infrastructure 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 @@ -245,14 +237,14 @@ 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 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. @@ -274,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): @@ -312,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 @@ -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): @@ -522,10 +515,10 @@ 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\" +### Manually test that federation works Prerequisites: diff --git a/docs/src/understand/federation/api.md b/docs/src/understand/federation/api.md index a11e38397d..7b576d9234 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 {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 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,48 +175,65 @@ 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 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. @@ -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..6bb1e782bc 100644 --- a/docs/src/understand/federation/architecture.md +++ b/docs/src/understand/federation/architecture.md @@ -1,87 +1,71 @@ -(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 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. -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 -internal backend components to other, remote backends. +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** 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 infrastructure 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. 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. 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 @@ -89,21 +73,18 @@ 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. 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 +93,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 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 {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 +115,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..a71c6e158b --- /dev/null +++ b/docs/src/understand/federation/backend-communication.md @@ -0,0 +1,155 @@ +(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 (infrastructure 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 +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. + +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 infrastructure 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 the backend domain of that user's backend. +- When receiving a message from another backend that authenticates + 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 infrastructure domain to run their Wire backend. + +To make discovery possible, any party hosting a Wire backend has to +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 infrastructure domain. + +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. +``` + +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)= + + +(srv-ttl-and-caching)= + +### SRV TTL and Caching + +After retrieving the SRV record for a given domain, the local backend +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 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 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 infrastructure 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 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 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`. + +(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 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 +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/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/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 0000000000..faf81be988 Binary files /dev/null and b/docs/src/understand/federation/img/federation-apis-flow.png differ 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 f6558c63df..25a0014e24 100644 Binary files a/docs/src/understand/federation/img/federation-flow.png and b/docs/src/understand/federation/img/federation-flow.png differ diff --git a/docs/src/understand/federation/img/federation-flow.txt b/docs/src/understand/federation/img/federation-flow.txt index e4723c11cb..c9c4be4bd2 100644 --- a/docs/src/understand/federation/img/federation-flow.txt +++ b/docs/src/understand/federation/img/federation-flow.txt @@ -1,10 +1,19 @@ title: Federator to Ingress/Federator Flow -Brig @infra.a.com -> 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/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 0000000000..35fb0ed9d2 Binary files /dev/null and b/docs/src/understand/federation/img/wire-fedcalls.png differ diff --git a/docs/src/understand/federation/index.md b/docs/src/understand/federation/index.md new file mode 100644 index 0000000000..a1dc6b6cfa --- /dev/null +++ b/docs/src/understand/federation/index.md @@ -0,0 +1,30 @@ +(federation-understand)= + +# Wire Federation + +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. + +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`. + +```{note} +The Federation development is work in progress. +``` + +```{toctree} +--- +maxdepth: 2 +numbered: true +glob: true +--- +architecture +backend-communication +* +``` 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/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 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/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 new file mode 100644 index 0000000000..dd9474bceb --- /dev/null +++ b/docs/src/understand/index.md @@ -0,0 +1,19 @@ +(understand)= + +# Reference + +```{toctree} +:glob: true +:maxdepth: 2 + +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/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/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/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/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/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..6926a81280 --- /dev/null +++ b/docs/src/understand/overview.md @@ -0,0 +1,143 @@ +(overview)= + +# Architecture 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/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/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/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/understand/single-sign-on/adfs/main.md b/docs/src/understand/single-sign-on/adfs/main.md new file mode 100644 index 0000000000..2e48fd531b --- /dev/null +++ b/docs/src/understand/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/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/understand/single-sign-on/azure/main.md b/docs/src/understand/single-sign-on/azure/main.md new file mode 100644 index 0000000000..dbd0338907 --- /dev/null +++ b/docs/src/understand/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/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.rst b/docs/src/understand/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/understand/single-sign-on/centrify/main.md index 12d6d530fd..ed88ba6668 100644 --- a/docs/src/how-to/single-sign-on/centrify/main.rst +++ b/docs/src/understand/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/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/generic-setup.md b/docs/src/understand/single-sign-on/generic-setup.md new file mode 100644 index 0000000000..d455899cca --- /dev/null +++ b/docs/src/understand/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/understand/single-sign-on/index.md b/docs/src/understand/single-sign-on/index.md new file mode 100644 index 0000000000..01317c99b5 --- /dev/null +++ b/docs/src/understand/single-sign-on/index.md @@ -0,0 +1,17 @@ +(sso-main-documentation)= + +# 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/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 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.rst b/docs/src/understand/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/understand/single-sign-on/okta/main.md index faf3799db0..6fe285c55f 100644 --- a/docs/src/how-to/single-sign-on/okta/main.rst +++ b/docs/src/understand/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/understand/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/understand/single-sign-on/trouble-shooting.md index da0ca43210..cdc7e1204a 100644 --- a/docs/src/how-to/single-sign-on/trouble-shooting.rst +++ b/docs/src/understand/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/understand/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/understand/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/understand/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/understand/single-sign-on/understand/Wire_SAML_Flow.png diff --git a/docs/src/understand/single-sign-on/understand/main.md b/docs/src/understand/single-sign-on/understand/main.md new file mode 100644 index 0000000000..465ef9dc48 --- /dev/null +++ b/docs/src/understand/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/understand/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/understand/single-sign-on/understand/token-step-01.png diff --git a/docs/src/understand/single-sign-on/token-step-02.png b/docs/src/understand/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/understand/single-sign-on/understand/token-step-02.png diff --git a/docs/src/understand/single-sign-on/token-step-03.png b/docs/src/understand/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/understand/single-sign-on/understand/token-step-03.png diff --git a/docs/src/understand/single-sign-on/token-step-04.png b/docs/src/understand/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/understand/single-sign-on/understand/token-step-04.png diff --git a/docs/src/understand/single-sign-on/token-step-05.png b/docs/src/understand/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/understand/single-sign-on/understand/token-step-05.png diff --git a/docs/src/understand/single-sign-on/token-step-06.png b/docs/src/understand/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/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/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/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/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 ------------------------------------------------------------------------- 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/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 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/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/nix/default.nix b/nix/default.nix index 34f37250e0..c377bf7102 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 ])) @@ -48,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/ 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/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" 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/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/src/Brig/User/Search/Index.hs b/services/brig/src/Brig/User/Search/Index.hs index 8dfc420437..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. @@ -62,6 +61,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 +73,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 +84,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 +772,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 +828,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 +854,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 +865,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/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/Internal.hs b/services/brig/test/integration/API/Internal.hs index c8bc85804b..f3fbeae676 100644 --- a/services/brig/test/integration/API/Internal.hs +++ b/services/brig/test/integration/API/Internal.hs @@ -28,7 +28,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 +79,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 +373,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/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/TeamUserSearch.hs b/services/brig/test/integration/API/TeamUserSearch.hs index ef4ab14088..10e762aa89 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 Nothing Nothing (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 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/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)) >= 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..9e2b99dc8d 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -687,7 +687,13 @@ defNewClientWithVerificationCode mbCode ty pks lpk = newClientVerificationCode = mbCode } -getPreKey :: Brig -> 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" @@ -710,13 +716,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/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/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/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/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index f193cbfee6..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,8 @@ 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 import qualified Wire.API.Team.Member as Teams import Wire.API.User @@ -125,6 +124,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 +270,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) @@ -315,10 +337,11 @@ 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]} 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) @@ -689,23 +714,30 @@ postMessageQualifiedLocalOwningBackendSuccess = do ] let mkPubClient c = PubClient c Nothing - brigApi d = - mkHandler @(FedApi 'Brig) $ - Named @"get-user-clients" $ \_ _ -> - 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 @@ -807,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 @@ -885,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 = @@ -965,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 @@ -987,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 @@ -1006,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 @@ -1027,8 +1048,7 @@ postMessageQualifiedLocalOwningBackendIgnoreMissingClients = do message "data" (Message.MismatchReportOnly (Set.fromList [chadOwningDomain])) - brigApi - galleyApi + mock pure resp2 !!! do const 412 === statusCode let expectedMissing = @@ -1048,8 +1068,7 @@ postMessageQualifiedLocalOwningBackendIgnoreMissingClients = do message "data" (Message.MismatchReportOnly (Set.fromList [deeRemote])) - brigApi - galleyApi + mock pure resp2 !!! do const 412 === statusCode let expectedMissing = @@ -1100,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 $ @@ -1138,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 @@ -1181,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 @@ -1539,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) @@ -2141,14 +2149,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 +2230,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 +2294,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 () @@ -2389,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 @@ -2432,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 @@ -2510,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 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 +2697,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 +2720,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 mockReply () (respDel, fedRequests) <- - withTempMockFederator mockReturnEve $ + withTempMockFederator' mockReturnEve $ deleteMemberQualified alice qBob qconvId let [galleyFederatedRequest] = fedRequestsForDomain remoteDomain Galley fedRequests assertRemoveUpdate galleyFederatedRequest qconvId qAlice [qUnqualified qEve] qBob @@ -2916,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 @@ -2939,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 @@ -2971,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 $ @@ -3002,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 @@ -3381,7 +3383,7 @@ putMemberOk update = do assertEqual "hidden_ref" (mupHiddenRef update) (misHiddenRef mis) x -> assertFailure $ "Unexpected event data: " ++ show x -- Verify new member state - rs <- getConv bob conv responseJsonUnsafe rs liftIO $ do assertBool "user" (isJust bob') @@ -3466,8 +3468,8 @@ putRemoteConvMemberOk update = do [localMemberToOther remoteDomain bobAsLocal] remoteConversationResponse = GetConversationsResponse [mockConversation] (rs, _) <- - withTempMockFederator - (const remoteConversationResponse) + withTempMockFederator' + (mockReply remoteConversationResponse) $ getConvQualified alice qconv 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 +3513,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 @@ -3590,10 +3592,10 @@ putRemoteReceiptModeOk = do cuAction = SomeConversationAction (sing @'ConversationReceiptModeUpdateTag) action } - let mockResponse = const (ConversationUpdateResponseUpdate responseConvUpdate) + let mockResponse = mockReply (ConversationUpdateResponseUpdate responseConvUpdate) WS.bracketR c adam $ \wsAdam -> 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 @@ -3775,11 +3777,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 +3791,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 +3828,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 = @@ -3861,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 @@ -3912,12 +3912,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 +3925,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 +3933,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 @@ -4011,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)) @@ -4057,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 @@ -4083,7 +4083,7 @@ updateTypingIndicatorToRemoteUserRemoteConv = do -- stopped void $ - withTempMockFederator (const ()) $ do + withTempMockFederator' (mockReply ()) $ do -- post typing indicator from bob to alice let tcReq = TypingDataUpdateRequest @@ -4119,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 @@ -4138,7 +4138,7 @@ updateTypingIndicatorFromRemoteUser = do -- stopped void $ - withTempMockFederator (const ()) $ do + withTempMockFederator' (mockReply ()) $ do -- post typing indicator from bob to alice let tcReq = TypingDataUpdateRequest @@ -4174,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 @@ -4193,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 23bb350eed..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,18 +1051,17 @@ 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 = @@ -235,8 +234,8 @@ postMLSConvOk = do pure rsp !!! do const 201 === statusCode const Nothing === fmap Wai.label . responseJsonError - cid <- assertConv rsp RegularConv alice qalice [] (Just nameMaxSize) Nothing - checkConvCreateEvent cid wsA + qcid <- assertConv rsp RegularConv alice qalice [] (Just nameMaxSize) Nothing + checkConvCreateEvent (qUnqualified qcid) wsA testSenderNotInConversation :: TestM () testSenderNotInConversation = do @@ -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 3ff97b0061..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 @@ -72,75 +72,71 @@ messageTimerInit :: TestM () messageTimerInit mtimer = do -- Create a conversation with a timer - [alice, bob, jane] <- randomUsers 3 - qAlice <- Qualified alice <$> 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 do (_, requests) <- - withTempMockFederator (const ()) $ + withTempMockFederator' (mockReply ()) $ putMessageTimerUpdateQualified bob qconv (ConversationMessageTimerUpdate timer1sec) !!! const 200 === statusCode @@ -186,15 +182,16 @@ messageTimerChangeWithoutAllowedAction :: TestM () messageTimerChangeWithoutAllowedAction = do -- Create a team and a guest user (tid, owner, member : _) <- createBindingTeamWithMembers 2 - guest <- randomUser + (guest, qguest) <- randomUserTuple connectUsers owner (list1 guest []) -- Create a conversation cid <- createTeamConvWithRole owner tid [member, guest] Nothing Nothing Nothing roleNameWireMember + let qcid = qguest $> 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..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 @@ -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 @@ -176,7 +175,7 @@ roleUpdateRemoteMember = do WS.bracketR c bob $ \wsB -> do (_, requests) <- - withTempMockFederator (const ()) $ + withTempMockFederator' (mockReply ()) $ putOtherMemberQualified bob qcharlie @@ -245,7 +244,7 @@ roleUpdateWithRemotes = do WS.bracketR2 c bob charlie $ \(wsB, wsC) -> do (_, requests) <- - withTempMockFederator (const ()) $ + withTempMockFederator' (mockReply ()) $ putOtherMemberQualified bob qcharlie @@ -304,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/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..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 :: @@ -1033,12 +1032,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 ["conversations", toByteString' c] + . 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 + [ "v2", + "conversations", + toByteString' (qDomain qcnv), + toByteString' (qUnqualified qcnv) + ] . zUser u . zConn "conn" . zType "access" @@ -1531,61 +1561,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 +1577,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 => @@ -2589,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 :: @@ -2789,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 = 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 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 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..bb62be4be4 --- /dev/null +++ b/tools/fedcalls/README.md @@ -0,0 +1,38 @@ +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 + +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 + +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 + +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 0000000000..26bc63134f Binary files /dev/null and b/tools/fedcalls/example.png differ 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] 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