diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index dc8abfb10d..0e87493471 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,5 +1,4 @@ ## Checklist - [ ] Add a new entry in an appropriate subdirectory of `changelog.d` - - [ ] Read and follow the -[PR guidelines](https://github.com/wireapp/wire-server/blob/develop/docs/developer/pr-guidelines.md) + - [ ] Read and follow the [PR guidelines](https://docs.wire.com/developer/developer/pr-guidelines.html) diff --git a/.gitignore b/.gitignore index 47112aa370..a70cae14b5 100644 --- a/.gitignore +++ b/.gitignore @@ -116,3 +116,6 @@ result-* # emacs misc .dir-locals.el + +/integration-ca-key.pem +/integration-ca.pem diff --git a/.hlint.yaml b/.hlint.yaml index ad6303e32d..805c927209 100644 --- a/.hlint.yaml +++ b/.hlint.yaml @@ -8,8 +8,13 @@ # Left for the programmer to decide. See discussion at https://github.com/wireapp/wire-server/pull/2382#discussion_r871194424 - ignore: { name: Avoid lambda } - ignore: { name: Avoid lambda using `infix` } - +- ignore: { name: Eta reduce } - ignore: { name: Use section } +- ignore: { name: Use underscore } + # custom rules: - hint: { lhs: (() <$), rhs: void } - hint: { lhs: return, rhs: pure } +## We want the latter to properly handle signals. +- error: { name: Use shutdown, lhs: runSettings, rhs: runSettingsWithShutdown } +- ignore: { name: Use shutdown, within: [Network.Wai.Utilities.Server, Federator.Response] } diff --git a/CHANGELOG.md b/CHANGELOG.md index ed7e44be1d..be8dd8cbdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,116 @@ +# [2022-09-27] (Chart Release 4.24.0) + +## Release notes + + +* For users of the (currently alpha) coturn Helm chart, **manual action is + required** when upgrading to this version. The labels applied to the Kubernetes + manifests in this chart have changed, in order to match the conventions used + in the wire-server charts. However, this may mean that upgrading with Helm can + fail, due to changes to the `StatefulSet` included in this chart -- in this + case, the `StatefulSet` must be deleted before the chart is upgraded. (#2677) + +* wire-server helm charts: Adjust default CPU/Memory resources: Remove CPU limits to avoid CPU throttling; adjust request CPU and memory based on observed values. Overall this decreases the amount of CPU/memory that the wire-server chart needs to install/schedule pods. (#2675) + +* Upgrade team-settings version to 4.12.1-v0.31.5-0-0167ea4 (#2180) + +* Upgrade webapp version to 2022-09-20-production.0-v0.31.2-0-7f74074 (#2302) + + +## API changes + + +* Add new endpoint `/mls/commit-bundles` for submitting MLS `CommitBundle`s. A `CommitBundle` is a triple consisting of a commit message, an optional welcome message and a public group state. (#2688) + +* MLS: Store and expose group info via `GET /conversations/:domain/:id/groupinfo` (#2721) + +* Add /mls/public-keys to nginz chart (#2676) + +* Users being kicked out results in member-leave events originating from the user who caused the change in the conversation (#2724) + +* Leaving an MLS conversation is now possible using the regular endpoint `DELETE /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}`. When a user leaves, the backend sends external remove proposals for all their clients in the corresponding MLS group. (#2667) + +* Validate remotely claimed key packages (#2692) + + +## Features + + +* The coturn chart now has support for exposing its metric endpoint with a + ServiceMonitor, which can be ingested by third-party metrics collection tools. (#2677) + +* Deleting clients creates MLS remove proposals (#2674) + +* External remove proposals are now sent to a group when a user is deleted (#2650) + +* Allow non-admins to commit add proposals in MLS conversations (#2691) + +* Optionally add invitation urls to the body of `/teams/{tid}/invitations`. This allows further processing; e.g. to send those links with custom emails or distribute them as QR codes. See [docs](https://docs.wire.com/developer/reference/config-options.html#expose-invitation-urls-to-team-admin) for details and privacy implications. (#2684) + + +## Bug fixes and other updates + + +* SCIM user deletion suffered from a couple of race conditions. The user in now first deleted in spar, because this process depends on data from brig. Then, the user is deleted in brig. If any error occurs, the SCIM deletion request can be made again. This change depends on brig being completely deployed before using the SCIM deletion endpoint in brig. In the unlikely event of using SCIM deletion during the deployment, these requests can be retried (in case of error). (#2637) + +* The 2nd factor password challenge team feature is disabled for SSO users (#2693) + +* Less surprising handling of SIGINT, SIGTERM for proxy, stern. Increase grace period for shutdown from 5s to 30s for all services. (#2715) + + +## Documentation + + +* Drop Client model (unused) from old swagger. + Add a description and example data for mls_public_keys field in new swagger. (#2657) + +* Document user deactivation (aka suspension) with SCIM. (#2720) + +* Monitoring page showed wrong wrong configuration charts. Updated prometheus-operator to kube-prometheus-stack chart in the documentation. (#2708) + + +## Internal changes + + +* Make client deletion asynchronous (#2669) + +* Allow external add proposals without previously uploading key packages. (#2661) + +* Allow legalhold tokens access to `/converations/` endpoint (#2682, #2726) + +* Move Brig.Sem.* modules to Brig.Effects (consistency) (#2672) + +* The labels applied to resources in the coturn chart have been changed to + reflect the conventions in the wire-server charts. (#2677) + +* Drop the `managed` column from `team_conv` table in Galley (#2127) + +* Fix link in PR template (#2673) + +* In Gundeck's 'notifications' cassandra table, switch to [TWCS](https://cassandra.apache.org/doc/latest/cassandra/operating/compaction/twcs.html) compaction strategy, which should be more efficient for this workload, and possibly bring performance benefits to latencies. + It may be beneficial to run a manual compaction before rolling out this + change (but things should also work without this manual operation). + In case you have time, run the following from a cassandra machine before deploying this update: `nodetool compact gundeck notifications`. (#2615) + +* Add regular expression support to libzauth ACL language (#2714) + +* Make test API calls point to the most recent version by default (#2695) + +* Clients and key package refs in an MLS conversation are now stored in their own table. (#2667) + +* Refactor MLS test framework (#2678) + +* Update mls-test-cli to version 0.5 (#2685) + +* Added rusty-jwt-tools to docker images (#2686) + +* The account API is now migrated to servant. (#2699, #2700, #2701, #2702, #2703, #2704, #2705, #2707) + +* Update nginz and cannon ACLs to match api-versioned paths (#2725) + +* For wire-server cloud, on kubernetes 1.21+, favour topology-aware routing, which reduces unnecessary inter-availability-zone traffic, reducing latency and cloud provider cross-AZ traffic costs. (#2723) + + # [2022-09-01] (Chart Release 4.23.0) ## Release notes diff --git a/Makefile b/Makefile index 90c9a95208..a60238394f 100644 --- a/Makefile +++ b/Makefile @@ -117,6 +117,31 @@ cabal-fmt: ghcid: ghcid -l=hlint --command "cabal repl $(target)" +.PHONY: hlint-check-all +hlint-check-all: + ./tools/hlint.sh -f all -m check + +.PHONY: hlint-check-pr +hlint-check-pr: + ./tools/hlint.sh -f pr -m check + +.PHONY: hlint-inplace-pr +hlint-inplace-pr: + ./tools/hlint.sh -f pr -m inplace + + +.PHONY: hlint-inplace-all +hlint-inplace-all: + ./tools/hlint.sh -f all -m inplace + +.PHONY: hlint-check +hlint-check: + ./tools/hlint.sh -f changeset -m check + +.PHONY: hlint-inplace +hlint-inplace: + ./tools/hlint.sh -f changeset -m inplace + # reset db using cabal .PHONY: db-reset-package db-reset-package: c diff --git a/build/ubuntu/Dockerfile.builder b/build/ubuntu/Dockerfile.builder index df52ce9bc4..bd919ee5d1 100644 --- a/build/ubuntu/Dockerfile.builder +++ b/build/ubuntu/Dockerfile.builder @@ -10,9 +10,22 @@ RUN cd /tmp && \ RUN cd /tmp/mls-test-cli && RUSTFLAGS='-C target-feature=+crt-static' cargo build --release --target x86_64-unknown-linux-gnu +FROM rust:1.63 as rusty-jwt-tools-builder + +# compile rusty-jwt-tools +RUN cd /tmp && \ + git clone https://github.com/wireapp/rusty-jwt-tools && \ + cd rusty-jwt-tools && \ + git checkout 6370cd556f03f6834d0b8043615ffaf0044ef1fa && \ + git rev-parse HEAD + +RUN cd /tmp/rusty-jwt-tools && cargo build --release --target x86_64-unknown-linux-gnu + FROM ${prebuilder} COPY --from=mls-test-cli-builder /tmp/mls-test-cli/target/x86_64-unknown-linux-gnu/release/mls-test-cli /usr/bin/mls-test-cli +COPY --from=rusty-jwt-tools-builder /tmp/rusty-jwt-tools/target/x86_64-unknown-linux-gnu/release/librusty_jwt_tools.so /usr/lib +COPY --from=rusty-jwt-tools-builder /tmp/rusty-jwt-tools/target/x86_64-unknown-linux-gnu/release/librusty_jwt_tools_ffi.so /usr/lib WORKDIR / diff --git a/build/ubuntu/Dockerfile.deps b/build/ubuntu/Dockerfile.deps index 7b356804b4..458f71f01a 100644 --- a/build/ubuntu/Dockerfile.deps +++ b/build/ubuntu/Dockerfile.deps @@ -20,11 +20,25 @@ RUN cd /tmp && \ RUN cd /tmp/mls-test-cli && RUSTFLAGS='-C target-feature=+crt-static' cargo build --release --target x86_64-unknown-linux-gnu +FROM rust:1.63 as rusty-jwt-tools-builder + +# compile rusty-jwt-tools +RUN cd /tmp && \ + git clone https://github.com/wireapp/rusty-jwt-tools && \ + cd rusty-jwt-tools && \ + git checkout 6370cd556f03f6834d0b8043615ffaf0044ef1fa && \ + git rev-parse HEAD + +RUN cd /tmp/rusty-jwt-tools && cargo build --release --target x86_64-unknown-linux-gnu + + # Minimal dependencies for ubuntu-compiled, dynamically linked wire-server Haskell services FROM ubuntu:20.04 COPY --from=cryptobox-builder /tmp/cryptobox-c/target/release/libcryptobox.so /usr/lib COPY --from=mls-test-cli-builder /tmp/mls-test-cli/target/x86_64-unknown-linux-gnu/release/mls-test-cli /usr/bin/mls-test-cli +COPY --from=rusty-jwt-tools-builder /tmp/rusty-jwt-tools/target/x86_64-unknown-linux-gnu/release/librusty_jwt_tools.so /usr/lib +COPY --from=rusty-jwt-tools-builder /tmp/rusty-jwt-tools/target/x86_64-unknown-linux-gnu/release/librusty_jwt_tools_ffi.so /usr/lib RUN export DEBIAN_FRONTEND=noninteractive && \ apt-get update && \ diff --git a/cabal.project b/cabal.project index c0c9d989ab..b6beec6f68 100644 --- a/cabal.project +++ b/cabal.project @@ -157,6 +157,12 @@ source-repository-package location: https://gitlab.com/twittner/wai-routing tag: 7e996a93fec5901767f845a50316b3c18e51a61d +source-repository-package + type: git + location: https://github.com/wireapp/tasty.git + tag: 394943c7672e5ad269e5587528b7678caf3b0720 + subdir: hunit + allow-older: * allow-newer: * diff --git a/cassandra-schema.cql b/cassandra-schema.cql index c4666a1071..c2ba0cbe06 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -38,7 +38,6 @@ CREATE TABLE galley_test.meta ( CREATE TABLE galley_test.team_conv ( team uuid, conv uuid, - managed boolean, PRIMARY KEY (team, conv) ) WITH CLUSTERING ORDER BY (conv ASC) AND bloom_filter_fp_chance = 0.1 @@ -129,6 +128,7 @@ CREATE TABLE galley_test.team_features ( app_lock_status int, conference_calling int, digital_signatures int, + expose_invitation_urls_to_team_admin int, file_sharing int, file_sharing_lock_status int, guest_links_lock_status int, @@ -169,7 +169,6 @@ CREATE TABLE galley_test.member ( conversation_role text, hidden boolean, hidden_ref text, - mls_clients set, otr_archived boolean, otr_archived_ref text, otr_muted boolean, @@ -263,7 +262,6 @@ CREATE TABLE galley_test.member_remote_user ( user_remote_domain text, user_remote_id uuid, conversation_role text, - mls_clients set, PRIMARY KEY (conv, user_remote_domain, user_remote_id) ) WITH CLUSTERING ORDER BY (user_remote_domain ASC, user_remote_id ASC) AND bloom_filter_fp_chance = 0.1 @@ -366,15 +364,18 @@ CREATE TABLE galley_test.group_id_conv_id ( AND read_repair_chance = 0.0 AND speculative_retry = '99PERCENTILE'; -CREATE TABLE galley_test.user ( - user uuid, +CREATE TABLE galley_test.member_client ( conv uuid, - PRIMARY KEY (user, conv) -) WITH CLUSTERING ORDER BY (conv ASC) - AND bloom_filter_fp_chance = 0.1 + user_domain text, + user uuid, + client text, + key_package_ref blob, + PRIMARY KEY (conv, user_domain, user, client) +) WITH CLUSTERING ORDER BY (user_domain ASC, user ASC, client ASC) + AND bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} AND crc_check_chance = 1.0 AND dclocal_read_repair_chance = 0.1 @@ -460,6 +461,7 @@ CREATE TABLE galley_test.conversation ( message_timer bigint, name text, protocol int, + public_group_state blob, receipt_mode int, team uuid, type int @@ -566,6 +568,26 @@ CREATE TABLE galley_test.mls_proposal_refs ( AND read_repair_chance = 0.0 AND speculative_retry = '99PERCENTILE'; +CREATE TABLE galley_test.user ( + user uuid, + conv uuid, + PRIMARY KEY (user, conv) +) WITH CLUSTERING ORDER BY (conv ASC) + AND bloom_filter_fp_chance = 0.1 + AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} + AND comment = '' + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} + AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} + AND crc_check_chance = 1.0 + AND dclocal_read_repair_chance = 0.1 + AND default_time_to_live = 0 + AND gc_grace_seconds = 864000 + AND max_index_interval = 2048 + AND memtable_flush_period_in_ms = 0 + AND min_index_interval = 128 + AND read_repair_chance = 0.0 + AND speculative_retry = '99PERCENTILE'; + CREATE KEYSPACE gundeck_test WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND durable_writes = true; CREATE TABLE gundeck_test.push ( @@ -602,7 +624,7 @@ CREATE TABLE gundeck_test.notifications ( AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy', 'tombstone_threshold': '0.1'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy', 'compaction_window_size': '1', 'compaction_window_unit': 'DAYS', 'max_threshold': '32', 'min_threshold': '4'} AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} AND crc_check_chance = 1.0 AND dclocal_read_repair_chance = 0.1 diff --git a/charts/backoffice/templates/service.yaml b/charts/backoffice/templates/service.yaml index 3422d81a77..a3ae8a9d9b 100644 --- a/charts/backoffice/templates/service.yaml +++ b/charts/backoffice/templates/service.yaml @@ -7,6 +7,8 @@ metadata: chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} + annotations: + service.kubernetes.io/topology-aware-hints: auto spec: type: ClusterIP ports: diff --git a/charts/backoffice/values.yaml b/charts/backoffice/values.yaml index cb7b10bdfd..bbdb1e881e 100644 --- a/charts/backoffice/values.yaml +++ b/charts/backoffice/values.yaml @@ -13,11 +13,10 @@ service: externalPort: 8080 resources: requests: - memory: 128Mi - cpu: 125m + memory: 20Mi + cpu: 30m limits: - memory: 512Mi - cpu: 500m + memory: 50Mi config: logLevel: Info galebHost: galeb.integrations diff --git a/charts/brig/templates/service.yaml b/charts/brig/templates/service.yaml index 432be27dd1..63d52526e2 100644 --- a/charts/brig/templates/service.yaml +++ b/charts/brig/templates/service.yaml @@ -7,6 +7,8 @@ metadata: chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} + annotations: + service.kubernetes.io/topology-aware-hints: auto spec: type: ClusterIP ports: diff --git a/charts/brig/values.yaml b/charts/brig/values.yaml index ced8902dcf..efb2ecf525 100644 --- a/charts/brig/values.yaml +++ b/charts/brig/values.yaml @@ -7,11 +7,10 @@ service: internalPort: 8080 resources: requests: - memory: "256Mi" + memory: "200Mi" cpu: "100m" limits: memory: "512Mi" - cpu: "500m" metrics: serviceMonitor: enable: false diff --git a/charts/cannon/conf/static/zauth.acl b/charts/cannon/conf/static/zauth.acl index 9498b8cc43..8e6d629346 100644 --- a/charts/cannon/conf/static/zauth.acl +++ b/charts/cannon/conf/static/zauth.acl @@ -1,17 +1 @@ -a (blacklist (path "/provider") - (path "/provider/**") - (path "/bot") - (path "/bot/**") - (path "/i/**")) - -b (whitelist (path "/bot") - (path "/bot/**")) - -p (whitelist (path "/provider") - (path "/provider/**")) - -# LegalHold Access Tokens -la (whitelist (path "/notifications") - (path "/assets/v3/**") - (path "/users") - (path "/users/**")) +a (whitelist (regex "/await")) diff --git a/charts/cannon/templates/headless-service.yaml b/charts/cannon/templates/headless-service.yaml index 5c107d0bc2..2788f00c0d 100644 --- a/charts/cannon/templates/headless-service.yaml +++ b/charts/cannon/templates/headless-service.yaml @@ -2,7 +2,7 @@ # We use it this way so we can handle routing requests to specific cannons directly rather than distributing requests # between pods. # -# Read more about this technique in the StatefulSet guide: +# Read more about this technique in the StatefulSet guide: # https://kubernetes.io/docs/tutorials/stateful-application/basic-stateful-set/ apiVersion: v1 kind: Service @@ -13,6 +13,8 @@ metadata: chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} + annotations: + service.kubernetes.io/topology-aware-hints: auto spec: type: ClusterIP # This is what makes it a Headless Service diff --git a/charts/cannon/templates/nginz-service.yaml b/charts/cannon/templates/nginz-service.yaml index 704e2e2a25..901c35abaa 100644 --- a/charts/cannon/templates/nginz-service.yaml +++ b/charts/cannon/templates/nginz-service.yaml @@ -18,6 +18,7 @@ metadata: release: {{ .Release.Name }} heritage: {{ .Release.Service }} annotations: + service.kubernetes.io/topology-aware-hints: auto {{- if .Values.service.nginz.externalDNS.enabled }} external-dns.alpha.kubernetes.io/ttl: {{ .Values.service.nginz.externalDNS.ttl | quote }} external-dns.alpha.kubernetes.io/hostname: {{ required "Please provide .service.nginz.hostname when .service.nginz.enabled and .service.nginz.externalDNS.enabled are True" .Values.service.nginz.hostname | quote }} diff --git a/charts/cannon/values.yaml b/charts/cannon/values.yaml index f5ca6b721a..2a8ac45232 100644 --- a/charts/cannon/values.yaml +++ b/charts/cannon/values.yaml @@ -84,8 +84,7 @@ resources: memory: "256Mi" cpu: "100m" limits: - memory: "512Mi" - cpu: "500m" + memory: "1024Mi" service: name: cannon internalPort: 8080 diff --git a/charts/cargohold/templates/service.yaml b/charts/cargohold/templates/service.yaml index af4957e907..c6d7422a79 100644 --- a/charts/cargohold/templates/service.yaml +++ b/charts/cargohold/templates/service.yaml @@ -7,6 +7,8 @@ metadata: chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} + annotations: + service.kubernetes.io/topology-aware-hints: auto spec: type: ClusterIP ports: diff --git a/charts/cargohold/values.yaml b/charts/cargohold/values.yaml index 77198b778d..3c46d9cf43 100644 --- a/charts/cargohold/values.yaml +++ b/charts/cargohold/values.yaml @@ -10,11 +10,10 @@ metrics: enable: false resources: requests: - memory: "256Mi" + memory: "80Mi" cpu: "100m" limits: - memory: "512Mi" - cpu: "500m" + memory: "200Mi" config: logLevel: Info logFormat: StructuredJSON diff --git a/charts/coturn/templates/_helpers.yaml b/charts/coturn/templates/_helpers.yaml deleted file mode 100644 index 32fea22520..0000000000 --- a/charts/coturn/templates/_helpers.yaml +++ /dev/null @@ -1,45 +0,0 @@ -{{- define "coturn.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "coturn.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "coturn.labels" -}} -helm.sh/chart: {{ include "coturn.chart" . }} -{{ include "coturn.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Values.image.tag | default .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "coturn.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{- define "coturn.selectorLabels" -}} -app.kubernetes.io/name: {{ include "coturn.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} diff --git a/charts/coturn/templates/configmap-coturn-conf-template.yaml b/charts/coturn/templates/configmap-coturn-conf-template.yaml index 76e0f95605..4a2a4c4c06 100644 --- a/charts/coturn/templates/configmap-coturn-conf-template.yaml +++ b/charts/coturn/templates/configmap-coturn-conf-template.yaml @@ -1,9 +1,7 @@ apiVersion: v1 kind: ConfigMap metadata: - name: {{ include "coturn.fullname" . }} - labels: - {{- include "coturn.selectorLabels" . | nindent 4 }} + name: coturn data: coturn.conf.template: | diff --git a/charts/coturn/templates/secret.yaml b/charts/coturn/templates/secret.yaml index af6a8563cf..6dd5521206 100644 --- a/charts/coturn/templates/secret.yaml +++ b/charts/coturn/templates/secret.yaml @@ -8,9 +8,10 @@ kind: Secret metadata: name: coturn labels: + app: coturn chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} - release: "{{ .Release.Name }}" - heritage: "{{ .Release.Service }}" + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} type: Opaque stringData: zrest_secret.txt: | diff --git a/charts/coturn/templates/service-account.yaml b/charts/coturn/templates/service-account.yaml index 1bea5d5908..ce2803840f 100644 --- a/charts/coturn/templates/service-account.yaml +++ b/charts/coturn/templates/service-account.yaml @@ -2,16 +2,22 @@ apiVersion: v1 kind: ServiceAccount metadata: - name: {{ include "coturn.fullname" . }} + name: coturn labels: - {{- include "coturn.labels" . | nindent 4 }} + app: coturn + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - name: {{ include "coturn.fullname" . }} + name: coturn labels: - {{- include "coturn.labels" . | nindent 4 }} + app: coturn + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} rules: - apiGroups: [""] resources: [nodes] @@ -20,14 +26,17 @@ rules: apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: - name: {{ include "coturn.fullname" . }} + name: coturn labels: - {{- include "coturn.labels" . | nindent 4 }} + app: coturn + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} roleRef: kind: ClusterRole apiGroup: rbac.authorization.k8s.io - name: {{ include "coturn.fullname" . }} + name: coturn subjects: - kind: ServiceAccount - name: {{ include "coturn.fullname" . }} + name: coturn namespace: {{ .Release.Namespace }} diff --git a/charts/coturn/templates/service.yaml b/charts/coturn/templates/service.yaml index a5f8f15bd5..f1420c44d6 100644 --- a/charts/coturn/templates/service.yaml +++ b/charts/coturn/templates/service.yaml @@ -2,9 +2,12 @@ apiVersion: v1 kind: Service metadata: - name: {{ include "coturn.fullname" . }} + name: coturn labels: - {{- include "coturn.labels" . | nindent 4 }} + app: coturn + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} spec: # Needs to be headless # See: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/ @@ -22,5 +25,9 @@ spec: port: {{ .Values.coturnTurnTlsListenPort }} targetPort: coturn-tls {{- end }} + - name: status-http + port: {{ .Values.coturnMetricsListenPort }} + targetPort: status-http selector: - {{- include "coturn.selectorLabels" . | nindent 4 }} + app: coturn + release: {{ .Release.Name }} diff --git a/charts/coturn/templates/servicemonitor.yaml b/charts/coturn/templates/servicemonitor.yaml new file mode 100644 index 0000000000..a21f0faea4 --- /dev/null +++ b/charts/coturn/templates/servicemonitor.yaml @@ -0,0 +1,19 @@ +{{- if .Values.metrics.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: coturn + labels: + app: coturn + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + endpoints: + - port: status-http + path: /metrics + selector: + matchLabels: + app: coturn + release: {{ .Release.Name }} +{{- end }} diff --git a/charts/coturn/templates/statefulset.yaml b/charts/coturn/templates/statefulset.yaml index daf90ace40..8ab28192b5 100644 --- a/charts/coturn/templates/statefulset.yaml +++ b/charts/coturn/templates/statefulset.yaml @@ -1,9 +1,12 @@ apiVersion: apps/v1 kind: StatefulSet metadata: - name: {{ include "coturn.fullname" . }} + name: coturn labels: - {{- include "coturn.labels" . | nindent 4 }} + app: coturn + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} spec: replicas: {{ .Values.replicaCount }} @@ -12,10 +15,10 @@ spec: # affect upgrades. podManagementPolicy: Parallel - serviceName: {{ include "coturn.fullname" . }} + serviceName: coturn selector: matchLabels: - {{- include "coturn.selectorLabels" . | nindent 6 }} + app: coturn template: metadata: {{- with .Values.podAnnotations }} @@ -24,7 +27,8 @@ spec: {{- end }} labels: - {{- include "coturn.selectorLabels" . | nindent 8 }} + app: coturn + release: {{ .Release.Name }} spec: securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} @@ -33,7 +37,7 @@ spec: shareProcessNamespace: true {{- end }} hostNetwork: true - serviceAccountName: {{ include "coturn.fullname" . }} + serviceAccountName: coturn volumes: - name: external-ip emptyDir: {} @@ -41,7 +45,7 @@ spec: emptyDir: {} - name: coturn-config-template configMap: - name: {{ include "coturn.fullname" . }} + name: coturn - name: secrets secret: secretName: coturn diff --git a/charts/coturn/values.yaml b/charts/coturn/values.yaml index 1504bbcdca..eede1626be 100644 --- a/charts/coturn/values.yaml +++ b/charts/coturn/values.yaml @@ -36,6 +36,10 @@ tls: pullPolicy: IfNotPresent tag: 1aa6cbbf2ce3a5182ec47e3579bbcb8f47e22fdc +metrics: + serviceMonitor: + enabled: false + # This chart optionally supports waiting for traffic to drain from coturn # before pods are terminated. Warning: coturn does not have any way to steer # incoming client traffic away from itself on its own, so this functionality diff --git a/charts/federator/templates/service.yaml b/charts/federator/templates/service.yaml index 5394e54b1a..22d018b913 100644 --- a/charts/federator/templates/service.yaml +++ b/charts/federator/templates/service.yaml @@ -7,6 +7,8 @@ metadata: chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} + annotations: + service.kubernetes.io/topology-aware-hints: auto spec: type: ClusterIP ports: diff --git a/charts/galley/templates/configmap.yaml b/charts/galley/templates/configmap.yaml index 9a877ab26f..c5ce757ace 100644 --- a/charts/galley/templates/configmap.yaml +++ b/charts/galley/templates/configmap.yaml @@ -54,6 +54,9 @@ data: {{- if .settings.maxFanoutSize }} maxFanoutSize: {{ .settings.maxFanoutSize }} {{- end }} + {{- if .settings.exposeInvitationURLsTeamAllowlist }} + exposeInvitationURLsTeamAllowlist: {{ .settings.exposeInvitationURLsTeamAllowlist }} + {{- end }} conversationCodeURI: {{ .settings.conversationCodeURI | quote }} {{- if .settings.enableIndexedBillingTeamMembers }} enableIndexedBillingTeamMembers: {{ .settings.enableIndexedBillingTeamMembers }} @@ -92,15 +95,15 @@ data: {{- if .settings.featureFlags.appLock }} appLock: {{- toYaml .settings.featureFlags.appLock | nindent 10 }} - {{- end }} + {{- end }} {{- if .settings.featureFlags.conferenceCalling }} conferenceCalling: {{- toYaml .settings.featureFlags.conferenceCalling | nindent 10 }} - {{- end }} + {{- end }} {{- if .settings.featureFlags.selfDeletingMessages }} selfDeletingMessages: {{- toYaml .settings.featureFlags.selfDeletingMessages | nindent 10 }} - {{- end }} + {{- end }} {{- if .settings.featureFlags.conversationGuestLinks }} conversationGuestLinks: {{- toYaml .settings.featureFlags.conversationGuestLinks | nindent 10 }} diff --git a/charts/galley/templates/service.yaml b/charts/galley/templates/service.yaml index f79d3a70e9..d7cdd38ce4 100644 --- a/charts/galley/templates/service.yaml +++ b/charts/galley/templates/service.yaml @@ -7,6 +7,8 @@ metadata: chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} + annotations: + service.kubernetes.io/topology-aware-hints: auto spec: type: ClusterIP ports: diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index 76406d88aa..4ee17754e5 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -11,11 +11,10 @@ metrics: enable: false resources: requests: - memory: "256Mi" + memory: "100Mi" cpu: "100m" limits: - memory: "512Mi" - cpu: "500m" + memory: "200Mi" config: logLevel: Info logFormat: StructuredJSON @@ -27,6 +26,7 @@ config: settings: httpPoolSize: 128 maxTeamSize: 10000 + exposeInvitationURLsTeamAllowlist: [] maxConvSize: 500 # Before making indexedBillingTeamMember true while upgrading, please # refer to notes here: https://github.com/wireapp/wire-server-deploy/releases/tag/v2020-05-15 @@ -80,7 +80,7 @@ config: validateSAMLemails: defaults: status: enabled - + aws: region: "eu-west-1" proxy: {} diff --git a/charts/gundeck/templates/service.yaml b/charts/gundeck/templates/service.yaml index 0d27085f1a..c685bd4504 100644 --- a/charts/gundeck/templates/service.yaml +++ b/charts/gundeck/templates/service.yaml @@ -7,6 +7,8 @@ metadata: chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} + annotations: + service.kubernetes.io/topology-aware-hints: auto spec: type: ClusterIP ports: diff --git a/charts/gundeck/values.yaml b/charts/gundeck/values.yaml index d9b10037e2..67d35e937a 100644 --- a/charts/gundeck/values.yaml +++ b/charts/gundeck/values.yaml @@ -10,11 +10,10 @@ metrics: enable: false resources: requests: - memory: "256Mi" + memory: "300Mi" cpu: "100m" limits: - memory: "512Mi" - cpu: "500m" + memory: "1Gi" config: logLevel: Info logFormat: StructuredJSON diff --git a/charts/legalhold/templates/service.yaml b/charts/legalhold/templates/service.yaml index 74b8d98028..4a178e268e 100644 --- a/charts/legalhold/templates/service.yaml +++ b/charts/legalhold/templates/service.yaml @@ -2,6 +2,8 @@ apiVersion: v1 kind: Service metadata: name: "{{ .Release.Name }}-hold" + annotations: + service.kubernetes.io/topology-aware-hints: auto spec: type: ClusterIP selector: diff --git a/charts/nginz/static/conf/zauth.acl b/charts/nginz/static/conf/zauth.acl index 9498b8cc43..3b644bf3d9 100644 --- a/charts/nginz/static/conf/zauth.acl +++ b/charts/nginz/static/conf/zauth.acl @@ -1,17 +1,15 @@ -a (blacklist (path "/provider") - (path "/provider/**") - (path "/bot") - (path "/bot/**") - (path "/i/**")) +a (blacklist (regex "(/v[0-9]+)?/provider(/.*)?") + (regex "(/v[0-9]+)?/bot(/.*)?") + (regex "(/v[0-9]+)?/i/.*")) -b (whitelist (path "/bot") - (path "/bot/**")) +b (whitelist (regex "(/v[0-9]+)?/bot(/.*)?")) -p (whitelist (path "/provider") - (path "/provider/**")) +p (whitelist (regex "(/v[0-9]+)?/provider(/.*)?")) # LegalHold Access Tokens -la (whitelist (path "/notifications") - (path "/assets/v3/**") - (path "/users") - (path "/users/**")) +# FUTUREWORK: remove /legalhold/conversations/ when support for v1 dropped +la (whitelist (regex "(/v[0-9]+)?/notifications") + (regex "(/v[0-9]+)?/assets/v3/.*") + (regex "(/v[0-9]+)?/users(/.*)?") + (regex "(/v[0-9]+)?/legalhold/conversations/[^/]+") + (regex "(/v[0-9]+)?/conversations/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")) diff --git a/charts/nginz/templates/service.yaml b/charts/nginz/templates/service.yaml index 8ed76cdaaa..6a5c2420f7 100644 --- a/charts/nginz/templates/service.yaml +++ b/charts/nginz/templates/service.yaml @@ -7,6 +7,8 @@ metadata: chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} + annotations: + service.kubernetes.io/topology-aware-hints: auto spec: type: ClusterIP ports: diff --git a/charts/nginz/values.yaml b/charts/nginz/values.yaml index 3cd3e6d2b5..bb6790f8e0 100644 --- a/charts/nginz/values.yaml +++ b/charts/nginz/values.yaml @@ -4,8 +4,7 @@ resources: memory: "256Mi" cpu: "100m" limits: - memory: "1024Mi" - cpu: "2" + memory: "800Mi" metrics: serviceMonitor: enabled: false @@ -398,6 +397,9 @@ nginx_conf: envs: - all doc: true + - path: /legalhold/conversations/(.*) + envs: + - all - path: /teams$ envs: - all @@ -476,6 +478,12 @@ nginx_conf: - path: /mls/messages envs: - all + - path: /mls/commit-bundles + envs: + - all + - path: /mls/public-keys + envs: + - all - path: /nonce/clients envs: - all diff --git a/charts/proxy/templates/service.yaml b/charts/proxy/templates/service.yaml index 2bda5053b2..f3640fa434 100644 --- a/charts/proxy/templates/service.yaml +++ b/charts/proxy/templates/service.yaml @@ -7,6 +7,8 @@ metadata: chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} + annotations: + service.kubernetes.io/topology-aware-hints: auto spec: type: ClusterIP ports: diff --git a/charts/proxy/values.yaml b/charts/proxy/values.yaml index 94dbcd70d6..90605af047 100644 --- a/charts/proxy/values.yaml +++ b/charts/proxy/values.yaml @@ -10,11 +10,10 @@ metrics: enable: false resources: requests: - memory: "128Mi" - cpu: "100m" + memory: "25Mi" + cpu: "50m" limits: - memory: "512Mi" - cpu: "500m" + memory: "50Mi" config: logLevel: Info logFormat: StructuredJSON diff --git a/charts/spar/templates/service.yaml b/charts/spar/templates/service.yaml index 711967459f..201b604a82 100644 --- a/charts/spar/templates/service.yaml +++ b/charts/spar/templates/service.yaml @@ -7,6 +7,8 @@ metadata: chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} + annotations: + service.kubernetes.io/topology-aware-hints: auto spec: type: ClusterIP ports: diff --git a/charts/spar/values.yaml b/charts/spar/values.yaml index 28a9681871..89631616cf 100644 --- a/charts/spar/values.yaml +++ b/charts/spar/values.yaml @@ -7,11 +7,10 @@ metrics: enable: false resources: requests: - memory: "128Mi" - cpu: "100m" + memory: "25Mi" + cpu: "50m" limits: - memory: "512Mi" - cpu: "500m" + memory: "50Mi" service: externalPort: 8080 internalPort: 8080 diff --git a/charts/team-settings/values.yaml b/charts/team-settings/values.yaml index 65cd0bc57f..03da568f12 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.11.0-v0.31.1-0-9e64150" + tag: "4.12.1-v0.31.5-0-0167ea4" service: https: externalPort: 443 diff --git a/charts/webapp/values.yaml b/charts/webapp/values.yaml index 78f3b51a12..7cffabca9a 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-06-30-production.0-v0.30.5-0-3e2aaf6" + tag: "2022-09-20-production.0-v0.31.2-0-7f74074" service: https: externalPort: 443 diff --git a/deploy/services-demo/conf/nginz/nginx.conf b/deploy/services-demo/conf/nginz/nginx.conf index 36ae5cd902..9074229501 100644 --- a/deploy/services-demo/conf/nginz/nginx.conf +++ b/deploy/services-demo/conf/nginz/nginx.conf @@ -214,7 +214,7 @@ http { proxy_pass http://brig; } - location ~* ^/teams/invitations/([^/]*)$ { + location ~* ^(/v[0-9]+)?/teams/invitations/([^/]*)$ { include common_response_no_zauth.conf; proxy_pass http://brig; } @@ -226,7 +226,7 @@ http { ## brig authenticated endpoints - location /self { + location ~* ^(/v[0-9]+)?/self$ { include common_response_with_zauth.conf; proxy_pass http://brig; } @@ -261,7 +261,7 @@ http { proxy_pass http://brig; } - location /clients { + location ~* ^(/v[0-9]+)?/clients$ { include common_response_with_zauth.conf; proxy_pass http://brig; } @@ -325,6 +325,10 @@ http { proxy_pass http://galley; } + location ~* ^(/v[0-9]+)?/legalhold/conversations/(.*)$ { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } location /conversations { include common_response_with_zauth.conf; @@ -421,6 +425,16 @@ http { proxy_pass http://galley; } + location /mls/commit-bundles { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location /mls/public-keys { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + # Gundeck Endpoints rewrite ^/api-docs/push /push/api-docs?base_url=http://127.0.0.1:8080/ break; @@ -440,7 +454,7 @@ http { proxy_pass http://gundeck; } - location /notifications { + location ~* ^(/v[0-9]+)?/notifications$ { include common_response_with_zauth.conf; proxy_pass http://gundeck; } diff --git a/deploy/services-demo/conf/nginz/zauth_acl.txt b/deploy/services-demo/conf/nginz/zauth_acl.txt index 9498b8cc43..3b644bf3d9 100644 --- a/deploy/services-demo/conf/nginz/zauth_acl.txt +++ b/deploy/services-demo/conf/nginz/zauth_acl.txt @@ -1,17 +1,15 @@ -a (blacklist (path "/provider") - (path "/provider/**") - (path "/bot") - (path "/bot/**") - (path "/i/**")) +a (blacklist (regex "(/v[0-9]+)?/provider(/.*)?") + (regex "(/v[0-9]+)?/bot(/.*)?") + (regex "(/v[0-9]+)?/i/.*")) -b (whitelist (path "/bot") - (path "/bot/**")) +b (whitelist (regex "(/v[0-9]+)?/bot(/.*)?")) -p (whitelist (path "/provider") - (path "/provider/**")) +p (whitelist (regex "(/v[0-9]+)?/provider(/.*)?")) # LegalHold Access Tokens -la (whitelist (path "/notifications") - (path "/assets/v3/**") - (path "/users") - (path "/users/**")) +# FUTUREWORK: remove /legalhold/conversations/ when support for v1 dropped +la (whitelist (regex "(/v[0-9]+)?/notifications") + (regex "(/v[0-9]+)?/assets/v3/.*") + (regex "(/v[0-9]+)?/users(/.*)?") + (regex "(/v[0-9]+)?/legalhold/conversations/[^/]+") + (regex "(/v[0-9]+)?/conversations/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")) diff --git a/docs/src/developer/reference/cassandra-schema.cql b/docs/src/developer/reference/cassandra-schema.cql index 0939aa54a6..8382d36050 120000 --- a/docs/src/developer/reference/cassandra-schema.cql +++ b/docs/src/developer/reference/cassandra-schema.cql @@ -1 +1 @@ -../../../cassandra-schema.cql \ No newline at end of file +../../../../cassandra-schema.cql \ No newline at end of file diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index 8a74338aa7..07ce0ec7ca 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -99,6 +99,53 @@ IMPORTANT: If you switch this back to `disabled-permanently` from that have created them while it was allowed. This may change in the future. +### Expose invitation URLs to team admin + +For further processing (e.g. sending custom emails or rendering the URLs as QR +codes), team invitation URLs can be made part of the result of +`GET /teams/{tid}/invitations`. + +```json +{ + "has_more": false, + "invitations": [ + { + "created_at": "2022-09-15T15:47:28.577Z", + "created_by": "375f56fe-7f12-4c0c-aed8-d48c0326d1fb", + "email": "foo@example.com", + "id": "4decf7f8-bdd4-43b3-aaf2-e912e2c0c46f", + "name": null, + "phone": null, + "role": "member", + "team": "51612209-3b61-49b0-8c55-d21ae65efc1a", + "url": "http://127.0.0.1:8080/register?team=51612209-3b61-49b0-8c55-d21ae65efc1a&team_code=RpxGkK_yjw8ZBegJuFQO0hha-2Tneajp" + } + ] +} +``` + +This can be a privacy issue as it allows the team admin to impersonate as +another team member. The feature is disabled by default. + +To activate this feature two steps are needed. First, the team id (tid) has to +be added to the list of teams for which this feature *can* be enabled +(`exposeInvitationURLsTeamAllowlist`). This is done in `galley`'s `values.yaml`: + +```yaml +settings: + exposeInvitationURLsTeamAllowlist: ["51612209-3b61-49b0-8c55-d21ae65efc1a", ...] +``` + +Then, the feature can be set for the team by enabling the +`exposeInvitationURLsToTeamAdmin` flag. This is done by making a `PUT` request +to `/teams/{tid}/features/exposeInvitationURLsToTeamAdmin` with the body: + +```json +{ + "status": "enabled" +} +``` + ### Team searchVisibility The team flag `searchVisibility` affects the outbound search of user diff --git a/docs/src/developer/reference/conversation.md b/docs/src/developer/reference/conversation.md index eedbfa7ce0..44db3a2860 100644 --- a/docs/src/developer/reference/conversation.md +++ b/docs/src/developer/reference/conversation.md @@ -82,7 +82,6 @@ export WIRE_CONV='{ "users": [], "name": "'${WIRE_CONV_NAME}'", "team": { - "managed": false, "teamid": "'${WIRE_TEAMID}'" }, "receipt_mode": 0, diff --git a/docs/src/developer/reference/team/legalhold.md b/docs/src/developer/reference/team/legalhold.md index b1cee83b9b..081796610e 100644 --- a/docs/src/developer/reference/team/legalhold.md +++ b/docs/src/developer/reference/team/legalhold.md @@ -137,23 +137,27 @@ blocked from using wire by their team admin (if they are a team user), but they cannot be assigned a LH device, and they cannot enter conversations with LH devices present. -For now, there is on way in the UI for the user to grant consent. -Instead, "implict consent" can be given by the site operator for any -team in the server configuration file `galley.yaml`: +For now, there isn't any UI for the user to grant their initial consent. +Instead, an "implict consent" can be given by the site operator by setting ```yaml featureFlags: # [...] legalhold: whitelist-teams-and-implicit-consent - legalHoldTeamsWhitelist: - - 14172c08-b3c8-11eb-a763-6fe8c2ea993d - - 162d7894-b3c8-11eb-b137-074ff453399d ``` +in galley's config and then using non-exposed, internal endpoints on the galley +pod to update the set of teams whose users are considered to have given their +initial consent: + +- `put /i/legalhold/whitelisted-teams/:team-id` - Add team +- `delete /i/legalhold/whitelisted-teams/:team-id` - Remove team +- `get /i/legalhold/whitelisted-teams` - List all teams + Since consent is required for LH to work, users in teams that are not whitelisted cannot be assigned LH devices (pull request #1502), and they are blocked or removed from conversations that are exposed to LH -devices (TODO: name the PRs where this happens). +devices (#1507, #1595). ### Implementation status and future work diff --git a/docs/src/how-to/install/monitoring.rst b/docs/src/how-to/install/monitoring.rst index d886b888bd..eb49b1a404 100644 --- a/docs/src/how-to/install/monitoring.rst +++ b/docs/src/how-to/install/monitoring.rst @@ -133,7 +133,7 @@ file. # This configuration switches to use memory instead of disk for metrics services # NOTE: If the pods are killed you WILL lose all your metrics history - prometheus-operator: + kube-prometheus-stack: grafana: persistence: enabled: false @@ -160,7 +160,7 @@ file. .. code:: yaml - prometheus-operator: + kube-prometheus-stack: grafana: persistence: storageClassName: "" diff --git a/docs/src/how-to/install/team-feature-settings.md b/docs/src/how-to/install/team-feature-settings.md index ff1670f344..21733829f9 100644 --- a/docs/src/how-to/install/team-feature-settings.md +++ b/docs/src/how-to/install/team-feature-settings.md @@ -31,7 +31,9 @@ galley: Note that the lock status is required but has no effect, as it is currently not supported for team admins to enable or disable `sndFactorPasswordChallenge`. We recommend to set the lock status to `locked`. -### Rate limiting of code generation requests +Currently the 2nd factor password challenge if enabled has no effect for SSO users. + +## Rate limiting of code generation requests The default delay between code generation requests is 5 minutes. This setting can be overridden in the Helm charts: @@ -68,7 +70,7 @@ galley: lockStatus: locked ``` -### TTL for nonces +## TTL for nonces Nonces that can be retrieved e.g. by calling `HEAD /nonce/clients` have a default time-to-live of 5 minutes. To change this setting add the following to your Helm overrides in `values/wire-server/values.yaml`: diff --git a/docs/src/release-notes.rst b/docs/src/release-notes.rst index df1f346eab..497af3cca3 100644 --- a/docs/src/release-notes.rst +++ b/docs/src/release-notes.rst @@ -7,5 +7,8 @@ This page previously contained the release notes for the project, and they were However, Github since updated the feature, making this page un-necessary. -You can find the list of releases, including full release notes, at the following link: `Release Notes `_ +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/understand/single-sign-on/main.rst b/docs/src/understand/single-sign-on/main.rst index acfe8ff313..e12c4c69a0 100644 --- a/docs/src/understand/single-sign-on/main.rst +++ b/docs/src/understand/single-sign-on/main.rst @@ -546,6 +546,10 @@ For each put request, you need to provide the full json object. All omitted fie -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 diff --git a/libs/api-bot/src/Network/Wire/Bot/Assert.hs b/libs/api-bot/src/Network/Wire/Bot/Assert.hs index ed40c15e95..3457ee2eef 100644 --- a/libs/api-bot/src/Network/Wire/Bot/Assert.hs +++ b/libs/api-bot/src/Network/Wire/Bot/Assert.hs @@ -1,5 +1,4 @@ {-# LANGUAGE LambdaCase #-} -{-# LANGUAGE OverloadedStrings #-} -- This file is part of the Wire Server implementation. -- diff --git a/libs/api-bot/src/Network/Wire/Bot/Cache.hs b/libs/api-bot/src/Network/Wire/Bot/Cache.hs index 1a8b41d2cd..5ef86e566b 100644 --- a/libs/api-bot/src/Network/Wire/Bot/Cache.hs +++ b/libs/api-bot/src/Network/Wire/Bot/Cache.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} -- This file is part of the Wire Server implementation. @@ -64,13 +65,12 @@ empty :: IO Cache empty = Cache <$> newIORef [] get :: (MonadIO m, HasCallStack) => Cache -> m CachedUser -get c = liftIO . atomicModifyIORef (cache c) $ \u -> - case u of - [] -> - error - "Cache.get: an account was requested from the cache, \ - \but the cache of available user accounts is empty" - (x : xs) -> (xs, x) +get c = liftIO . atomicModifyIORef (cache c) $ \case + [] -> + error + "Cache.get: an account was requested from the cache, \ + \but the cache of available user accounts is empty" + (x : xs) -> (xs, x) put :: MonadIO m => Cache -> CachedUser -> m () put c a = liftIO . atomicModifyIORef (cache c) $ \u -> (a : u, ()) diff --git a/libs/api-client/src/Network/Wire/Client/Monad.hs b/libs/api-client/src/Network/Wire/Client/Monad.hs index cb6af59bd3..ffaff6ea46 100644 --- a/libs/api-client/src/Network/Wire/Client/Monad.hs +++ b/libs/api-client/src/Network/Wire/Client/Monad.hs @@ -1,6 +1,5 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE OverloadedStrings #-} -- This file is part of the Wire Server implementation. -- diff --git a/libs/bilge/src/Bilge/Request.hs b/libs/bilge/src/Bilge/Request.hs index 99d4acfc38..ce095faef5 100644 --- a/libs/bilge/src/Bilge/Request.hs +++ b/libs/bilge/src/Bilge/Request.hs @@ -156,7 +156,7 @@ expectStatus property r = r {Rq.checkResponse = check} | property (HTTP.statusCode (Rq.responseStatus res)) = pure () | otherwise = do some <- Lazy.toStrict <$> brReadSome (Rq.responseBody res) 1024 - throwHttp $ Rq.StatusCodeException (() <$ res) some + throwHttp $ Rq.StatusCodeException (void res) some checkStatus :: (Status -> ResponseHeaders -> CookieJar -> Maybe SomeException) -> Request -> Request checkStatus f r = r {Rq.checkResponse = check} diff --git a/libs/brig-types/src/Brig/Types/Common.hs b/libs/brig-types/src/Brig/Types/Common.hs index d5946b2b20..2b95cb7639 100644 --- a/libs/brig-types/src/Brig/Types/Common.hs +++ b/libs/brig-types/src/Brig/Types/Common.hs @@ -80,7 +80,7 @@ isValidPhonePrefix = isRight . parseOnly e164Prefix -- | get all valid prefixes of a phone number or phone number prefix -- e.g. from +123456789 get prefixes ["+1", "+12", "+123", ..., "+123456789" ] allPrefixes :: Text -> [PhonePrefix] -allPrefixes t = catMaybes $ parsePhonePrefix <$> Text.inits t +allPrefixes t = mapMaybe parsePhonePrefix (Text.inits t) instance FromJSON PhonePrefix where parseJSON = withText "PhonePrefix" $ \s -> diff --git a/libs/extended/src/Servant/API/Extended.hs b/libs/extended/src/Servant/API/Extended.hs index e045711599..f87fc5d214 100644 --- a/libs/extended/src/Servant/API/Extended.hs +++ b/libs/extended/src/Servant/API/Extended.hs @@ -94,7 +94,7 @@ instance requestHeaders request case canHandleCTypeH (Proxy :: Proxy list) (cs contentTypeH) :: Maybe (BL.ByteString -> Either String a) of Nothing -> delayedFail err415 - Just f -> return f + Just f -> pure f -- Body check, we get a body parsing functions as the first argument. bodyCheck :: (BL.ByteString -> Either String a) -> @@ -102,10 +102,10 @@ instance bodyCheck f = withRequest $ \request -> do mrqbody <- fmapL (makeCustomError @tag @a) . f <$> liftIO (lazyRequestBody request) case sbool :: SBool (FoldLenient mods) of - STrue -> return mrqbody + STrue -> pure mrqbody SFalse -> case mrqbody of Left e -> delayedFailFatal e - Right v -> return v + Right v -> pure v instance HasSwagger (ReqBody' '[Required, Strict] cts a :> api) => diff --git a/libs/galley-types/src/Galley/Types/Conversations/Members.hs b/libs/galley-types/src/Galley/Types/Conversations/Members.hs index 12926efced..eb602cfe10 100644 --- a/libs/galley-types/src/Galley/Types/Conversations/Members.hs +++ b/libs/galley-types/src/Galley/Types/Conversations/Members.hs @@ -33,7 +33,6 @@ where import Data.Domain import Data.Id as Id import Data.Qualified -import qualified Data.Set as Set import Imports import Wire.API.Conversation import Wire.API.Conversation.Role (RoleName, roleNameWireAdmin) @@ -42,8 +41,7 @@ import Wire.API.Provider.Service (ServiceRef) -- | Internal (cassandra) representation of a remote conversation member. data RemoteMember = RemoteMember { rmId :: Remote UserId, - rmConvRoleName :: RoleName, - rmMLSClients :: Set ClientId + rmConvRoleName :: RoleName } deriving stock (Show) @@ -63,8 +61,7 @@ data LocalMember = LocalMember { lmId :: UserId, lmStatus :: MemberStatus, lmService :: Maybe ServiceRef, - lmConvRoleName :: RoleName, - lmMLSClients :: Set ClientId + lmConvRoleName :: RoleName } deriving stock (Show) @@ -77,8 +74,7 @@ newMemberWithRole (u, r) = { lmId = u, lmService = Nothing, lmStatus = defMemberStatus, - lmConvRoleName = r, - lmMLSClients = Set.empty + lmConvRoleName = r } localMemberToOther :: Domain -> LocalMember -> OtherMember diff --git a/libs/hscim/src/Web/Scim/Client.hs b/libs/hscim/src/Web/Scim/Client.hs index abbafc62c1..477e90b427 100644 --- a/libs/hscim/src/Web/Scim/Client.hs +++ b/libs/hscim/src/Web/Scim/Client.hs @@ -75,7 +75,7 @@ type HasScimClient tag = ) scimClients :: HasScimClient tag => ClientEnv -> Site tag (AsClientT IO) -scimClients env = genericClientHoist $ \x -> runClientM x env >>= either throwIO return +scimClients env = genericClientHoist $ \x -> runClientM x env >>= either throwIO pure -- config @@ -130,7 +130,7 @@ postUser :: HasScimClient tag => ClientEnv -> Maybe (AuthData tag) -> - (User tag) -> + User tag -> IO (StoredUser tag) postUser env tok = case users (scimClients env) tok of ((_ :<|> (_ :<|> r)) :<|> (_ :<|> (_ :<|> _))) -> r @@ -139,7 +139,7 @@ putUser :: ClientEnv -> Maybe (AuthData tag) -> UserId tag -> - (User tag) -> + User tag -> IO (StoredUser tag) putUser env tok = case users (scimClients env) tok of ((_ :<|> (_ :<|> _)) :<|> (r :<|> (_ :<|> _))) -> r diff --git a/libs/hscim/src/Web/Scim/ContentType.hs b/libs/hscim/src/Web/Scim/ContentType.hs index 3e9d203c89..4b6a2e803f 100644 --- a/libs/hscim/src/Web/Scim/ContentType.hs +++ b/libs/hscim/src/Web/Scim/ContentType.hs @@ -14,6 +14,9 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . +{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} + +{-# HLINT ignore "Use list literal" #-} -- | SCIM defines its own content type (application/scim+json). It's -- intended to be used for all requests and responses; see the first diff --git a/libs/hscim/src/Web/Scim/Filter.hs b/libs/hscim/src/Web/Scim/Filter.hs index 46b96e7041..5862f6a36b 100644 --- a/libs/hscim/src/Web/Scim/Filter.hs +++ b/libs/hscim/src/Web/Scim/Filter.hs @@ -57,7 +57,7 @@ module Web.Scim.Filter ) where -import Control.Applicative (optional, (<|>)) +import Control.Applicative (optional) import Data.Aeson as Aeson import Data.Aeson.Parser as Aeson import Data.Aeson.Text as Aeson @@ -176,7 +176,7 @@ parseFilter supportedSchemas = -- @ pAttrPath :: [Schema] -> Parser AttrPath pAttrPath supportedSchemas = do - schema <- (Just <$> (pSchema supportedSchemas <* char ':')) <|> pure Nothing + schema <- optional (pSchema supportedSchemas <* char ':') AttrPath schema <$> pAttrName <*> optional pSubAttr -- | subAttr = "." ATTRNAME @@ -193,8 +193,8 @@ pCompValue :: Parser CompValue pCompValue = choice [ ValNull <$ string "null", - ValBool True <$ (stringCI "true"), - ValBool False <$ (stringCI "false"), + ValBool True <$ stringCI "true", + ValBool False <$ stringCI "false", ValNumber <$> Aeson.scientific, ValString <$> Aeson.jstring ] diff --git a/libs/hscim/src/Web/Scim/Schema/PatchOp.hs b/libs/hscim/src/Web/Scim/Schema/PatchOp.hs index 7c4fb3b692..686e58b3ba 100644 --- a/libs/hscim/src/Web/Scim/Schema/PatchOp.hs +++ b/libs/hscim/src/Web/Scim/Schema/PatchOp.hs @@ -105,16 +105,16 @@ operationFromJSON schemas' = let o = KeyMap.fromList . map (first lowerKey) . KeyMap.toList $ v Operation <$> (o .: "op") - <*> (Aeson.explicitParseFieldMaybe (pathFromJSON schemas') o "path") + <*> Aeson.explicitParseFieldMaybe (pathFromJSON schemas') o "path" <*> (o .:? "value") pathFromJSON :: [Schema] -> Value -> Aeson.Parser Path pathFromJSON schemas' = - withText "Path" $ either fail pure . (parsePath schemas') + withText "Path" $ either fail pure . parsePath schemas' instance ToJSON Operation where toJSON (Operation op' path' value') = - object $ ("op" .= op') : concat [optionalField "path" path', optionalField "value" value'] + object $ ("op" .= op') : optionalField "path" path' ++ optionalField "value" value' where optionalField fname = \case Nothing -> [] diff --git a/libs/hscim/src/Web/Scim/Schema/User.hs b/libs/hscim/src/Web/Scim/Schema/User.hs index 4525ccb275..2da8ae9b31 100644 --- a/libs/hscim/src/Web/Scim/Schema/User.hs +++ b/libs/hscim/src/Web/Scim/Schema/User.hs @@ -3,7 +3,6 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE UndecidableInstances #-} -{-# LANGUAGE ViewPatterns #-} -- This file is part of the Wire Server implementation. -- @@ -349,7 +348,7 @@ instance (UserTypes tag, FromJSON (User tag), Patchable (UserExtra tag)) => Patc | isUserSchema schema = applyUserOperation user op | isSupportedCustomSchema schema = (\x -> user {extra = x}) <$> applyOperation (extra user) op | otherwise = - throwError $ badRequest InvalidPath $ Just $ "we only support these schemas: " <> (Text.intercalate ", " $ map getSchemaUri (supportedSchemas @tag)) + throwError $ badRequest InvalidPath $ Just $ "we only support these schemas: " <> Text.intercalate ", " (map getSchemaUri (supportedSchemas @tag)) where isSupportedCustomSchema = maybe False (`elem` supportedSchemas @tag) applyOperation user op = applyUserOperation user op diff --git a/libs/hscim/src/Web/Scim/Server.hs b/libs/hscim/src/Web/Scim/Server.hs index 9b3fe16bed..2d51c43715 100644 --- a/libs/hscim/src/Web/Scim/Server.hs +++ b/libs/hscim/src/Web/Scim/Server.hs @@ -94,7 +94,6 @@ siteServer conf = users = \authData -> toServant (userServer @tag authData), groups = \authData -> toServant (groupServer @tag authData) } - where ---------------------------------------------------------------------------- -- Server-starting utilities diff --git a/libs/hscim/src/Web/Scim/Server/Mock.hs b/libs/hscim/src/Web/Scim/Server/Mock.hs index d92adc109f..adeccdb05d 100644 --- a/libs/hscim/src/Web/Scim/Server/Mock.hs +++ b/libs/hscim/src/Web/Scim/Server/Mock.hs @@ -104,7 +104,7 @@ instance UserTypes Mock where instance UserDB Mock TestServer where getUsers () mbFilter = do - m <- userDB <$> ask + m <- asks userDB users <- liftSTM $ ListT.toList $ STMMap.listT m let check user = case mbFilter of Nothing -> pure True @@ -116,20 +116,20 @@ instance UserDB Mock TestServer where fromList . sortWith (Common.id . thing) <$> filterM check (snd <$> users) getUser () uid = do - m <- userDB <$> ask + m <- asks userDB liftSTM (STMMap.lookup uid m) >>= \case Nothing -> throwScim (notFound "User" (pack (show uid))) Just x -> pure x postUser () user = do - m <- userDB <$> ask + m <- asks userDB uid <- Id <$> liftSTM (STMMap.size m) let newUser = WithMeta (createMeta UserResource) $ WithId uid user liftSTM $ STMMap.insert newUser uid m - return newUser + pure newUser putUser () uid user = do - m <- userDB <$> ask + m <- asks userDB liftSTM (STMMap.lookup uid m) >>= \case Nothing -> throwScim (notFound "User" (pack (show uid))) Just stored -> do @@ -138,7 +138,7 @@ instance UserDB Mock TestServer where pure newUser deleteUser () uid = do - m <- userDB <$> ask + m <- asks userDB liftSTM (STMMap.lookup uid m) >>= \case Nothing -> throwScim (notFound "User" (pack (show uid))) Just _ -> liftSTM $ STMMap.delete uid m @@ -155,25 +155,25 @@ instance GroupTypes Mock where instance GroupDB Mock TestServer where getGroups () = do - m <- groupDB <$> ask + m <- asks groupDB groups <- liftSTM $ ListT.toList $ STMMap.listT m - return $ fromList . sortWith (Common.id . thing) $ snd <$> groups + pure $ fromList . sortWith (Common.id . thing) $ snd <$> groups getGroup () gid = do - m <- groupDB <$> ask + m <- asks groupDB liftSTM (STMMap.lookup gid m) >>= \case Nothing -> throwScim (notFound "Group" (pack (show gid))) Just grp -> pure grp postGroup () grp = do - m <- groupDB <$> ask + m <- asks groupDB gid <- Id <$> liftSTM (STMMap.size m) let newGroup = WithMeta (createMeta GroupResource) $ WithId gid grp liftSTM $ STMMap.insert newGroup gid m - return newGroup + pure newGroup putGroup () gid grp = do - m <- groupDB <$> ask + m <- asks groupDB liftSTM (STMMap.lookup gid m) >>= \case Nothing -> throwScim (notFound "Group" (pack (show gid))) Just stored -> do @@ -184,7 +184,7 @@ instance GroupDB Mock TestServer where patchGroup _ _ _ = throwScim (serverError "PATCH /Users not implemented") deleteGroup () gid = do - m <- groupDB <$> ask + m <- asks groupDB liftSTM (STMMap.lookup gid m) >>= \case Nothing -> throwScim (notFound "Group" (pack (show gid))) Just _ -> liftSTM $ STMMap.delete gid m @@ -243,7 +243,7 @@ filterUser :: Filter -> User extra -> Either Text Bool filterUser (FilterAttrCompare (AttrPath schema' attrib subAttr) op val) user | isUserSchema schema' = case (subAttr, val) of - (Nothing, (ValString str)) + (Nothing, ValString str) | attrib == "userName" -> Right (compareStr op (CI.foldCase (userName user)) (CI.foldCase str)) (Nothing, _) diff --git a/libs/hscim/src/Web/Scim/Test/Acceptance.hs b/libs/hscim/src/Web/Scim/Test/Acceptance.hs index 12e165375d..c4eea4122a 100644 --- a/libs/hscim/src/Web/Scim/Test/Acceptance.hs +++ b/libs/hscim/src/Web/Scim/Test/Acceptance.hs @@ -264,7 +264,7 @@ microsoftAzure AcceptanceConfig {..} = do -- Delete User delete' queryConfig ("/Users/" <> testuid) "" `shouldRespondWith` 204 delete' queryConfig ("/Users/" <> testuid) "" `shouldEventuallyRespondWith` 404 - it "Group operations" $ \_ -> pending + it "Group operations" $ const pending sampleUser1 :: Text -> L.ByteString sampleUser1 userName1 = diff --git a/libs/libzauth/libzauth-c/Cargo.lock b/libs/libzauth/libzauth-c/Cargo.lock index 71636e381b..7acceb85ae 100644 --- a/libs/libzauth/libzauth-c/Cargo.lock +++ b/libs/libzauth/libzauth-c/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +dependencies = [ + "memchr", +] + [[package]] name = "asexp" version = "0.3.2" @@ -23,6 +32,12 @@ dependencies = [ "signature", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.125" @@ -41,12 +56,35 @@ dependencies = [ "walkdir", ] +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + [[package]] name = "pkg-config" version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +[[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + [[package]] name = "rustc-serialize" version = "0.3.24" @@ -130,9 +168,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "zauth" -version = "3.0.0" +version = "3.1.0" dependencies = [ "asexp", + "lazy_static", + "regex", "rustc-serialize", "sodiumoxide", ] diff --git a/libs/libzauth/libzauth/Cargo.toml b/libs/libzauth/libzauth/Cargo.toml index bcff7126d8..34920bd6f2 100644 --- a/libs/libzauth/libzauth/Cargo.toml +++ b/libs/libzauth/libzauth/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zauth" -version = "3.0.0" +version = "3.1.0" authors = ["Wire Swiss GmbH "] license = "AGPL-3.0" @@ -11,6 +11,8 @@ name = "zauth" asexp = ">= 0.3" rustc-serialize = ">= 0.3" sodiumoxide = "^0.2.7" +regex = "1.6" +lazy_static = "1.4" [dev-dependencies] clap = ">= 2.0" diff --git a/libs/libzauth/libzauth/src/acl.rs b/libs/libzauth/libzauth/src/acl.rs index a14ab697f5..0920d06602 100644 --- a/libs/libzauth/libzauth/src/acl.rs +++ b/libs/libzauth/libzauth/src/acl.rs @@ -15,31 +15,34 @@ // You should have received a copy of the GNU Affero General Public License along // with this program. If not, see . -use std::collections::HashMap; use asexp::Sexp; -use tree::Tree; +use matcher::{Item, Matcher}; +use std::collections::HashMap; #[derive(Debug, Clone)] pub enum Error { - Parse(&'static str) + Parse(&'static str), } pub type AclResult = Result; #[derive(Debug, Clone)] pub struct Acl { - acl: HashMap + acl: HashMap, } impl Acl { pub fn new() -> Acl { - Acl { acl: HashMap::new() } + Acl { + acl: HashMap::new(), + } } pub fn from_str(s: &str) -> AclResult { - match Sexp::parse_toplevel(s) { - Err(()) => Err(Error::Parse("invalid s-expressions")), - Ok(sexp) => Acl::from_sexp(&sexp) + let sexp = Sexp::parse_toplevel(s); + match sexp { + Err(()) => Err(Error::Parse("invalid s-expressions")), + Ok(sexp) => Acl::from_sexp(&sexp), } } @@ -51,31 +54,30 @@ impl Acl { if let Some(k) = key.get_str().map(String::from) { acl.insert(k, List::from_sexp(&list)?); } else { - return Err(Error::Parse("not a string")) + return Err(Error::Parse("not a string")); } } Ok(Acl { acl }) } - _ => Err(Error::Parse("expected key and values")) + _ => Err(Error::Parse("expected key and values")), } } pub fn allowed(&self, key: &str, path: &str) -> bool { - self.acl.get(key).map(|list| { - match *list { - List::Black(Some(ref t)) => !t.contains(path), - List::Black(None) => true, - List::White(Some(ref t)) => t.contains(path), - List::White(None) => false - } - }).unwrap_or(false) + self.acl + .get(key) + .map(|list| match *list { + List::Black(ref t) => !t.contains(path), + List::White(ref t) => t.contains(path), + }) + .unwrap_or(false) } } #[derive(Debug, Clone)] enum List { - Black(Option), - White(Option) + Black(Matcher), + White(Matcher), } impl List { @@ -83,50 +85,38 @@ impl List { let items = match *s { Sexp::Tuple(ref a) => a.as_slice(), Sexp::Array(ref a) => a.as_slice(), - _ => return Err(Error::Parse("s-expr not a list")) + _ => return Err(Error::Parse("s-expr not a list")), }; if items.is_empty() { - return Err(Error::Parse("list is empty")) + return Err(Error::Parse("list is empty")); } match items[0].get_str() { - Some("blacklist") => List::items(&items[1 ..]).map(List::Black), - Some("whitelist") => List::items(&items[1 ..]).map(List::White), - _ => Err(Error::Parse("'blacklist' or 'whitelist' expected")) + Some("blacklist") => List::items(&items[1..]).map(List::Black), + Some("whitelist") => List::items(&items[1..]).map(List::White), + _ => Err(Error::Parse("'blacklist' or 'whitelist' expected")), } } - fn items(xs: &[Sexp]) -> AclResult> { - match xs.len() { - 0 => Ok(None), - 1 if List::is_unit(&xs[0]) => Ok(None), - _ => { - let mut t = Tree::new(); - for x in xs { - t.add(&List::read_path(x)?) - } - Ok(Some(t)) - } - } + fn items(xs: &[Sexp]) -> AclResult { + let items: AclResult> = xs.iter().map(List::read_path).collect(); + let m = Matcher::new(&items?); + Ok(m) } - fn is_unit(s: &Sexp) -> bool { - match *s { - Sexp::Tuple(ref a) if a.is_empty() => true, - _ => false - } - } - - fn read_path(s: &Sexp) -> AclResult { + fn read_path(s: &Sexp) -> AclResult { match *s { Sexp::Tuple(ref a) | Sexp::Array(ref a) if a.len() == 2 => { match (a[0].get_str(), a[1].get_str()) { - (Some("path"), Some(x)) => Ok(String::from(x)), - _ => Err(Error::Parse("'path' not found")) + (Some("path"), Some(x)) => Ok(Item::Str(String::from(x))), + (Some("regex"), Some(x)) => { + Ok(Item::Regex(String::from(x))) + } + _ => Err(Error::Parse("'path' not found")), } } - _ => return Err(Error::Parse("s-expr not a list")) + _ => return Err(Error::Parse("s-expr not a list")), } } } @@ -144,14 +134,15 @@ mod tests { (path "/a/**")) b (whitelist (path "/conversation/message") - (path "/foo/bar/*")) + (path "/foo/bar/*") + (regex "(/v[0-9]+)?/foo/baz/[^/]+")) # this is a comment that should not lead to a parse failure. la (whitelist (path "/legalhold/**")) - x (blacklist ()) + x (blacklist) - y (whitelist ()) + y (whitelist) "#; #[test] @@ -165,8 +156,12 @@ mod tests { assert!(!acl.allowed("u", "/x/here/z")); assert!(acl.allowed("u", "/x/here/z/x")); assert!(acl.allowed("b", "/conversation/message")); - assert!(acl.allowed("b", "/foo/bar/baz")); + assert!(acl.allowed("b", "/foo/bar/quux")); + assert!(!acl.allowed("b", "/foo/bar/")); + assert!(acl.allowed("b", "/foo/baz/quux")); assert!(!acl.allowed("b", "/foo/bar/")); + assert!(acl.allowed("b", "/v97/foo/baz/quux")); + assert!(!acl.allowed("b", "/voo/foo/baz/quux")); assert!(!acl.allowed("b", "/anywhere/else/")); assert!(acl.allowed("x", "/everywhere")); assert!(acl.allowed("x", "/")); diff --git a/libs/libzauth/libzauth/src/lib.rs b/libs/libzauth/libzauth/src/lib.rs index 192fa31b6f..7aed0ff371 100644 --- a/libs/libzauth/libzauth/src/lib.rs +++ b/libs/libzauth/libzauth/src/lib.rs @@ -16,6 +16,8 @@ // with this program. If not, see . extern crate asexp; +extern crate lazy_static; +extern crate regex; extern crate rustc_serialize; extern crate sodiumoxide; @@ -23,7 +25,7 @@ pub mod acl; pub mod error; pub mod zauth; -mod tree; +mod matcher; pub use acl::Acl; pub use error::Error; diff --git a/libs/libzauth/libzauth/src/matcher.rs b/libs/libzauth/libzauth/src/matcher.rs new file mode 100644 index 0000000000..622ba8be98 --- /dev/null +++ b/libs/libzauth/libzauth/src/matcher.rs @@ -0,0 +1,118 @@ +// 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 . + +use lazy_static::lazy_static; +use regex::Regex; + +pub enum Item { + Str(String), + Regex(String), +} + +#[derive(Debug, Clone)] +pub struct Matcher { + regex: Option, +} + +lazy_static! { + static ref SLASHES: Regex = Regex::new("/+").unwrap(); + static ref DOUBLE_STAR_PATTERN: Regex = Regex::new(r#"\\\*\\\*"#).unwrap(); + static ref STAR_PATTERN: Regex = Regex::new(r#"\\\*"#).unwrap(); +} + +impl Matcher { + pub fn new(items: &Vec) -> Self { + if items.len() == 0 { + return Self { regex: None }; + } + + let items = items + .iter() + .map(|item| match item { + Item::Str(item) => { + let item = SLASHES.replace_all(item, "/"); + let item = item.trim_end_matches("/"); + let pattern = regex::escape(item); + let pattern = + DOUBLE_STAR_PATTERN.replace_all(&pattern, ".*"); + let pattern = STAR_PATTERN.replace_all(&pattern, "[^/]+"); + + let mut text = String::new(); + text.push_str("("); + text.push_str(&pattern); + text.push_str(")"); + text + } + Item::Regex(r) => r.clone(), + }) + .collect::>(); + + let mut pattern = String::new(); + pattern.push_str("^("); + pattern.push_str(&items.join("|")); + pattern.push_str(")$"); + Self { + regex: Some(Regex::new(&pattern).unwrap()), + } + } + + pub fn contains(&self, s: &str) -> bool { + match &self.regex { + None => false, + Some(r) => { + let s = SLASHES.replace_all(s, "/"); + let s = s.trim_end_matches("/"); + r.is_match(&s) + } + } + } +} + +// Tests //////////////////////////////////////////////////////////////////// + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test() { + let mut items = Vec::new(); + items.push(Item::Str("/foo".to_string())); + items.push(Item::Str("/foo/bar/baz".to_string())); + items.push(Item::Str("/x/y/".to_string())); + items.push(Item::Str("/i/**".to_string())); + items.push(Item::Str("/j/*".to_string())); + items.push(Item::Str("/k/v*".to_string())); + items.push(Item::Str("/a//c".to_string())); + items.push(Item::Regex("(/v[0-9]+)?/notifications".to_string())); + let t = Matcher::new(&items); + + assert!(t.contains("/foo")); + assert!(t.contains("/foo/bar/baz")); + assert!(t.contains("/x/y")); + assert!(!t.contains("/foo/bar")); + assert!(t.contains("/a/c")); + assert!(!t.contains("/a")); + assert!(t.contains("/i/foo")); + assert!(t.contains("/i/foo/zoo")); + assert!(t.contains("/j/foo")); + assert!(!t.contains("/j/foo/zoo")); + assert!(t.contains("/notifications")); + assert!(t.contains("/v33/notifications")); + assert!(!t.contains("/versions/notifications")); + } +} diff --git a/libs/libzauth/libzauth/src/tree.rs b/libs/libzauth/libzauth/src/tree.rs deleted file mode 100644 index 2a55075e0f..0000000000 --- a/libs/libzauth/libzauth/src/tree.rs +++ /dev/null @@ -1,97 +0,0 @@ -// 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 . - -//! Internal module to provide efficient lookup trees for paths. -//! Actually a port of wai-zauth's Network.Wai.Zauth.Tree with -//! the addtional support for "deep wildcards" (specified with "**"). - -use std::collections::HashMap; -use std::collections::hash_map::Entry; - -#[derive(Debug, Clone)] -pub struct Tree { - end_marker: bool, - subtree: HashMap -} - -impl Tree { - pub fn new() -> Tree { - Tree { - end_marker: false, - subtree: HashMap::new() - } - } - - pub fn add(&mut self, s: &str) { - add_parts(self, s.split('/').filter(|s| !s.is_empty())) - } - - pub fn contains(&self, s: &str) -> bool { - let mut tree = self; - for p in s.split('/').filter(|s| !s.is_empty()) { - match tree.subtree.get(p).or_else(|| tree.subtree.get("*")) { - None => return tree.subtree.get("**").is_some(), - Some(t) => tree = t - } - } - tree.end_marker - } -} - -fn add_parts<'a, I>(tree: &mut Tree, mut s: I) - where I: Iterator { - match s.next() { - None => tree.end_marker = true, - Some(p) => { - let next = - match tree.subtree.entry(String::from(p)) { - Entry::Vacant(e) => e.insert(Tree::new()), - Entry::Occupied(e) => e.into_mut() - }; - add_parts(next, s) - } - } -} - -// Tests //////////////////////////////////////////////////////////////////// - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test() { - let mut t = Tree::new(); - t.add("/foo"); - t.add("/foo/bar/baz"); - t.add("/x/y/"); - t.add("/i/**"); - t.add("/j/*"); - t.add("/a//c"); - - assert!(t.contains("/foo")); - assert!(t.contains("/foo/bar/baz")); - assert!(t.contains("/x/y")); - assert!(!t.contains("/foo/bar")); - assert!(t.contains("/a/c")); - assert!(!t.contains("/a")); - assert!(t.contains("/i/foo")); - assert!(t.contains("/i/foo/zoo")); - assert!(t.contains("/j/foo")); - assert!(!t.contains("/j/foo/zoo")); - } -} diff --git a/libs/metrics-wai/src/Data/Metrics/Test.hs b/libs/metrics-wai/src/Data/Metrics/Test.hs index 50e07b9811..308824e2fd 100644 --- a/libs/metrics-wai/src/Data/Metrics/Test.hs +++ b/libs/metrics-wai/src/Data/Metrics/Test.hs @@ -48,7 +48,7 @@ pathsConsistencyCheck (Paths forest) = mconcat $ go [] <$> forest where here = findSiteConsistencyError (reverse $ root : prefix) trees findSiteConsistencyError :: [PathSegment] -> Tree.Forest PathSegment -> Maybe SiteConsistencyError - findSiteConsistencyError prefix subtrees = case catMaybes $ captureVars <$> subtrees of + findSiteConsistencyError prefix subtrees = case mapMaybe captureVars subtrees of [] -> Nothing [_] -> Nothing bad@(_ : _ : _) -> Just $ SiteConsistencyError (either cs cs <$> prefix) bad diff --git a/libs/ropes/src/Ropes/Twilio.hs b/libs/ropes/src/Ropes/Twilio.hs index 5696708135..9776ffffde 100644 --- a/libs/ropes/src/Ropes/Twilio.hs +++ b/libs/ropes/src/Ropes/Twilio.hs @@ -1,4 +1,5 @@ {-# LANGUAGE DeriveDataTypeable #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} -- This file is part of the Wire Server implementation. @@ -140,12 +141,11 @@ instance FromJSON CarrierInfo where <*> o .:? "type" instance FromJSON PhoneType where - parseJSON = withText "PhoneType" $ \t -> - case t of - "mobile" -> pure Mobile - "landline" -> pure Landline - "voip" -> pure VoIp - x -> fail $ "Unexpected phone type: " ++ show x + parseJSON = withText "PhoneType" $ \case + "mobile" -> pure Mobile + "landline" -> pure Landline + "voip" -> pure VoIp + x -> fail $ "Unexpected phone type: " ++ show x -- * Functions diff --git a/libs/schema-profunctor/src/Data/Schema.hs b/libs/schema-profunctor/src/Data/Schema.hs index 3ff9e560f3..f2cdd73dcb 100644 --- a/libs/schema-profunctor/src/Data/Schema.hs +++ b/libs/schema-profunctor/src/Data/Schema.hs @@ -888,8 +888,11 @@ instance S.HasSchema NamedSwaggerDoc S.Schema where instance S.HasSchema d S.Schema => S.HasSchema (SchemaP d v w a b) S.Schema where schema = doc . S.schema -instance S.HasDescription SwaggerDoc (Maybe Text) where - description = declared . S.description - instance S.HasDescription NamedSwaggerDoc (Maybe Text) where description = declared . S.schema . S.description + +instance {-# OVERLAPPABLE #-} S.HasDescription s a => S.HasDescription (WithDeclare s) a where + description = declared . S.description + +instance {-# OVERLAPPABLE #-} S.HasExample s a => S.HasExample (WithDeclare s) a where + example = declared . S.example diff --git a/libs/tasty-cannon/src/Test/Tasty/Cannon.hs b/libs/tasty-cannon/src/Test/Tasty/Cannon.hs index f7f556886f..8b0b437277 100644 --- a/libs/tasty-cannon/src/Test/Tasty/Cannon.hs +++ b/libs/tasty-cannon/src/Test/Tasty/Cannon.hs @@ -288,8 +288,8 @@ awaitMatch :: (HasCallStack, MonadIO m, MonadCatch m) => Timeout -> WebSocket -> - (Notification -> Assertion) -> - m (Either MatchTimeout Notification) + (Notification -> IO a) -> + m (Either MatchTimeout a) awaitMatch t ws match = go [] [] where go buf errs = do @@ -297,9 +297,9 @@ awaitMatch t ws match = go [] [] case mn of Just n -> do - liftIO (match n) + a <- liftIO (match n) refill buf - pure (Right n) + pure (Right a) `catchAll` \e -> case asyncExceptionFromException e of Just x -> throwM (x :: SomeAsyncException) Nothing -> @@ -322,15 +322,15 @@ assertMatch :: (HasCallStack, MonadIO m, MonadCatch m) => Timeout -> WebSocket -> - (Notification -> Assertion) -> - m Notification + (Notification -> IO a) -> + m a assertMatch t ws f = awaitMatch t ws f >>= assertSuccess assertMatch_ :: (HasCallStack, MonadIO m, MonadCatch m) => Timeout -> WebSocket -> - (Notification -> Assertion) -> + (Notification -> IO a) -> m () assertMatch_ t w = void . assertMatch t w @@ -338,40 +338,40 @@ awaitMatchN :: (HasCallStack, MonadIO m) => Timeout -> [WebSocket] -> - (Notification -> Assertion) -> - m [Either MatchTimeout Notification] + (Notification -> IO a) -> + m [Either MatchTimeout a] awaitMatchN t wss f = snd <$$> awaitMatchN' t (((),) <$> wss) f awaitMatchN' :: (HasCallStack, MonadIO m) => Timeout -> [(extra, WebSocket)] -> - (Notification -> Assertion) -> - m [(extra, Either MatchTimeout Notification)] + (Notification -> IO a) -> + m [(extra, Either MatchTimeout a)] awaitMatchN' t wss f = liftIO $ mapConcurrently (\(extra, ws) -> (extra,) <$> awaitMatch t ws f) wss assertMatchN :: (HasCallStack, MonadIO m, MonadThrow m) => Timeout -> [WebSocket] -> - (Notification -> Assertion) -> - m [Notification] + (Notification -> IO a) -> + m [a] assertMatchN t wss f = awaitMatchN t wss f >>= mapM assertSuccess assertMatchN_ :: (HasCallStack, MonadIO m, MonadThrow m) => Timeout -> [WebSocket] -> - (Notification -> Assertion) -> + (Notification -> IO a) -> m () assertMatchN_ t wss f = void $ assertMatchN t wss f -assertSuccess :: (HasCallStack, MonadIO m, MonadThrow m) => Either MatchTimeout Notification -> m Notification +assertSuccess :: (HasCallStack, MonadIO m, MonadThrow m) => Either MatchTimeout a -> m a assertSuccess = either throwM pure assertNoEvent :: (HasCallStack, MonadIO m, MonadCatch m) => Timeout -> [WebSocket] -> m () assertNoEvent t ww = do - results <- awaitMatchN' t (zip [(0 :: Int) ..] ww) (const $ pure ()) + results <- awaitMatchN' t (zip [(0 :: Int) ..] ww) pure for_ results $ \(ix, result) -> either (const $ pure ()) (liftIO . f ix) result where diff --git a/libs/types-common-aws/src/AWS/Util.hs b/libs/types-common-aws/src/AWS/Util.hs index dac97a3e6e..1eff3fe67e 100644 --- a/libs/types-common-aws/src/AWS/Util.hs +++ b/libs/types-common-aws/src/AWS/Util.hs @@ -29,4 +29,4 @@ readAuthExpiration env = do AWS.Ref _ ref -> do readIORef ref now <- getCurrentTime - pure $ ((`diffUTCTime` now) . AWS.fromTime) <$> (AWS._authExpiration authEnv) + pure $ (`diffUTCTime` now) . AWS.fromTime <$> AWS._authExpiration authEnv diff --git a/libs/types-common/src/Data/Json/Util.hs b/libs/types-common/src/Data/Json/Util.hs index ab58254a87..daeed51852 100644 --- a/libs/types-common/src/Data/Json/Util.hs +++ b/libs/types-common/src/Data/Json/Util.hs @@ -49,13 +49,12 @@ module Data.Json.Util where import qualified Cassandra as CQL -import Control.Lens (coerced, (%~), (?~)) +import Control.Lens hiding ((#), (.=)) import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson as A import qualified Data.Aeson.Types as A import qualified Data.Attoparsec.Text as Atto import qualified Data.Attoparsec.Time as Atto -import Data.Bifunctor import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Base64.URL as B64U import qualified Data.ByteString.Builder as BB @@ -205,8 +204,11 @@ instance ToHttpApiData Base64ByteString where instance S.ToParamSchema Base64ByteString where toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString +-- base64("example") ~> "ZXhhbXBsZQo=" base64SchemaN :: ValueSchema NamedSwaggerDoc ByteString -base64SchemaN = toBase64Text .= parsedText "Base64ByteString" fromBase64Text +base64SchemaN = + (toBase64Text .= parsedText "Base64ByteString" fromBase64Text) + & doc %~ fmap (S.schema . S.example ?~ A.String "ZXhhbXBsZQo=") base64Schema :: ValueSchema SwaggerDoc ByteString base64Schema = unnamed base64SchemaN diff --git a/libs/types-common/src/Data/Qualified.hs b/libs/types-common/src/Data/Qualified.hs index 12180988f6..0eb22eb4d5 100644 --- a/libs/types-common/src/Data/Qualified.hs +++ b/libs/types-common/src/Data/Qualified.hs @@ -41,6 +41,7 @@ module Data.Qualified indexQualified, bucketQualified, bucketRemote, + isLocal, deprecatedSchema, qualifiedSchema, qualifiedObjectSchema, @@ -157,6 +158,9 @@ bucketRemote = . indexQualified . fmap qUntagged +isLocal :: Local x -> Qualified a -> Bool +isLocal loc = foldQualified loc (const True) (const False) + ---------------------------------------------------------------------- deprecatedSchema :: S.HasDescription doc (Maybe Text) => Text -> ValueSchema doc a -> ValueSchema doc a diff --git a/libs/types-common/src/Data/Range.hs b/libs/types-common/src/Data/Range.hs index 137b1ed840..ffceb0fddf 100644 --- a/libs/types-common/src/Data/Range.hs +++ b/libs/types-common/src/Data/Range.hs @@ -390,13 +390,13 @@ instance Bounds (List1 a) where within x = within (toNonEmpty x) instance Bounds (Set a) where - within x y z = rangeCheck (Set.size x) y z + within x = rangeCheck (Set.size x) instance Bounds (Seq a) where - within x y z = rangeCheck (Seq.length x) y z + within x = rangeCheck (Seq.length x) instance Bounds (Map k a) where - within x y z = rangeCheck (Map.size x) y z + within x = rangeCheck (Map.size x) instance Bounds (HashMap k a) where within x y z = rangeCheck (length (take (fromIntegral z + 1) (HashMap.toList x))) y z @@ -409,7 +409,7 @@ instance Bounds a => Bounds (Maybe a) where within (Just x) y z = within x y z instance Bounds (AsciiText r) where - within x y z = within (Ascii.toText x) y z + within x = within (Ascii.toText x) ----------------------------------------------------------------------------- diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs index 8bbf528649..ea4708a325 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs @@ -122,8 +122,10 @@ newSettings (Server h p l m t) = do -- on receiving either the INT or TERM signals. After closing -- the listen socket, Warp will be allowed to drain existing -- connections up to the given number of seconds. -runSettingsWithShutdown :: Settings -> Application -> Word16 -> IO () -runSettingsWithShutdown s app secs = do +-- +-- See also: https://gitlab.haskell.org/ghc/ghc/-/merge_requests/7681 +runSettingsWithShutdown :: Settings -> Application -> Maybe Word16 -> IO () +runSettingsWithShutdown s app (fromMaybe defaultShutdownTime -> secs) = do initialization latch <- newEmptyMVar let s' = setInstallShutdownHandler (catchSignals latch) s @@ -145,6 +147,9 @@ runSettingsWithShutdown s app secs = do Just (Left ex) -> throwIO ex _ -> cancel srv +defaultShutdownTime :: Word16 +defaultShutdownTime = 30 + compile :: Monad m => Routes a m b -> Tree (App m) compile routes = Route.prepare (Route.renderer predicateError >> routes) where diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs index b654911291..ee690d531d 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs @@ -69,6 +69,18 @@ type GalleyApi = :<|> FedEndpoint "mls-welcome" MLSWelcomeRequest EmptyResponse :<|> FedEndpoint "on-mls-message-sent" RemoteMLSMessage EmptyResponse :<|> FedEndpoint "send-mls-message" MessageSendRequest MLSMessageResponse + :<|> FedEndpoint "send-mls-commit-bundle" MessageSendRequest MLSMessageResponse + :<|> FedEndpoint "query-group-info" GetGroupInfoRequest GetGroupInfoResponse + :<|> FedEndpoint "on-client-removed" ClientRemovedRequest EmptyResponse + +data ClientRemovedRequest = ClientRemovedRequest + { crrUser :: UserId, + crrClient :: ClientId, + crrConvs :: [ConvId] + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform ClientRemovedRequest) + deriving (FromJSON, ToJSON) via (CustomEncoded ClientRemovedRequest) data GetConversationsRequest = GetConversationsRequest { gcrUserId :: UserId, @@ -301,3 +313,21 @@ data MLSMessageResponse | MLSMessageResponseUpdates [ConversationUpdate] deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON) via (CustomEncoded MLSMessageResponse) + +data GetGroupInfoRequest = GetGroupInfoRequest + { -- | Conversation is assumed to be owned by the target domain, this allows + -- us to protect against relay attacks + ggireqConv :: ConvId, + -- | Sender is assumed to be owned by the origin domain, this allows us to + -- protect against spoofing attacks + ggireqSender :: UserId + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform GetGroupInfoRequest) + deriving (ToJSON, FromJSON) via (CustomEncoded GetGroupInfoRequest) + +data GetGroupInfoResponse + = GetGroupInfoResponseError GalleyError + | GetGroupInfoResponseState Base64ByteString + deriving stock (Eq, Show, Generic) + deriving (ToJSON, FromJSON) via (CustomEncoded GetGroupInfoResponse) diff --git a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/ConversationUpdate.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/ConversationUpdate.hs index e19c3b6732..a9eebfde1f 100644 --- a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/ConversationUpdate.hs +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/ConversationUpdate.hs @@ -72,5 +72,5 @@ testObject_ConversationUpdate2 = cuConvId = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000006")), cuAlreadyPresentUsers = [chad, dee], - cuAction = SomeConversationAction (sing @'ConversationLeaveTag) (pure qAlice) + cuAction = SomeConversationAction (sing @'ConversationLeaveTag) () } diff --git a/libs/wire-api-federation/test/golden/testObject_ConversationUpdate2.json b/libs/wire-api-federation/test/golden/testObject_ConversationUpdate2.json index 21f5d72822..8b443934be 100644 --- a/libs/wire-api-federation/test/golden/testObject_ConversationUpdate2.json +++ b/libs/wire-api-federation/test/golden/testObject_ConversationUpdate2.json @@ -1,13 +1,6 @@ { "cuAction": { - "action": { - "users": [ - { - "domain": "golden.example.com", - "id": "00000000-0000-0000-0000-000100004007" - } - ] - }, + "action": {}, "tag": "ConversationLeaveTag" }, "cuAlreadyPresentUsers": [ diff --git a/libs/wire-api-federation/test/golden/testObject_MessageSendReponse1.json b/libs/wire-api-federation/test/golden/testObject_MessageSendReponse1.json index d35a577690..e95ce811b4 100644 --- a/libs/wire-api-federation/test/golden/testObject_MessageSendReponse1.json +++ b/libs/wire-api-federation/test/golden/testObject_MessageSendReponse1.json @@ -1,49 +1,49 @@ { "Right": { - "failed_to_send": { + "deleted": { "golden.example.com": { - "00000000-0000-0000-0000-000200000008": [ - "0" - ], - "00000000-0000-0000-0000-000100000007": [ + "00000000-0000-0000-0000-000100000005": [ "0", "1" + ], + "00000000-0000-0000-0000-000200000006": [ + "0" ] } }, - "redundant": { + "failed_to_send": { "golden.example.com": { - "00000000-0000-0000-0000-000100000003": [ + "00000000-0000-0000-0000-000100000007": [ "0", "1" ], - "00000000-0000-0000-0000-000200000004": [ + "00000000-0000-0000-0000-000200000008": [ "0" ] } }, - "time": "1864-04-12T12:22:43.673Z", "missing": { "golden.example.com": { - "00000000-0000-0000-0000-000200000000": [ - "0" - ], "00000000-0000-0000-0000-000100000002": [ "0", "1" + ], + "00000000-0000-0000-0000-000200000000": [ + "0" ] } }, - "deleted": { + "redundant": { "golden.example.com": { - "00000000-0000-0000-0000-000100000005": [ + "00000000-0000-0000-0000-000100000003": [ "0", "1" ], - "00000000-0000-0000-0000-000200000006": [ + "00000000-0000-0000-0000-000200000004": [ "0" ] } - } + }, + "time": "1864-04-12T12:22:43.673Z" } } \ No newline at end of file diff --git a/libs/wire-api-federation/test/golden/testObject_MessageSendReponse3.json b/libs/wire-api-federation/test/golden/testObject_MessageSendReponse3.json index 21fb6f1f90..7080dfa8c3 100644 --- a/libs/wire-api-federation/test/golden/testObject_MessageSendReponse3.json +++ b/libs/wire-api-federation/test/golden/testObject_MessageSendReponse3.json @@ -1,52 +1,52 @@ { "Left": { - "tag": "MessageNotSentClientMissing", "contents": { - "failed_to_send": { + "deleted": { "golden.example.com": { - "00000000-0000-0000-0000-000200000008": [ - "0" - ], - "00000000-0000-0000-0000-000100000007": [ + "00000000-0000-0000-0000-000100000005": [ "0", "1" + ], + "00000000-0000-0000-0000-000200000006": [ + "0" ] } }, - "redundant": { + "failed_to_send": { "golden.example.com": { - "00000000-0000-0000-0000-000100000003": [ + "00000000-0000-0000-0000-000100000007": [ "0", "1" ], - "00000000-0000-0000-0000-000200000004": [ + "00000000-0000-0000-0000-000200000008": [ "0" ] } }, - "time": "1864-04-12T12:22:43.673Z", "missing": { "golden.example.com": { - "00000000-0000-0000-0000-000200000000": [ - "0" - ], "00000000-0000-0000-0000-000100000002": [ "0", "1" + ], + "00000000-0000-0000-0000-000200000000": [ + "0" ] } }, - "deleted": { + "redundant": { "golden.example.com": { - "00000000-0000-0000-0000-000100000005": [ + "00000000-0000-0000-0000-000100000003": [ "0", "1" ], - "00000000-0000-0000-0000-000200000006": [ + "00000000-0000-0000-0000-000200000004": [ "0" ] } - } - } + }, + "time": "1864-04-12T12:22:43.673Z" + }, + "tag": "MessageNotSentClientMissing" } } \ No newline at end of file diff --git a/libs/wire-api-federation/test/golden/testObject_NewConnectionRequest1.json b/libs/wire-api-federation/test/golden/testObject_NewConnectionRequest1.json index cebe1dfa47..0657122cdb 100644 --- a/libs/wire-api-federation/test/golden/testObject_NewConnectionRequest1.json +++ b/libs/wire-api-federation/test/golden/testObject_NewConnectionRequest1.json @@ -1,5 +1,5 @@ { - "to": "1669240c-c510-43e0-bf1a-33378fa4ba55", + "action": "RemoteConnect", "from": "69f66843-6cf1-48fb-8c05-1cf58c23566a", - "action": "RemoteConnect" + "to": "1669240c-c510-43e0-bf1a-33378fa4ba55" } \ No newline at end of file diff --git a/libs/wire-api-federation/test/golden/testObject_NewConnectionRequest2.json b/libs/wire-api-federation/test/golden/testObject_NewConnectionRequest2.json index 4610970610..32f52b7f30 100644 --- a/libs/wire-api-federation/test/golden/testObject_NewConnectionRequest2.json +++ b/libs/wire-api-federation/test/golden/testObject_NewConnectionRequest2.json @@ -1,5 +1,5 @@ { - "to": "1669240c-c510-43e0-bf1a-33378fa4ba55", + "action": "RemoteRescind", "from": "69f66843-6cf1-48fb-8c05-1cf58c23566a", - "action": "RemoteRescind" + "to": "1669240c-c510-43e0-bf1a-33378fa4ba55" } \ No newline at end of file diff --git a/libs/wire-api-federation/test/golden/testObject_NewConnectionResponse1.json b/libs/wire-api-federation/test/golden/testObject_NewConnectionResponse1.json index 61c94bf0db..8742918c4b 100644 --- a/libs/wire-api-federation/test/golden/testObject_NewConnectionResponse1.json +++ b/libs/wire-api-federation/test/golden/testObject_NewConnectionResponse1.json @@ -1,4 +1,4 @@ { - "tag": "NewConnectionResponseOk", - "contents": null + "contents": null, + "tag": "NewConnectionResponseOk" } \ No newline at end of file diff --git a/libs/wire-api-federation/test/golden/testObject_NewConnectionResponse2.json b/libs/wire-api-federation/test/golden/testObject_NewConnectionResponse2.json index 84fa71d736..d9f4636ea3 100644 --- a/libs/wire-api-federation/test/golden/testObject_NewConnectionResponse2.json +++ b/libs/wire-api-federation/test/golden/testObject_NewConnectionResponse2.json @@ -1,4 +1,4 @@ { - "tag": "NewConnectionResponseOk", - "contents": "RemoteConnect" + "contents": "RemoteConnect", + "tag": "NewConnectionResponseOk" } \ No newline at end of file diff --git a/libs/wire-api-federation/test/golden/testObject_NewConnectionResponse3.json b/libs/wire-api-federation/test/golden/testObject_NewConnectionResponse3.json index aeee3a6db9..d520e8340e 100644 --- a/libs/wire-api-federation/test/golden/testObject_NewConnectionResponse3.json +++ b/libs/wire-api-federation/test/golden/testObject_NewConnectionResponse3.json @@ -1,4 +1,4 @@ { - "tag": "NewConnectionResponseOk", - "contents": "RemoteRescind" + "contents": "RemoteRescind", + "tag": "NewConnectionResponseOk" } \ No newline at end of file diff --git a/libs/wire-api/src/Wire/API/Connection.hs b/libs/wire-api/src/Wire/API/Connection.hs index fa3fc2af11..6da96c6345 100644 --- a/libs/wire-api/src/Wire/API/Connection.hs +++ b/libs/wire-api/src/Wire/API/Connection.hs @@ -168,7 +168,7 @@ data Relation | Cancelled | -- | behaves like blocked, the extra constructor is just to inform why. MissingLegalholdConsent - deriving stock (Eq, Ord, Show, Generic) + deriving stock (Bounded, Enum, Eq, Ord, Show, Generic) deriving (Arbitrary) via (GenericUniform Relation) deriving (FromJSON, ToJSON, S.ToSchema) via (Schema Relation) diff --git a/libs/wire-api/src/Wire/API/Conversation.hs b/libs/wire-api/src/Wire/API/Conversation.hs index f9ad1b122e..affad99eb3 100644 --- a/libs/wire-api/src/Wire/API/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Conversation.hs @@ -722,7 +722,12 @@ newtype ConvTeamInfo = ConvTeamInfo } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ConvTeamInfo) - deriving (FromJSON, ToJSON, S.ToSchema) via Schema ConvTeamInfo + deriving (FromJSON, ToJSON, S.ToSchema) via (Schema ConvTeamInfo) + +managedDesc :: Text +managedDesc = + "This field MUST NOT be used by clients. " + <> "It is here only for backwards compatibility of the interface." instance ToSchema ConvTeamInfo where schema = @@ -734,7 +739,7 @@ instance ToSchema ConvTeamInfo where <* const () .= fieldWithDocModifier "managed" - (description ?~ "(Not parsed any more) Whether this is a managed team conversation") + (description ?~ managedDesc) (c (False :: Bool)) where c :: ToJSON a => a -> ValueSchema SwaggerDoc () @@ -746,7 +751,7 @@ modelTeamInfo = Doc.defineModel "TeamInfo" $ do Doc.property "teamid" Doc.bytes' $ Doc.description "Team ID" Doc.property "managed" Doc.bool' $ - Doc.description "Is this a managed team conversation?" + Doc.description managedDesc -------------------------------------------------------------------------------- -- invite diff --git a/libs/wire-api/src/Wire/API/Conversation/Action.hs b/libs/wire-api/src/Wire/API/Conversation/Action.hs index bd0700a521..2d92ec4365 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Action.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Action.hs @@ -53,7 +53,7 @@ import Wire.Arbitrary (Arbitrary (..)) -- individual effects per conversation action. See 'HasConversationActionEffects'. type family ConversationAction (tag :: ConversationActionTag) :: * where ConversationAction 'ConversationJoinTag = ConversationJoin - ConversationAction 'ConversationLeaveTag = NonEmptyList.NonEmpty (Qualified UserId) + ConversationAction 'ConversationLeaveTag = () ConversationAction 'ConversationMemberUpdateTag = ConversationMemberUpdate ConversationAction 'ConversationDeleteTag = () ConversationAction 'ConversationRenameTag = ConversationRename @@ -87,7 +87,7 @@ conversationActionSchema SConversationLeaveTag = objectWithDocModifier "ConversationLeave" (S.description ?~ "The action of some users leaving a conversation on their own") - $ field "users" (nonEmptyArray schema) + $ pure () conversationActionSchema SConversationRemoveMembersTag = objectWithDocModifier "ConversationRemoveMembers" @@ -151,7 +151,7 @@ conversationActionToEvent tag now quid qcnv action = let ConversationJoin newMembers role = action in EdMembersJoin $ SimpleMembers (map (`SimpleMember` role) (toList newMembers)) SConversationLeaveTag -> - EdMembersLeave (QualifiedUserIdList (toList action)) + EdMembersLeave (QualifiedUserIdList [quid]) SConversationRemoveMembersTag -> EdMembersLeave (QualifiedUserIdList (toList action)) SConversationMemberUpdateTag -> diff --git a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs index 718caa3071..492a2ef68b 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs @@ -78,7 +78,7 @@ protocolTag (ProtocolMLS _) = ProtocolMLSTag protocolValidAction :: Protocol -> ConversationActionTag -> Bool protocolValidAction ProtocolProteus _ = True protocolValidAction (ProtocolMLS _) ConversationJoinTag = False -protocolValidAction (ProtocolMLS _) ConversationLeaveTag = False +protocolValidAction (ProtocolMLS _) ConversationLeaveTag = True protocolValidAction (ProtocolMLS _) ConversationRemoveMembersTag = False protocolValidAction (ProtocolMLS _) ConversationDeleteTag = False protocolValidAction (ProtocolMLS _) _ = True diff --git a/libs/wire-api/src/Wire/API/Error/Brig.hs b/libs/wire-api/src/Wire/API/Error/Brig.hs index 544a3755cd..2ce133f35f 100644 --- a/libs/wire-api/src/Wire/API/Error/Brig.hs +++ b/libs/wire-api/src/Wire/API/Error/Brig.hs @@ -64,6 +64,13 @@ data BrigError | PasswordAuthenticationFailed | TooManyTeamInvitations | InsufficientTeamPermissions + | KeyPackageDecodingError + | InvalidKeyPackageRef + | CustomerExtensionBlockedDomain + | PasswordResetInProgress + | InvalidPasswordResetKey + | InvalidPasswordResetCode + | ResetPasswordMustDiffer instance KnownError (MapError e) => IsSwaggerError (e :: BrigError) where addToSwagger = addStaticErrorToSwagger @(MapError e) @@ -172,3 +179,24 @@ type instance MapError 'PasswordAuthenticationFailed = 'StaticError 403 "passwor type instance MapError 'TooManyTeamInvitations = 'StaticError 403 "too-many-team-invitations" "Too many team invitations for this team" type instance MapError 'InsufficientTeamPermissions = 'StaticError 403 "insufficient-permissions" "Insufficient team permissions" + +type instance MapError 'KeyPackageDecodingError = 'StaticError 409 "decoding-error" "Key package could not be TLS-decoded" + +type instance MapError 'InvalidKeyPackageRef = 'StaticError 409 "invalid-reference" "Key package's reference does not match its data" + +type instance + MapError 'CustomerExtensionBlockedDomain = + 'StaticError + 451 + "domain-blocked-for-registration" + "[Customer extension] the email domain example.com \ + \that you are attempting to register a user with has been \ + \blocked for creating wire users. Please contact your IT department." + +type instance MapError 'PasswordResetInProgress = 'StaticError 409 "code-exists" "A password reset is already in progress." + +type instance MapError 'InvalidPasswordResetKey = 'StaticError 400 "invalid-key" "Invalid email or mobile number for password reset." + +type instance MapError 'InvalidPasswordResetCode = 'StaticError 400 "invalid-code" "Invalid password reset code." + +type instance MapError 'ResetPasswordMustDiffer = 'StaticError 409 "password-must-differ" "For password reset, new and old password must be different." diff --git a/libs/wire-api/src/Wire/API/Error/Galley.hs b/libs/wire-api/src/Wire/API/Error/Galley.hs index 7ba3239c8b..171f894b3d 100644 --- a/libs/wire-api/src/Wire/API/Error/Galley.hs +++ b/libs/wire-api/src/Wire/API/Error/Galley.hs @@ -81,6 +81,8 @@ data GalleyError | MLSSelfRemovalNotAllowed | MLSGroupConversationMismatch | MLSClientSenderUserMismatch + | MLSWelcomeMismatch + | MLSMissingGroupInfo | -- NoBindingTeamMembers | NoBindingTeam @@ -192,13 +194,17 @@ type instance MapError 'MLSClientMismatch = 'StaticError 409 "mls-client-mismatc type instance MapError 'MLSStaleMessage = 'StaticError 409 "mls-stale-message" "The conversation epoch in a message is too old" -type instance MapError 'MLSCommitMissingReferences = 'StaticError 409 "mls-commit-missing-references" "The commit is not refrencing all pending proposals" +type instance MapError 'MLSCommitMissingReferences = 'StaticError 400 "mls-commit-missing-references" "The commit is not referencing all pending proposals" -type instance MapError 'MLSSelfRemovalNotAllowed = 'StaticError 409 "mls-self-removal-not-allowed" "Self removal from group is not allowed" +type instance MapError 'MLSSelfRemovalNotAllowed = 'StaticError 400 "mls-self-removal-not-allowed" "Self removal from group is not allowed" -type instance MapError 'MLSGroupConversationMismatch = 'StaticError 409 "mls-group-conversation-mismatch" "Conversation ID resolved from Group ID does not match submitted Conversation ID" +type instance MapError 'MLSGroupConversationMismatch = 'StaticError 400 "mls-group-conversation-mismatch" "Conversation ID resolved from Group ID does not match submitted Conversation ID" -type instance MapError 'MLSClientSenderUserMismatch = 'StaticError 409 "mls-client-sender-user-mismatch" "User ID resolved from Client ID does not match message's sender user ID" +type instance MapError 'MLSClientSenderUserMismatch = 'StaticError 400 "mls-client-sender-user-mismatch" "User ID resolved from Client ID does not match message's sender user ID" + +type instance MapError 'MLSWelcomeMismatch = 'StaticError 400 "mls-welcome-mismatch" "The list of targets of a welcome message does not match the list of new clients in a group" + +type instance MapError 'MLSMissingGroupInfo = 'StaticError 404 "mls-missing-group-info" "The conversation has no group information" type instance MapError 'NoBindingTeamMembers = 'StaticError 403 "non-binding-team-members" "Both users must be members of the same binding team" diff --git a/libs/wire-api/src/Wire/API/Event/Conversation.hs b/libs/wire-api/src/Wire/API/Event/Conversation.hs index 9571ed8d86..f05018432d 100644 --- a/libs/wire-api/src/Wire/API/Event/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Event/Conversation.hs @@ -400,11 +400,10 @@ eventObjectSchema = mk (_, d) cid uid tm = Event cid uid tm d instance ToJSONObject Event where - toJSONObject e = + toJSONObject = KeyMap.fromList . fromMaybe [] . schemaOut eventObjectSchema - $ e instance FromJSON Event where parseJSON = schemaParseJSON diff --git a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs index 9308a1b1e2..aaf42cd5af 100644 --- a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs +++ b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs @@ -37,7 +37,7 @@ import Wire.Arbitrary newtype CipherSuite = CipherSuite {cipherSuiteNumber :: Word16} deriving stock (Eq, Show) - deriving newtype (ParseMLS, Arbitrary) + deriving newtype (ParseMLS, SerialiseMLS, Arbitrary) instance ToSchema CipherSuite where schema = diff --git a/libs/wire-api/src/Wire/API/MLS/Commit.hs b/libs/wire-api/src/Wire/API/MLS/Commit.hs index 7b4729cf6d..8f1a17c8ce 100644 --- a/libs/wire-api/src/Wire/API/MLS/Commit.hs +++ b/libs/wire-api/src/Wire/API/MLS/Commit.hs @@ -21,6 +21,7 @@ import Imports import Wire.API.MLS.KeyPackage import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation +import Wire.Arbitrary data Commit = Commit { cProposals :: [ProposalOrRef], @@ -53,7 +54,13 @@ data HPKECiphertext = HPKECiphertext { hcOutput :: ByteString, hcCiphertext :: ByteString } - deriving (Eq, Show) + deriving (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform HPKECiphertext) instance ParseMLS HPKECiphertext where parseMLS = HPKECiphertext <$> parseMLSBytes @Word16 <*> parseMLSBytes @Word16 + +instance SerialiseMLS HPKECiphertext where + serialiseMLS (HPKECiphertext out ct) = do + serialiseMLSBytes @Word16 out + serialiseMLSBytes @Word16 ct diff --git a/libs/wire-api/src/Wire/API/MLS/CommitBundle.hs b/libs/wire-api/src/Wire/API/MLS/CommitBundle.hs new file mode 100644 index 0000000000..67ebd6fd5d --- /dev/null +++ b/libs/wire-api/src/Wire/API/MLS/CommitBundle.hs @@ -0,0 +1,44 @@ +-- 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 Wire.API.MLS.CommitBundle where + +import qualified Data.Swagger as S +import Imports +import Wire.API.MLS.GroupInfoBundle +import Wire.API.MLS.Message +import Wire.API.MLS.Serialisation +import Wire.API.MLS.Welcome + +data CommitBundle = CommitBundle + { cbCommitMsg :: RawMLS (Message 'MLSPlainText), + cbWelcome :: Maybe (RawMLS Welcome), + cbGroupInfoBundle :: GroupInfoBundle + } + deriving (Eq, Show) + +instance ParseMLS CommitBundle where + parseMLS = CommitBundle <$> parseMLS <*> parseMLSOptional parseMLS <*> parseMLS + +instance S.ToSchema CommitBundle where + declareNamedSchema _ = pure (mlsSwagger "CommitBundle") + +instance SerialiseMLS CommitBundle where + serialiseMLS (CommitBundle commit welcome gi) = do + serialiseMLS commit + serialiseMLSOptional serialiseMLS welcome + serialiseMLS gi diff --git a/libs/wire-api/src/Wire/API/MLS/Credential.hs b/libs/wire-api/src/Wire/API/MLS/Credential.hs index 6cd03be33f..4b7d62f99e 100644 --- a/libs/wire-api/src/Wire/API/MLS/Credential.hs +++ b/libs/wire-api/src/Wire/API/MLS/Credential.hs @@ -19,6 +19,7 @@ module Wire.API.MLS.Credential where +import Cassandra.CQL import Control.Error.Util import Control.Lens ((?~)) import Data.Aeson (FromJSON (..), FromJSONKey (..), ToJSON (..), ToJSONKey (..)) @@ -84,6 +85,14 @@ data SignatureSchemeTag = Ed25519 deriving stock (Bounded, Enum, Eq, Ord, Show, Generic) deriving (Arbitrary) via GenericUniform SignatureSchemeTag +instance Cql SignatureSchemeTag where + ctype = Tagged TextColumn + toCql = CqlText . signatureSchemeName + fromCql (CqlText name) = + note ("Unexpected signature scheme: " <> T.unpack name) $ + signatureSchemeFromName name + fromCql _ = Left "SignatureScheme: Text expected" + signatureSchemeNumber :: SignatureSchemeTag -> Word16 signatureSchemeNumber Ed25519 = 0x807 @@ -130,12 +139,23 @@ data ClientIdentity = ClientIdentity ciUser :: UserId, ciClient :: ClientId } - deriving stock (Eq, Show, Generic) + deriving stock (Eq, Ord, Generic) deriving (FromJSON, ToJSON, S.ToSchema) via Schema ClientIdentity +instance Show ClientIdentity where + show (ClientIdentity dom u c) = + show u + <> ":" + <> T.unpack (client c) + <> "@" + <> T.unpack (domainText dom) + cidQualifiedClient :: ClientIdentity -> Qualified (UserId, ClientId) cidQualifiedClient cid = Qualified (ciUser cid, ciClient cid) (ciDomain cid) +cidQualifiedUser :: ClientIdentity -> Qualified UserId +cidQualifiedUser = fmap fst . cidQualifiedClient + instance ToSchema ClientIdentity where schema = object "ClientIdentity" $ diff --git a/libs/wire-api/src/Wire/API/MLS/Extension.hs b/libs/wire-api/src/Wire/API/MLS/Extension.hs index 18b1d551d2..406adfa7e8 100644 --- a/libs/wire-api/src/Wire/API/MLS/Extension.hs +++ b/libs/wire-api/src/Wire/API/MLS/Extension.hs @@ -52,8 +52,7 @@ import Wire.API.MLS.Serialisation import Wire.Arbitrary newtype ProtocolVersion = ProtocolVersion {pvNumber :: Word8} - deriving newtype (Eq, Ord, Show, Binary, Arbitrary) - deriving (ParseMLS) via (BinaryMLS ProtocolVersion) + deriving newtype (Eq, Ord, Show, Binary, Arbitrary, ParseMLS, SerialiseMLS) data ProtocolVersionTag = ProtocolMLS10 | ProtocolMLSDraft11 deriving stock (Bounded, Enum, Eq, Show, Generic) diff --git a/libs/wire-api/src/Wire/API/MLS/GroupInfoBundle.hs b/libs/wire-api/src/Wire/API/MLS/GroupInfoBundle.hs new file mode 100644 index 0000000000..57acb31499 --- /dev/null +++ b/libs/wire-api/src/Wire/API/MLS/GroupInfoBundle.hs @@ -0,0 +1,59 @@ +-- 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 Wire.API.MLS.GroupInfoBundle where + +import Imports +import Test.QuickCheck +import Wire.API.MLS.PublicGroupState +import Wire.API.MLS.Serialisation +import Wire.Arbitrary + +data GroupInfoEncryption = UnencryptedGroupInfo | JweEncryptedGroupInfo + deriving stock (Eq, Show, Generic, Bounded, Enum) + deriving (Arbitrary) via (GenericUniform GroupInfoEncryption) + +data GroupInfoTreeType = TreeFull | TreeDelta | TreeByRef + deriving stock (Eq, Show, Generic, Bounded, Enum) + deriving (Arbitrary) via (GenericUniform GroupInfoTreeType) + +data GroupInfoBundle = GroupInfoBundle + { gipEncryptionType :: GroupInfoEncryption, + gipTreeType :: GroupInfoTreeType, + gipGroupState :: RawMLS PublicGroupState + } + deriving stock (Eq, Show, Generic) + +instance Arbitrary GroupInfoBundle where + arbitrary = + GroupInfoBundle + <$> arbitrary + <*> arbitrary + <*> (mkRawMLS <$> arbitrary) + +instance ParseMLS GroupInfoBundle where + parseMLS = + GroupInfoBundle + <$> parseMLSEnum @Word8 "GroupInfoEncryptionEnum" + <*> parseMLSEnum @Word8 "RatchetTreeEnum" + <*> parseMLS + +instance SerialiseMLS GroupInfoBundle where + serialiseMLS (GroupInfoBundle e t pgs) = do + serialiseMLSEnum @Word8 e + serialiseMLSEnum @Word8 t + serialiseMLS pgs diff --git a/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs b/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs index 3d39d778c5..1a878a40b3 100644 --- a/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs +++ b/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs @@ -29,6 +29,7 @@ module Wire.API.MLS.KeyPackage kpInitKey, kpCredential, kpExtensions, + kpIdentity, kpRef, kpRef', KeyPackageTBS (..), @@ -37,6 +38,7 @@ module Wire.API.MLS.KeyPackage ) where +import Cassandra.CQL hiding (Set) import Control.Applicative import Control.Lens hiding (set, (.=)) import Data.Aeson (FromJSON, ToJSON) @@ -44,6 +46,7 @@ import Data.Binary import Data.Binary.Get import Data.Binary.Put import qualified Data.ByteString as B +import qualified Data.ByteString.Lazy as LBS import Data.Id import Data.Json.Util import Data.Qualified @@ -79,6 +82,12 @@ instance ToSchema KeyPackageData where .= named "KeyPackage" base64Schema ) +instance Cql KeyPackageData where + ctype = Tagged BlobColumn + toCql = CqlBlob . LBS.fromStrict . kpData + fromCql (CqlBlob b) = pure . KeyPackageData . LBS.toStrict $ b + fromCql _ = Left "Expected CqlBlob" + data KeyPackageBundleEntry = KeyPackageBundleEntry { kpbeUser :: Qualified UserId, kpbeClient :: ClientId, @@ -132,6 +141,12 @@ instance ParseMLS KeyPackageRef where instance SerialiseMLS KeyPackageRef where serialiseMLS = putByteString . unKeyPackageRef +instance Cql KeyPackageRef where + ctype = Tagged BlobColumn + toCql = CqlBlob . LBS.fromStrict . unKeyPackageRef + fromCql (CqlBlob b) = pure . KeyPackageRef . LBS.toStrict $ b + fromCql _ = Left "Expected CqlBlob" + -- | Compute key package ref given a ciphersuite and the raw key package data. kpRef :: CipherSuiteTag -> KeyPackageData -> KeyPackageRef kpRef cs = @@ -174,6 +189,9 @@ data KeyPackage = KeyPackage } deriving stock (Eq, Show) +instance S.ToSchema KeyPackage where + declareNamedSchema _ = pure (mlsSwagger "KeyPackage") + kpProtocolVersion :: KeyPackage -> ProtocolVersion kpProtocolVersion = kpuProtocolVersion . rmValue . kpTBS @@ -189,6 +207,9 @@ kpCredential = kpuCredential . rmValue . kpTBS kpExtensions :: KeyPackage -> [Extension] kpExtensions = kpuExtensions . rmValue . kpTBS +kpIdentity :: KeyPackage -> Either Text ClientIdentity +kpIdentity = decodeMLS' @ClientIdentity . bcIdentity . kpCredential + rawKeyPackageSchema :: ValueSchema NamedSwaggerDoc (RawMLS KeyPackage) rawKeyPackageSchema = rawMLSSchema "KeyPackage" decodeMLS' diff --git a/libs/wire-api/src/Wire/API/MLS/Keys.hs b/libs/wire-api/src/Wire/API/MLS/Keys.hs index df7989ffd9..0aa187bb8c 100644 --- a/libs/wire-api/src/Wire/API/MLS/Keys.hs +++ b/libs/wire-api/src/Wire/API/MLS/Keys.hs @@ -59,7 +59,7 @@ instance ToSchema MLSPublicKeys where mlsKeysToPublic1 :: MLSKeys -> Map SignatureSchemeTag ByteString mlsKeysToPublic1 (MLSKeys mEd25519key) = - fold $ Map.singleton Ed25519 . convert . snd <$> mEd25519key + foldMap (Map.singleton Ed25519 . convert . snd) mEd25519key mlsKeysToPublic :: (SignaturePurpose -> MLSKeys) -> MLSPublicKeys mlsKeysToPublic f = flip foldMap [minBound .. maxBound] $ \purpose -> diff --git a/libs/wire-api/src/Wire/API/MLS/Message.hs b/libs/wire-api/src/Wire/API/MLS/Message.hs index 721f63c9c3..28594f3623 100644 --- a/libs/wire-api/src/Wire/API/MLS/Message.hs +++ b/libs/wire-api/src/Wire/API/MLS/Message.hs @@ -38,12 +38,11 @@ module Wire.API.MLS.Message MLSMessageSendingStatus (..), KnownFormatTag (..), verifyMessageSignature, - mkRemoveProposalMessage, + mkSignedMessage, ) where import Control.Lens ((?~)) -import Crypto.Error import Crypto.PubKey.Ed25519 import qualified Data.Aeson as A import Data.Binary @@ -341,14 +340,14 @@ verifyMessageSignature :: CipherSuiteTag -> Message 'MLSPlainText -> ByteString verifyMessageSignature cs msg pubkey = csVerifySignature cs pubkey (rmRaw (msgTBS msg)) (msgSignature (msgExtraFields msg)) -mkRemoveProposalMessage :: +mkSignedMessage :: SecretKey -> PublicKey -> GroupId -> Epoch -> - KeyPackageRef -> - Maybe (Message 'MLSPlainText) -mkRemoveProposalMessage priv pub gid epoch ref = maybeCryptoError $ do + MessagePayload 'MLSPlainText -> + Message 'MLSPlainText +mkSignedMessage priv pub gid epoch payload = let tbs = mkRawMLS $ MessageTBS @@ -357,7 +356,7 @@ mkRemoveProposalMessage priv pub gid epoch ref = maybeCryptoError $ do tbsMsgEpoch = epoch, tbsMsgAuthData = mempty, tbsMsgSender = PreconfiguredSender 0, - tbsMsgPayload = ProposalMessage (mkRemoveProposal ref) + tbsMsgPayload = payload } - let sig = BA.convert $ sign priv pub (rmRaw tbs) - pure (Message tbs (MessageExtraFields sig Nothing Nothing)) + sig = BA.convert $ sign priv pub (rmRaw tbs) + in Message tbs (MessageExtraFields sig Nothing Nothing) diff --git a/libs/wire-api/src/Wire/API/MLS/PublicGroupState.hs b/libs/wire-api/src/Wire/API/MLS/PublicGroupState.hs new file mode 100644 index 0000000000..d590260157 --- /dev/null +++ b/libs/wire-api/src/Wire/API/MLS/PublicGroupState.hs @@ -0,0 +1,126 @@ +-- 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 . +{-# LANGUAGE RecordWildCards #-} + +module Wire.API.MLS.PublicGroupState where + +import Data.Binary +import Data.Binary.Get +import Data.Binary.Put +import qualified Data.ByteString.Lazy as LBS +import qualified Data.Swagger as S +import Imports +import Servant.API.ContentTypes +import Test.QuickCheck hiding (label) +import Wire.API.MLS.CipherSuite +import Wire.API.MLS.Epoch +import Wire.API.MLS.Extension +import Wire.API.MLS.Group +import Wire.API.MLS.KeyPackage +import Wire.API.MLS.Serialisation +import Wire.API.MLS.Servant +import Wire.Arbitrary + +data PublicGroupStateTBS = PublicGroupStateTBS + { pgsVersion :: ProtocolVersion, + pgsCipherSuite :: CipherSuite, + pgsGroupId :: GroupId, + pgsEpoch :: Epoch, + pgsTreeHash :: ByteString, + pgsInterimTranscriptHash :: ByteString, + pgsConfirmedInterimTranscriptHash :: ByteString, + pgsGroupContextExtensions :: ByteString, + pgsOtherExtensions :: ByteString, + pgsExternalPub :: ByteString, + pgsSigner :: KeyPackageRef + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform PublicGroupStateTBS) + +instance ParseMLS PublicGroupStateTBS where + parseMLS = + PublicGroupStateTBS + <$> label "pgsVersion" parseMLS + <*> label "pgsCipherSuite" parseMLS + <*> label "pgsGroupId" parseMLS + <*> label "pgsEpoch" parseMLS + <*> label "pgsTreeHash" (parseMLSBytes @Word8) + <*> label "pgsInterimTranscriptHash" (parseMLSBytes @Word8) + <*> label "pgsConfirmedInterimTranscriptHash" (parseMLSBytes @Word8) + <*> label "pgsGroupContextExtensions" (parseMLSBytes @Word32) + <*> label "pgsOtherExtensions" (parseMLSBytes @Word32) + <*> label "pgsExternalPub" (parseMLSBytes @Word16) + <*> label "pgsSigner" parseMLS + +instance SerialiseMLS PublicGroupStateTBS where + serialiseMLS (PublicGroupStateTBS {..}) = do + serialiseMLS pgsVersion + serialiseMLS pgsCipherSuite + serialiseMLS pgsGroupId + serialiseMLS pgsEpoch + serialiseMLSBytes @Word8 pgsTreeHash + serialiseMLSBytes @Word8 pgsInterimTranscriptHash + serialiseMLSBytes @Word8 pgsConfirmedInterimTranscriptHash + serialiseMLSBytes @Word32 pgsGroupContextExtensions + serialiseMLSBytes @Word32 pgsOtherExtensions + serialiseMLSBytes @Word16 pgsExternalPub + serialiseMLS pgsSigner + +data PublicGroupState = PublicGroupState + { pgTBS :: RawMLS PublicGroupStateTBS, + pgSignature :: ByteString + } + deriving stock (Eq, Show, Generic) + +-- | A type that holds an MLS-encoded 'PublicGroupState' value via +-- 'serialiseMLS'. +newtype OpaquePublicGroupState = OpaquePublicGroupState + {unOpaquePublicGroupState :: ByteString} + deriving (Generic, Eq, Show) + deriving (Arbitrary) via (GenericUniform OpaquePublicGroupState) + +instance ParseMLS OpaquePublicGroupState where + parseMLS = OpaquePublicGroupState . LBS.toStrict <$> getRemainingLazyByteString + +instance SerialiseMLS OpaquePublicGroupState where + serialiseMLS (OpaquePublicGroupState bs) = putByteString bs + +instance S.ToSchema OpaquePublicGroupState where + declareNamedSchema _ = pure (mlsSwagger "OpaquePublicGroupState") + +instance MimeRender MLS OpaquePublicGroupState where + mimeRender _ = LBS.fromStrict . unOpaquePublicGroupState + +toOpaquePublicGroupState :: RawMLS PublicGroupState -> OpaquePublicGroupState +toOpaquePublicGroupState = OpaquePublicGroupState . rmRaw + +instance Arbitrary PublicGroupState where + arbitrary = + PublicGroupState + <$> (mkRawMLS <$> arbitrary) + <*> arbitrary + +instance ParseMLS PublicGroupState where + parseMLS = + PublicGroupState + <$> label "pgTBS" parseMLS + <*> label "pgSignature" (parseMLSBytes @Word16) + +instance SerialiseMLS PublicGroupState where + serialiseMLS PublicGroupState {..} = do + serialiseMLS pgTBS + serialiseMLSBytes @Word16 pgSignature diff --git a/libs/wire-api/src/Wire/API/MLS/Serialisation.hs b/libs/wire-api/src/Wire/API/MLS/Serialisation.hs index 6510ac3100..3e99b33cfb 100644 --- a/libs/wire-api/src/Wire/API/MLS/Serialisation.hs +++ b/libs/wire-api/src/Wire/API/MLS/Serialisation.hs @@ -27,11 +27,12 @@ module Wire.API.MLS.Serialisation serialiseMLSOptional, parseMLSEnum, serialiseMLSEnum, - BinaryMLS (..), MLSEnumError (..), fromMLSEnum, toMLSEnum', toMLSEnum, + encodeMLS, + encodeMLS', decodeMLS, decodeMLS', decodeMLSWith, @@ -163,15 +164,20 @@ instance ParseMLS Word32 where parseMLS = get instance ParseMLS Word64 where parseMLS = get +instance SerialiseMLS Word8 where serialiseMLS = put + instance SerialiseMLS Word16 where serialiseMLS = put instance SerialiseMLS Word32 where serialiseMLS = put --- | A wrapper to generate a 'ParseMLS' instance given a 'Binary' instance. -newtype BinaryMLS a = BinaryMLS a +instance SerialiseMLS Word64 where serialiseMLS = put + +-- | Encode an MLS value to a lazy bytestring. +encodeMLS :: SerialiseMLS a => a -> LByteString +encodeMLS = runPut . serialiseMLS -instance Binary a => ParseMLS (BinaryMLS a) where - parseMLS = BinaryMLS <$> get +encodeMLS' :: SerialiseMLS a => a -> ByteString +encodeMLS' = LBS.toStrict . encodeMLS -- | Decode an MLS value from a lazy bytestring. Return an error message in case of failure. decodeMLS :: ParseMLS a => LByteString -> Either Text a diff --git a/libs/wire-api/src/Wire/API/MLS/Welcome.hs b/libs/wire-api/src/Wire/API/MLS/Welcome.hs index 4231b08aba..929dc78af5 100644 --- a/libs/wire-api/src/Wire/API/MLS/Welcome.hs +++ b/libs/wire-api/src/Wire/API/MLS/Welcome.hs @@ -24,12 +24,16 @@ import Wire.API.MLS.Commit import Wire.API.MLS.Extension import Wire.API.MLS.KeyPackage import Wire.API.MLS.Serialisation +import Wire.Arbitrary data Welcome = Welcome - { welCipherSuite :: CipherSuite, + { welProtocolVersion :: ProtocolVersion, + welCipherSuite :: CipherSuite, welSecrets :: [GroupSecrets], welGroupInfo :: ByteString } + deriving (Show, Eq, Generic) + deriving (Arbitrary) via (GenericUniform Welcome) instance S.ToSchema Welcome where declareNamedSchema _ = pure (mlsSwagger "Welcome") @@ -37,16 +41,29 @@ instance S.ToSchema Welcome where instance ParseMLS Welcome where parseMLS = Welcome - -- Note: the extra protocol version at the beginning of the welcome - -- message is present in openmls-0.4.0-pre, but is not part of the spec - <$> (parseMLS @ProtocolVersion *> parseMLS) + <$> parseMLS @ProtocolVersion + <*> parseMLS <*> parseMLSVector @Word32 parseMLS <*> parseMLSBytes @Word32 +instance SerialiseMLS Welcome where + serialiseMLS (Welcome pv cs ss gi) = do + serialiseMLS pv + serialiseMLS cs + serialiseMLSVector @Word32 serialiseMLS ss + serialiseMLSBytes @Word32 gi + data GroupSecrets = GroupSecrets { gsNewMember :: KeyPackageRef, gsSecrets :: HPKECiphertext } + deriving (Show, Eq, Generic) + deriving (Arbitrary) via (GenericUniform GroupSecrets) instance ParseMLS GroupSecrets where parseMLS = GroupSecrets <$> parseMLS <*> parseMLS + +instance SerialiseMLS GroupSecrets where + serialiseMLS (GroupSecrets kp sec) = do + serialiseMLS kp + serialiseMLS sec diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs index eec1d208ef..1c03b4f6e9 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -30,6 +30,8 @@ module Wire.API.Routes.Internal.Brig swaggerDoc, module Wire.API.Routes.Internal.Brig.EJPD, NewKeyPackageRef (..), + NewKeyPackage (..), + NewKeyPackageResult (..), ) where @@ -155,6 +157,7 @@ type AccountAPI = :> MultiVerb 'POST '[Servant.JSON] CreateUserSparInternalResponses (Either CreateUserSparError SelfProfile) ) +-- | The missing ref is implicit by the capture data NewKeyPackageRef = NewKeyPackageRef { nkprUserId :: Qualified UserId, nkprClientId :: ClientId, @@ -171,6 +174,34 @@ instance ToSchema NewKeyPackageRef where <*> nkprClientId .= field "client_id" schema <*> nkprConversation .= field "conversation" schema +data NewKeyPackage = NewKeyPackage + { nkpConversation :: Qualified ConvId, + nkpKeyPackage :: KeyPackageData + } + deriving stock (Eq, Show, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema NewKeyPackage) + +instance ToSchema NewKeyPackage where + schema = + object "NewKeyPackage" $ + NewKeyPackage + <$> nkpConversation .= field "conversation" schema + <*> nkpKeyPackage .= field "key_package" schema + +data NewKeyPackageResult = NewKeyPackageResult + { nkpresClientIdentity :: ClientIdentity, + nkpresKeyPackageRef :: KeyPackageRef + } + deriving stock (Eq, Show, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema NewKeyPackageResult) + +instance ToSchema NewKeyPackageResult where + schema = + object "NewKeyPackageResult" $ + NewKeyPackageResult + <$> nkpresClientIdentity .= field "client_identity" schema + <*> nkpresKeyPackageRef .= field "key_package_ref" schema + type MLSAPI = "mls" :> ( ( "key-packages" :> Capture "ref" KeyPackageRef @@ -214,6 +245,15 @@ type MLSAPI = ) :<|> GetMLSClients :<|> MapKeyPackageRefs + :<|> Named + "put-key-package-add" + ( "key-package-add" + :> ReqBody '[Servant.JSON] NewKeyPackage + :> MultiVerb1 + 'PUT + '[Servant.JSON] + (Respond 200 "Key package ref mapping updated" NewKeyPackageResult) + ) ) type PutConversationByKeyPackageRef = diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index 0bc1a9ca28..e75deb517a 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -19,6 +19,7 @@ module Wire.API.Routes.Public.Brig where +import qualified Data.Aeson as A (FromJSON, ToJSON, Value) import Data.ByteString.Conversion import Data.Code (Timeout) import Data.CommaSeparatedList (CommaSeparatedList) @@ -30,7 +31,10 @@ import Data.Nonce (Nonce) import Data.Qualified (Qualified (..)) import Data.Range import Data.SOP -import Data.Swagger hiding (Contact, Header) +import Data.Schema as Schema +import Data.Swagger hiding (Contact, Header, Schema, ToSchema) +import qualified Data.Swagger as S +import qualified Generics.SOP as GSOP import Imports hiding (head) import Servant (JSON) import Servant hiding (Handler, JSON, addHeader, respond) @@ -50,9 +54,11 @@ import Wire.API.Routes.Public.Util import Wire.API.Routes.QualifiedCapture import Wire.API.Routes.Version import Wire.API.User hiding (NoIdentity) +import Wire.API.User.Activation import Wire.API.User.Client import Wire.API.User.Client.Prekey import Wire.API.User.Handle +import Wire.API.User.Password (CompletePasswordReset, NewPasswordReset, PasswordReset, PasswordResetKey) import Wire.API.User.RichInfo (RichInfoAssocList) import Wire.API.User.Search (Contact, RoleFilter, SearchResult, TeamContact, TeamUserSearchSortBy, TeamUserSearchSortOrder) import Wire.API.UserMap @@ -384,6 +390,154 @@ type AccountAPI = :> ReqBody '[JSON] NewUserPublic :> MultiVerb 'POST '[JSON] RegisterResponses (Either RegisterError RegisterSuccess) ) + -- This endpoint can lead to the following events being sent: + -- UserDeleted event to contacts of deleted user + -- MemberLeave event to members for all conversations the user was in (via galley) + :<|> Named + "verify-delete" + ( Summary "Verify account deletion with a code." + :> CanThrow 'InvalidCode + :> "delete" + :> ReqBody '[JSON] VerifyDeleteUser + :> MultiVerb 'POST '[JSON] '[RespondEmpty 200 "Deletion is initiated."] () + ) + -- This endpoint can lead to the following events being sent: + -- - UserActivated event to the user, if account gets activated + -- - UserIdentityUpdated event to the user, if email or phone get activated + :<|> Named + "get-activate" + ( Summary "Activate (i.e. confirm) an email address or phone number." + :> Description "See also 'POST /activate' which has a larger feature set." + :> CanThrow 'UserKeyExists + :> CanThrow 'InvalidActivationCodeWrongUser + :> CanThrow 'InvalidActivationCodeWrongCode + :> CanThrow 'InvalidEmail + :> CanThrow 'InvalidPhone + :> "activate" + :> QueryParam' '[Required, Strict, Description "Activation key"] "key" ActivationKey + :> QueryParam' '[Required, Strict, Description "Activation code"] "code" ActivationCode + :> MultiVerb + 'GET + '[JSON] + GetActivateResponse + ActivationRespWithStatus + ) + -- docs/reference/user/activation.md {#RefActivationSubmit} + -- + -- This endpoint can lead to the following events being sent: + -- - UserActivated event to the user, if account gets activated + -- - UserIdentityUpdated event to the user, if email or phone get activated + :<|> Named + "post-activate" + ( Summary "Activate (i.e. confirm) an email address or phone number." + :> Description + "Activation only succeeds once and the number of \ + \failed attempts for a valid key is limited." + :> CanThrow 'UserKeyExists + :> CanThrow 'InvalidActivationCodeWrongUser + :> CanThrow 'InvalidActivationCodeWrongCode + :> CanThrow 'InvalidEmail + :> CanThrow 'InvalidPhone + :> "activate" + :> ReqBody '[JSON] Activate + :> MultiVerb + 'POST + '[JSON] + GetActivateResponse + ActivationRespWithStatus + ) + -- docs/reference/user/activation.md {#RefActivationRequest} + :<|> Named + "post-activate-send" + ( Summary "Send (or resend) an email or phone activation code." + :> CanThrow 'UserKeyExists + :> CanThrow 'InvalidEmail + :> CanThrow 'InvalidPhone + :> CanThrow 'BlacklistedEmail + :> CanThrow 'BlacklistedPhone + :> CanThrow 'CustomerExtensionBlockedDomain + :> "activate" + :> "send" + :> ReqBody '[JSON] SendActivationCode + :> MultiVerb 'POST '[JSON] '[RespondEmpty 200 "Activation code sent."] () + ) + :<|> Named + "post-password-reset" + ( Summary "Initiate a password reset." + :> CanThrow 'PasswordResetInProgress + :> CanThrow 'InvalidPasswordResetKey + :> "password-reset" + :> ReqBody '[JSON] NewPasswordReset + :> MultiVerb 'POST '[JSON] '[RespondEmpty 201 "Password reset code created and sent by email."] () + ) + :<|> Named + "post-password-reset-complete" + ( Summary "Complete a password reset." + :> CanThrow 'InvalidPasswordResetCode + :> "password-reset" + :> "complete" + :> ReqBody '[JSON] CompletePasswordReset + :> MultiVerb 'POST '[JSON] '[RespondEmpty 200 "Password reset successful."] () + ) + :<|> Named + "post-password-reset-key-deprecated" + ( Summary "Complete a password reset." + :> CanThrow 'PasswordResetInProgress + :> CanThrow 'InvalidPasswordResetKey + :> CanThrow 'InvalidPasswordResetCode + :> CanThrow 'ResetPasswordMustDiffer + :> Description "DEPRECATED: Use 'POST /password-reset/complete'." + :> "password-reset" + :> Capture' '[Description "An opaque key for a pending password reset."] "key" PasswordResetKey + :> ReqBody '[JSON] PasswordReset + :> MultiVerb 'POST '[JSON] '[RespondEmpty 200 "Password reset successful."] () + ) + :<|> Named + "onboarding" + ( Summary "Upload contacts and invoke matching." + :> Description + "DEPRECATED: the feature has been turned off, the end-point does \ + \nothing and always returns '{\"results\":[],\"auto-connects\":[]}'." + :> ZUser + :> "onboarding" + :> "v3" + :> ReqBody '[JSON] JsonValue + :> Post '[JSON] DeprecatedMatchingResult + ) + +newtype JsonValue = JsonValue {fromJsonValue :: A.Value} + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema JsonValue) + +instance ToSchema JsonValue where + schema = fromJsonValue .= (JsonValue <$> named "Body" jsonValue) + +data DeprecatedMatchingResult = DeprecatedMatchingResult + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema DeprecatedMatchingResult) + +instance ToSchema DeprecatedMatchingResult where + schema = + object + "DeprecatedMatchingResult" + $ DeprecatedMatchingResult + <$ const [] .= field "results" (array (null_ @SwaggerDoc)) + <* const [] .= field "auto-connects" (array (null_ @SwaggerDoc)) + +data ActivationRespWithStatus + = ActivationResp ActivationResponse + | ActivationRespDryRun + | ActivationRespPass + | ActivationRespSuccessNoIdent + deriving (Generic) + deriving (AsUnion GetActivateResponse) via GenericAsUnion GetActivateResponse ActivationRespWithStatus + +instance GSOP.Generic ActivationRespWithStatus + +type GetActivateResponse = + '[ Respond 200 "Activation successful." ActivationResponse, + RespondEmpty 200 "Activation successful. (Dry run)", + RespondEmpty 204 "A recent activation was already successful.", + RespondEmpty 200 "Activation successful." + ] type PrekeyAPI = Named diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs index 719fc9b33f..eb56a4639c 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs @@ -41,8 +41,10 @@ import Wire.API.Error import qualified Wire.API.Error.Brig as BrigError import Wire.API.Error.Galley import Wire.API.Event.Conversation +import Wire.API.MLS.CommitBundle import Wire.API.MLS.Keys import Wire.API.MLS.Message +import Wire.API.MLS.PublicGroupState import Wire.API.MLS.Serialisation import Wire.API.MLS.Servant import Wire.API.MLS.Welcome @@ -185,6 +187,18 @@ type ConversationAPI = :> Capture "cnv" ConvId :> Get '[Servant.JSON] Conversation ) + :<|> Named + "get-unqualified-conversation-legalhold-alias" + -- This alias exists, so that it can be uniquely selected in zauth.acl + ( Summary "Get a conversation by ID (Legalhold alias)" + :> CanThrow 'ConvNotFound + :> CanThrow 'ConvAccessDenied + :> ZLocalUser + :> "legalhold" + :> "conversations" + :> Capture "cnv" ConvId + :> Get '[Servant.JSON] Conversation + ) :<|> Named "get-conversation" ( Summary "Get a conversation by ID" @@ -206,6 +220,24 @@ type ConversationAPI = :> "roles" :> Get '[Servant.JSON] ConversationRolesList ) + :<|> Named + "get-group-info" + ( Summary "Get MLS group information" + :> CanThrow 'ConvNotFound + :> CanThrow 'MLSMissingGroupInfo + :> ZLocalUser + :> "conversations" + :> QualifiedCapture "cnv" ConvId + :> "groupinfo" + :> MultiVerb1 + 'GET + '[MLS] + ( Respond + 200 + "The group information" + OpaquePublicGroupState + ) + ) :<|> Named "list-conversation-ids-unqualified" ( Summary "[deprecated] Get all local conversation IDs." @@ -1124,6 +1156,8 @@ type FeatureAPI = :<|> FeatureStatusPut '() SndFactorPasswordChallengeConfig :<|> FeatureStatusGet MLSConfig :<|> FeatureStatusPut '() MLSConfig + :<|> FeatureStatusGet ExposeInvitationURLsToTeamAdminConfig + :<|> FeatureStatusPut '() ExposeInvitationURLsToTeamAdminConfig :<|> FeatureStatusGet SearchVisibilityInboundConfig :<|> FeatureStatusPut '() SearchVisibilityInboundConfig :<|> AllFeatureConfigsUserGet @@ -1368,6 +1402,33 @@ type MLSMessagingAPI = :> ReqBody '[MLS] (RawMLS SomeMessage) :> MultiVerb1 'POST '[JSON] (Respond 201 "Message sent" MLSMessageSendingStatus) ) + :<|> Named + "mls-commit-bundle" + ( Summary "Post a MLS CommitBundle" + :> From 'V2 + :> CanThrow 'ConvAccessDenied + :> CanThrow 'ConvMemberNotFound + :> CanThrow 'ConvNotFound + :> CanThrow 'LegalHoldNotEnabled + :> CanThrow 'MLSClientMismatch + :> CanThrow 'MLSCommitMissingReferences + :> CanThrow 'MLSKeyPackageRefNotFound + :> CanThrow 'MLSProposalNotFound + :> CanThrow 'MLSProtocolErrorTag + :> CanThrow 'MLSSelfRemovalNotAllowed + :> CanThrow 'MLSStaleMessage + :> CanThrow 'MLSUnsupportedMessage + :> CanThrow 'MLSUnsupportedProposal + :> CanThrow 'MLSClientSenderUserMismatch + :> CanThrow 'MLSGroupConversationMismatch + :> CanThrow 'MLSWelcomeMismatch + :> CanThrow 'MissingLegalholdConsent + :> CanThrow MLSProposalFailure + :> "commit-bundles" + :> ZConn + :> ReqBody '[MLS] (RawMLS CommitBundle) + :> MultiVerb1 'POST '[JSON] (Respond 201 "Commit accepted and forwarded" MLSMessageSendingStatus) + ) :<|> Named "mls-public-keys" ( Summary "Get public keys used by the backend to sign external proposals" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs index 2ad6cb25ae..26bd5205d0 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs @@ -107,13 +107,15 @@ type IdpCreate = ReqBodyCustomError '[RawXML, JSON] "wai-error" IdPMetadataInfo :> QueryParam' '[Optional, Strict] "replaces" SAML.IdPId :> QueryParam' '[Optional, Strict] "api_version" WireIdPAPIVersion - :> QueryParam' '[Optional, Strict] "handle" (Range 1 32 Text) -- todo(leif): check length limitation + -- FUTUREWORK: The handle is restricted to 32 characters. Can we find a more reasonable upper bound and create a type for it? Also see `IdpUpdate`. + :> QueryParam' '[Optional, Strict] "handle" (Range 1 32 Text) :> PostCreated '[JSON] IdP type IdpUpdate = ReqBodyCustomError '[RawXML, JSON] "wai-error" IdPMetadataInfo :> Capture "id" SAML.IdPId - :> QueryParam' '[Optional, Strict] "handle" (Range 1 32 Text) -- todo(leif): check length limitation + -- FUTUREWORK: The handle is restricted to 32 characters. Can we find a more reasonable upper bound and create a type for it? Also see `IdpCreate`. + :> QueryParam' '[Optional, Strict] "handle" (Range 1 32 Text) :> Put '[JSON] IdP type IdpDelete = diff --git a/libs/wire-api/src/Wire/API/Routes/Version.hs b/libs/wire-api/src/Wire/API/Routes/Version.hs index 959c6abd3c..a269f64442 100644 --- a/libs/wire-api/src/Wire/API/Routes/Version.hs +++ b/libs/wire-api/src/Wire/API/Routes/Version.hs @@ -73,6 +73,10 @@ instance FromHttpApiData Version where parseHeader = first Text.pack . Aeson.eitherDecode . LBS.fromStrict parseUrlPiece = parseHeader . Text.encodeUtf8 +instance ToHttpApiData Version where + toHeader = LBS.toStrict . Aeson.encode + toUrlPiece = Text.decodeUtf8 . toHeader + supportedVersions :: [Version] supportedVersions = [minBound .. maxBound] diff --git a/libs/wire-api/src/Wire/API/Swagger.hs b/libs/wire-api/src/Wire/API/Swagger.hs index a83d78587a..20aaddca84 100644 --- a/libs/wire-api/src/Wire/API/Swagger.hs +++ b/libs/wire-api/src/Wire/API/Swagger.hs @@ -36,12 +36,10 @@ import qualified Wire.API.Team.Conversation as Team.Conversation import qualified Wire.API.Team.Invitation as Team.Invitation import qualified Wire.API.Team.Permission as Team.Permission import qualified Wire.API.User as User -import qualified Wire.API.User.Activation as User.Activation import qualified Wire.API.User.Auth as User.Auth import qualified Wire.API.User.Client as User.Client import qualified Wire.API.User.Client.Prekey as User.Client.Prekey import qualified Wire.API.User.Handle as User.Handle -import qualified Wire.API.User.Password as User.Password import qualified Wire.API.User.Profile as User.Profile import qualified Wire.API.User.RichInfo as User.RichInfo import qualified Wire.API.User.Search as User.Search @@ -102,10 +100,6 @@ models = User.modelUser, User.modelEmailUpdate, User.modelDelete, - User.modelVerifyDelete, - User.Activation.modelActivate, - User.Activation.modelSendActivationCode, - User.Activation.modelActivationResponse, User.Auth.modelSendLoginCode, User.Auth.modelLoginCodeResponse, User.Auth.modelLogin, @@ -118,14 +112,11 @@ models = User.Client.modelNewClient, User.Client.modelUpdateClient, User.Client.modelDeleteClient, - User.Client.modelClient, User.Client.modelSigkeys, User.Client.modelLocation, -- re-export from types-common User.Client.Prekey.modelPrekey, User.Handle.modelUserHandleInfo, User.Handle.modelCheckHandles, - User.Password.modelNewPasswordReset, - User.Password.modelCompletePasswordReset, User.Profile.modelAsset, User.RichInfo.modelRichInfo, User.RichInfo.modelRichField, diff --git a/libs/wire-api/src/Wire/API/Team/Conversation.hs b/libs/wire-api/src/Wire/API/Team/Conversation.hs index 3fd614cd6b..207ba65422 100644 --- a/libs/wire-api/src/Wire/API/Team/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Team/Conversation.hs @@ -36,11 +36,11 @@ module Wire.API.Team.Conversation ) where -import Control.Lens (At (at), makeLenses, over, (?~)) -import Data.Aeson hiding (fieldLabelModifier) +import Control.Lens (makeLenses, (?~)) +import qualified Data.Aeson as A import Data.Id (ConvId) -import Data.Proxy -import Data.Swagger +import Data.Schema +import qualified Data.Swagger as S import qualified Data.Swagger.Build.Api as Doc import Imports import Wire.Arbitrary (Arbitrary, GenericUniform (..)) @@ -53,17 +53,28 @@ newtype TeamConversation = TeamConversation } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform TeamConversation) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema TeamConversation) + +managedDesc :: Text +managedDesc = + "This field MUST NOT be used by clients. " + <> "It is here only for backwards compatibility of the interface." instance ToSchema TeamConversation where - declareNamedSchema _ = do - idSchema <- declareSchemaRef (Proxy @ConvId) - pure $ - NamedSchema (Just "TeamConversation") $ - mempty - & description ?~ "team conversation data" - & over - properties - (at "conversation" ?~ idSchema) + schema = + objectWithDocModifier + "TeamConversation" + (description ?~ "Team conversation data") + $ TeamConversation + <$> _conversationId .= field "conversation" schema + <* const () + .= fieldWithDocModifier + "managed" + (description ?~ managedDesc) + (c (False :: Bool)) + where + c :: A.ToJSON a => a -> ValueSchema SwaggerDoc () + c val = mkSchema mempty (const (pure ())) (const (pure (A.toJSON val))) newTeamConversation :: ConvId -> TeamConversation newTeamConversation = TeamConversation @@ -73,18 +84,8 @@ modelTeamConversation = Doc.defineModel "TeamConversation" $ do Doc.description "team conversation data" Doc.property "conversation" Doc.bytes' $ Doc.description "conversation ID" - -instance ToJSON TeamConversation where - toJSON t = - object - [ "conversation" .= _conversationId t, - -- FUTUREWORK: get rid of the "managed" field in the next version of the API - "managed" .= False - ] - -instance FromJSON TeamConversation where - parseJSON = withObject "team conversation" $ \o -> - TeamConversation <$> o .: "conversation" + Doc.property "managed" Doc.bytes' $ + Doc.description managedDesc -------------------------------------------------------------------------------- -- TeamConversationList @@ -95,15 +96,15 @@ newtype TeamConversationList = TeamConversationList deriving (Generic) deriving stock (Eq, Show) deriving newtype (Arbitrary) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema TeamConversationList) instance ToSchema TeamConversationList where - declareNamedSchema _ = do - convs <- declareSchema (Proxy @[TeamConversation]) - pure $ - NamedSchema (Just "TeamConversationList") $ - mempty - & description ?~ "team conversation list" - & properties . at "conversations" ?~ Inline convs + schema = + objectWithDocModifier + "TeamConversationList" + (description ?~ "Team conversation list") + $ TeamConversationList + <$> _teamConversations .= field "conversations" (array schema) newTeamConversationList :: [TeamConversation] -> TeamConversationList newTeamConversationList = TeamConversationList @@ -114,12 +115,5 @@ modelTeamConversationList = Doc.defineModel "TeamConversationListList" $ do Doc.property "conversations" (Doc.unique $ Doc.array (Doc.ref modelTeamConversation)) $ Doc.description "the array of team conversations" -instance ToJSON TeamConversationList where - toJSON t = object ["conversations" .= _teamConversations t] - -instance FromJSON TeamConversationList where - parseJSON = withObject "team conversation list" $ \o -> do - TeamConversationList <$> o .: "conversations" - makeLenses ''TeamConversation makeLenses ''TeamConversationList diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 1e2a33f803..41eb91997a 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -67,6 +67,7 @@ module Wire.API.Team.Feature DigitalSignaturesConfig (..), ConferenceCallingConfig (..), GuestLinksConfig (..), + ExposeInvitationURLsToTeamAdminConfig (..), SndFactorPasswordChallengeConfig (..), SearchVisibilityInboundConfig (..), ClassifiedDomainsConfig (..), @@ -579,6 +580,7 @@ allFeatureModels = withStatusNoLockModel @SndFactorPasswordChallengeConfig, withStatusNoLockModel @SearchVisibilityInboundConfig, withStatusNoLockModel @MLSConfig, + withStatusNoLockModel @ExposeInvitationURLsToTeamAdminConfig, withStatusModel @LegalholdConfig, withStatusModel @SSOConfig, withStatusModel @SearchVisibilityAvailableConfig, @@ -592,7 +594,8 @@ allFeatureModels = withStatusModel @GuestLinksConfig, withStatusModel @SndFactorPasswordChallengeConfig, withStatusModel @SearchVisibilityInboundConfig, - withStatusModel @MLSConfig + withStatusModel @MLSConfig, + withStatusModel @ExposeInvitationURLsToTeamAdminConfig ] <> catMaybes [ configModel @LegalholdConfig, @@ -608,7 +611,8 @@ allFeatureModels = configModel @GuestLinksConfig, configModel @SndFactorPasswordChallengeConfig, configModel @SearchVisibilityInboundConfig, - configModel @MLSConfig + configModel @MLSConfig, + configModel @ExposeInvitationURLsToTeamAdminConfig ] -------------------------------------------------------------------------------- @@ -939,6 +943,24 @@ instance IsFeatureConfig MLSConfig where 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 + +data ExposeInvitationURLsToTeamAdminConfig = ExposeInvitationURLsToTeamAdminConfig + deriving stock (Show, Eq, Generic) + deriving (Arbitrary) via (GenericUniform ExposeInvitationURLsToTeamAdminConfig) + +instance IsFeatureConfig ExposeInvitationURLsToTeamAdminConfig where + type FeatureSymbol ExposeInvitationURLsToTeamAdminConfig = "exposeInvitationURLsToTeamAdmin" + defFeatureStatus = withStatus FeatureStatusDisabled LockStatusLocked ExposeInvitationURLsToTeamAdminConfig FeatureTTLUnlimited + objectSchema = pure ExposeInvitationURLsToTeamAdminConfig + +instance ToSchema ExposeInvitationURLsToTeamAdminConfig where + schema = object "ExposeInvitationURLsToTeamAdminConfig" objectSchema + +instance FeatureTrivialConfig ExposeInvitationURLsToTeamAdminConfig where + trivialConfig = ExposeInvitationURLsToTeamAdminConfig + ---------------------------------------------------------------------- -- FeatureStatus @@ -1007,7 +1029,8 @@ data AllFeatureConfigs = AllFeatureConfigs afcSelfDeletingMessages :: WithStatus SelfDeletingMessagesConfig, afcGuestLink :: WithStatus GuestLinksConfig, afcSndFactorPasswordChallenge :: WithStatus SndFactorPasswordChallengeConfig, - afcMLS :: WithStatus MLSConfig + afcMLS :: WithStatus MLSConfig, + afcExposeInvitationURLsToTeamAdmin :: WithStatus ExposeInvitationURLsToTeamAdminConfig } deriving stock (Eq, Show) deriving (FromJSON, ToJSON, S.ToSchema) via (Schema AllFeatureConfigs) @@ -1030,6 +1053,7 @@ instance ToSchema AllFeatureConfigs where <*> afcGuestLink .= featureField <*> afcSndFactorPasswordChallenge .= featureField <*> afcMLS .= featureField + <*> afcExposeInvitationURLsToTeamAdmin .= featureField where featureField :: forall cfg. @@ -1054,5 +1078,6 @@ instance Arbitrary AllFeatureConfigs where <*> arbitrary <*> arbitrary <*> arbitrary + <*> arbitrary makeLenses ''ImplicitLockStatus diff --git a/libs/wire-api/src/Wire/API/Team/Invitation.hs b/libs/wire-api/src/Wire/API/Team/Invitation.hs index 4476698097..efcc60de35 100644 --- a/libs/wire-api/src/Wire/API/Team/Invitation.hs +++ b/libs/wire-api/src/Wire/API/Team/Invitation.hs @@ -34,6 +34,7 @@ import Data.Id import Data.Json.Util import qualified Data.Swagger.Build.Api as Doc import Imports +import URI.ByteString import Wire.API.Team.Role (Role, defaultRole, typeRole) import Wire.API.User.Identity (Email, Phone) import Wire.API.User.Profile (Locale, Name) @@ -104,7 +105,8 @@ data Invitation = Invitation inCreatedBy :: Maybe UserId, inInviteeEmail :: Email, inInviteeName :: Maybe Name, - inInviteePhone :: Maybe Phone + inInviteePhone :: Maybe Phone, + inInviteeUrl :: Maybe (URIRef Absolute) } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform Invitation) @@ -134,6 +136,9 @@ modelTeamInvitation = Doc.defineModel "TeamInvitation" $ do Doc.property "phone" Doc.string' $ do Doc.description "Phone number of the invitee, in the E.164 format" Doc.optional + Doc.property "url" Doc.string' $ do + Doc.description "URL of the invitation link to be sent to the invitee" + Doc.optional instance ToJSON Invitation where toJSON i = @@ -145,7 +150,8 @@ instance ToJSON Invitation where "created_by" .= inCreatedBy i, "email" .= inInviteeEmail i, "name" .= inInviteeName i, - "phone" .= inInviteePhone i + "phone" .= inInviteePhone i, + "url" .= inInviteeUrl i ] instance FromJSON Invitation where @@ -160,6 +166,7 @@ instance FromJSON Invitation where <*> o .: "email" <*> o .:? "name" <*> o .:? "phone" + <*> o .:? "url" -------------------------------------------------------------------------------- -- InvitationList diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 87f9d1fa3e..5dae1dfd9a 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -95,6 +95,7 @@ module Wire.API.User VerifyDeleteUser (..), mkVerifyDeleteUser, DeletionCodeTimeout (..), + DeleteUserResult (..), -- * List Users ListUsersQuery (..), @@ -108,7 +109,6 @@ module Wire.API.User modelEmailUpdate, modelUser, modelUserIdList, - modelVerifyDelete, -- * 2nd factor auth VerificationAction (..), @@ -1311,30 +1311,17 @@ data VerifyDeleteUser = VerifyDeleteUser } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform VerifyDeleteUser) - -modelVerifyDelete :: Doc.Model -modelVerifyDelete = Doc.defineModel "VerifyDelete" $ do - Doc.description "Data for verifying an account deletion." - Doc.property "key" Doc.string' $ - Doc.description "The identifying key of the account (i.e. user ID)." - Doc.property "code" Doc.string' $ - Doc.description "The verification code." + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema VerifyDeleteUser) mkVerifyDeleteUser :: Code.Key -> Code.Value -> VerifyDeleteUser mkVerifyDeleteUser = VerifyDeleteUser -instance ToJSON VerifyDeleteUser where - toJSON d = - A.object - [ "key" A..= verifyDeleteUserKey d, - "code" A..= verifyDeleteUserCode d - ] - -instance FromJSON VerifyDeleteUser where - parseJSON = A.withObject "VerifyDeleteUser" $ \o -> - VerifyDeleteUser - <$> o A..: "key" - <*> o A..: "code" +instance ToSchema VerifyDeleteUser where + schema = + objectWithDocModifier "VerifyDeleteUser" (description ?~ "Data for verifying an account deletion.") $ + VerifyDeleteUser + <$> verifyDeleteUserKey .= fieldWithDocModifier "key" (description ?~ "The identifying key of the account (i.e. user ID).") schema + <*> verifyDeleteUserCode .= fieldWithDocModifier "code" (description ?~ "The verification code.") schema -- | A response for a pending deletion code. newtype DeletionCodeTimeout = DeletionCodeTimeout @@ -1356,6 +1343,16 @@ instance FromJSON DeletionCodeTimeout where parseJSON = A.withObject "DeletionCodeTimeout" $ \o -> DeletionCodeTimeout <$> o A..: "expires_in" +-- | Result of an internal user/account deletion +data DeleteUserResult + = -- | User never existed + NoUser + | -- | User/account was deleted before + AccountAlreadyDeleted + | -- | User/account was deleted in this call + AccountDeleted + deriving (Eq, Show) + data ListUsersQuery = ListUsersByIds [Qualified UserId] | ListUsersByHandles (Range 1 4 [Qualified Handle]) diff --git a/libs/wire-api/src/Wire/API/User/Activation.hs b/libs/wire-api/src/Wire/API/User/Activation.hs index 385ceaa59c..4ec40cb6d3 100644 --- a/libs/wire-api/src/Wire/API/User/Activation.hs +++ b/libs/wire-api/src/Wire/API/User/Activation.hs @@ -32,22 +32,21 @@ module Wire.API.User.Activation -- * SendActivationCode SendActivationCode (..), - - -- * Swagger - modelActivate, - modelSendActivationCode, - modelActivationResponse, ) where -import Data.Aeson +import Control.Lens ((?~)) +import qualified Data.Aeson as A +import Data.Aeson.Types (Parser) import Data.ByteString.Conversion -import Data.Json.Util ((#)) -import Data.Schema (Schema (..), ToSchema, schemaIn) +import Data.Data (Proxy (Proxy)) +import Data.Schema +import Data.Swagger (ToParamSchema) import qualified Data.Swagger as S -import qualified Data.Swagger.Build.Api as Doc import Data.Text.Ascii +import Data.Tuple.Extra (fst3, snd3, thd3) import Imports +import Servant (FromHttpApiData (..)) import Wire.API.User.Identity import Wire.API.User.Profile import Wire.Arbitrary (Arbitrary, GenericUniform (..)) @@ -75,7 +74,13 @@ instance ToByteString ActivationTarget where newtype ActivationKey = ActivationKey {fromActivationKey :: AsciiBase64Url} deriving stock (Eq, Show, Generic) - deriving newtype (ToByteString, FromByteString, ToJSON, FromJSON, Arbitrary) + deriving newtype (ToSchema, ToByteString, FromByteString, A.ToJSON, A.FromJSON, Arbitrary) + +instance ToParamSchema ActivationKey where + toParamSchema _ = S.toParamSchema (Proxy @Text) + +instance FromHttpApiData ActivationKey where + parseUrlPiece = fmap ActivationKey . parseUrlPiece -------------------------------------------------------------------------------- -- ActivationCode @@ -87,7 +92,13 @@ newtype ActivationCode = ActivationCode {fromActivationCode :: AsciiBase64Url} deriving stock (Eq, Show, Generic) deriving newtype (ToByteString, FromByteString, ToSchema, Arbitrary) - deriving (ToJSON, FromJSON, S.ToSchema) via Schema ActivationCode + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema ActivationCode + +instance ToParamSchema ActivationCode where + toParamSchema _ = S.toParamSchema (Proxy @Text) + +instance FromHttpApiData ActivationCode where + parseQueryParam = fmap ActivationCode . parseUrlPiece -------------------------------------------------------------------------------- -- Activate @@ -100,54 +111,59 @@ data Activate = Activate } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform Activate) - -modelActivate :: Doc.Model -modelActivate = Doc.defineModel "Activate" $ do - Doc.description "Data for an activation request." - Doc.property "key" Doc.string' $ do - Doc.description "An opaque key to activate, as it was sent by the API." - Doc.optional - Doc.property "email" Doc.string' $ do - Doc.description "A known email address to activate." - Doc.optional - Doc.property "phone" Doc.string' $ do - Doc.description "A known phone number to activate." - Doc.optional - Doc.property "code" Doc.string' $ - Doc.description "The activation code." - Doc.property "label" Doc.string' $ do - Doc.description - "An optional label to associate with the access cookie, \ - \if one is granted during account activation." - Doc.optional - Doc.property "dryrun" Doc.bool' $ do - Doc.description - "Whether to perform a dryrun, i.e. to only check whether \ - \activation would succeed. Dry-runs never issue access \ - \cookies or tokens on success but failures still count \ - \towards the maximum failure count." - Doc.optional - -instance ToJSON Activate where - toJSON (Activate k c d) = - object - [key k, "code" .= c, "dryrun" .= d] + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema Activate + +instance ToSchema Activate where + schema = + objectWithDocModifier "Activate" objectDocs $ + Activate + <$> (maybeActivationTargetToTuple . activateTarget) .= maybeActivationTargetObjectSchema + <*> activateCode .= fieldWithDocModifier "code" codeDocs schema + <*> activateDryrun .= fieldWithDocModifier "dryrun" dryRunDocs schema where - key (ActivateKey ak) = "key" .= ak - key (ActivateEmail e) = "email" .= e - key (ActivatePhone p) = "phone" .= p - -instance FromJSON Activate where - parseJSON = withObject "Activation" $ \o -> - Activate - <$> key o - <*> o .: "code" - <*> o .:? "dryrun" .!= False - where - key o = - (ActivateKey <$> o .: "key") - <|> (ActivateEmail <$> o .: "email") - <|> (ActivatePhone <$> o .: "phone") + objectDocs :: NamedSwaggerDoc -> NamedSwaggerDoc + objectDocs = description ?~ "Data for an activation request." + + codeDocs :: NamedSwaggerDoc -> NamedSwaggerDoc + codeDocs = description ?~ "The activation code." + + dryRunDocs :: NamedSwaggerDoc -> NamedSwaggerDoc + dryRunDocs = + description + ?~ "At least one of key, email, or phone has to be present \ + \while key takes precedence over email, and email takes precedence over phone. \ + \Whether to perform a dryrun, i.e. to only check whether \ + \activation would succeed. Dry-runs never issue access \ + \cookies or tokens on success but failures still count \ + \towards the maximum failure count." + + maybeActivationTargetObjectSchema :: ObjectSchemaP SwaggerDoc (Maybe ActivationKey, Maybe Phone, Maybe Email) ActivationTarget + maybeActivationTargetObjectSchema = + withParser activationTargetTupleObjectSchema maybeActivationTargetTargetFromTuple + where + activationTargetTupleObjectSchema :: ObjectSchema SwaggerDoc (Maybe ActivationKey, Maybe Phone, Maybe Email) + activationTargetTupleObjectSchema = + (,,) + <$> fst3 .= maybe_ (optFieldWithDocModifier "key" keyDocs schema) + <*> snd3 .= maybe_ (optFieldWithDocModifier "phone" phoneDocs schema) + <*> thd3 .= maybe_ (optFieldWithDocModifier "email" emailDocs schema) + where + keyDocs = description ?~ "An opaque key to activate, as it was sent by the API." + phoneDocs = description ?~ "A known phone number to activate." + emailDocs = description ?~ "A known email address to activate." + + maybeActivationTargetTargetFromTuple :: (Maybe ActivationKey, Maybe Phone, Maybe Email) -> Parser ActivationTarget + maybeActivationTargetTargetFromTuple = \case + (Just key, _, _) -> pure $ ActivateKey key + (_, _, Just email) -> pure $ ActivateEmail email + (_, Just phone, _) -> pure $ ActivatePhone phone + _ -> fail "key, email or phone must be present" + + maybeActivationTargetToTuple :: ActivationTarget -> (Maybe ActivationKey, Maybe Phone, Maybe Email) + maybeActivationTargetToTuple = \case + ActivateKey key -> (Just key, Nothing, Nothing) + ActivatePhone phone -> (Nothing, Just phone, Nothing) + ActivateEmail email -> (Nothing, Nothing, Just email) -- | Information returned as part of a successful activation. data ActivationResponse = ActivationResponse @@ -158,34 +174,14 @@ data ActivationResponse = ActivationResponse } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ActivationResponse) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema ActivationResponse -modelActivationResponse :: Doc.Model -modelActivationResponse = Doc.defineModel "ActivationResponse" $ do - Doc.description "Response body of a successful activation request" - Doc.property "email" Doc.string' $ do - Doc.description "The email address that was activated." - Doc.optional - Doc.property "phone" Doc.string' $ do - Doc.description "The phone number that was activated." - Doc.optional - Doc.property "first" Doc.bool' $ - Doc.description "Whether this is the first successful activation (i.e. account activation)." - --- FUTUREWORK: de-deduplicate work with JSON instance for 'UserIdentity'? -instance ToJSON ActivationResponse where - toJSON (ActivationResponse ident first) = - object $ - "email" .= emailIdentity ident - # "phone" .= phoneIdentity ident - # "sso_id" .= ssoIdentity ident - # "first" .= first - # [] - -instance FromJSON ActivationResponse where - parseJSON = withObject "ActivationResponse" $ \o -> - ActivationResponse - <$> schemaIn userIdentityObjectSchema o - <*> o .:? "first" .!= False +instance ToSchema ActivationResponse where + schema = + objectWithDocModifier "ActivationResponse" (description ?~ "Response body of a successful activation request") $ + ActivationResponse + <$> activatedIdentity .= userIdentityObjectSchema + <*> activatedFirst .= (fromMaybe False <$> optFieldWithDocModifier "first" (description ?~ "Whether this is the first successful activation (i.e. account activation).") schema) -------------------------------------------------------------------------------- -- SendActivationCode @@ -200,43 +196,43 @@ data SendActivationCode = SendActivationCode } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform SendActivationCode) - -modelSendActivationCode :: Doc.Model -modelSendActivationCode = Doc.defineModel "SendActivationCode" $ do - Doc.description - "Data for requesting an email or phone activation code to be sent. \ - \One of 'email' or 'phone' must be present." - Doc.property "email" Doc.string' $ do - Doc.description "Email address to send the code to." - Doc.optional - Doc.property "phone" Doc.string' $ do - Doc.description "E.164 phone number to send the code to." - Doc.optional - Doc.property "locale" Doc.string' $ do - Doc.description "Locale to use for the activation code template." - Doc.optional - Doc.property "voice_call" Doc.bool' $ do - Doc.description "Request the code with a call instead (default is SMS)." - Doc.optional - -instance ToJSON SendActivationCode where - toJSON (SendActivationCode userKey locale call) = - object $ - either ("email" .=) ("phone" .=) userKey - # "locale" .= locale - # "voice_call" .= call - # [] - -instance FromJSON SendActivationCode where - parseJSON = withObject "SendActivationCode" $ \o -> do - e <- o .:? "email" - p <- o .:? "phone" - SendActivationCode - <$> key e p - <*> o .:? "locale" - <*> o .:? "voice_call" .!= False + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema SendActivationCode + +instance ToSchema SendActivationCode where + schema = + objectWithDocModifier "SendActivationCode" objectDesc $ + SendActivationCode + <$> (maybeUserKeyToTuple . saUserKey) .= userKeyObjectSchema + <*> saLocale .= maybe_ (optFieldWithDocModifier "locale" (description ?~ "Locale to use for the activation code template.") schema) + <*> saCall .= (fromMaybe False <$> optFieldWithDocModifier "voice_call" (description ?~ "Request the code with a call instead (default is SMS).") schema) where - key (Just _) (Just _) = fail "Only one of 'email' or 'phone' allowed." - key Nothing Nothing = fail "One of 'email' or 'phone' required." - key (Just e) Nothing = pure $ Left e - key Nothing (Just p) = pure $ Right p + maybeUserKeyToTuple :: Either Email Phone -> (Maybe Email, Maybe Phone) + maybeUserKeyToTuple = \case + Left email -> (Just email, Nothing) + Right phone -> (Nothing, Just phone) + + objectDesc :: NamedSwaggerDoc -> NamedSwaggerDoc + objectDesc = + description + ?~ "Data for requesting an email or phone activation code to be sent. \ + \One of 'email' or 'phone' must be present." + + userKeyObjectSchema :: ObjectSchemaP SwaggerDoc (Maybe Email, Maybe Phone) (Either Email Phone) + userKeyObjectSchema = + withParser userKeyTupleObjectSchema maybeUserKeyFromTuple + where + userKeyTupleObjectSchema :: ObjectSchema SwaggerDoc (Maybe Email, Maybe Phone) + userKeyTupleObjectSchema = + (,) + <$> fst .= maybe_ (optFieldWithDocModifier "email" phoneDocs schema) + <*> snd .= maybe_ (optFieldWithDocModifier "phone" emailDocs schema) + where + emailDocs = description ?~ "Email address to send the code to." + phoneDocs = description ?~ "E.164 phone number to send the code to." + + maybeUserKeyFromTuple :: (Maybe Email, Maybe Phone) -> Parser (Either Email Phone) + maybeUserKeyFromTuple = \case + (Just _, Just _) -> fail "Only one of 'email' or 'phone' allowed." + (Just email, Nothing) -> pure $ Left email + (Nothing, Just phone) -> pure $ Right phone + (Nothing, Nothing) -> fail "One of 'email' or 'phone' required." diff --git a/libs/wire-api/src/Wire/API/User/Client.hs b/libs/wire-api/src/Wire/API/User/Client.hs index 6f38cc75f6..7efbe1b78f 100644 --- a/libs/wire-api/src/Wire/API/User/Client.hs +++ b/libs/wire-api/src/Wire/API/User/Client.hs @@ -72,7 +72,6 @@ module Wire.API.User.Client modelClientCapabilityList, typeClientCapability, modelDeleteClient, - modelClient, modelSigkeys, modelLocation, -- re-export from types-common ) @@ -80,7 +79,7 @@ where import qualified Cassandra as Cql import Control.Applicative -import Control.Lens (over, view, (?~), (^.)) +import Control.Lens hiding (element, enum, set, (#), (.=)) import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson as A import qualified Data.Aeson.Key as Key @@ -97,6 +96,7 @@ import Data.Qualified import Data.Schema import qualified Data.Semigroup as Semigroup import qualified Data.Set as Set +import Data.Swagger hiding (Schema, ToSchema, schema) import qualified Data.Swagger as Swagger import qualified Data.Swagger.Build.Api as Doc import qualified Data.Text.Encoding as Text.E @@ -477,6 +477,24 @@ data Client = Client type MLSPublicKeys = Map SignatureSchemeTag ByteString +mlsPublicKeysSchema :: ValueSchema NamedSwaggerDoc MLSPublicKeys +mlsPublicKeysSchema = + mapSchema + & doc + %~ ( (description ?~ "Mapping from signature scheme (tags) to public key data") + . (example ?~ toJSON (Map.fromList $ map (,exampleValue) keys)) + ) + & named "MLSPublicKeys" + where + keys :: [SignatureSchemeTag] + keys = [minBound .. maxBound] + + exampleValue :: A.Value + exampleValue = fromMaybe (toJSON ("base64==" :: Text)) (base64Schema ^. doc . example) + + mapSchema :: ValueSchema SwaggerDoc MLSPublicKeys + mapSchema = map_ base64Schema + instance ToSchema Client where schema = object "Client" $ @@ -490,42 +508,10 @@ instance ToSchema Client where <*> clientLocation .= maybe_ (optField "location" schema) <*> clientModel .= maybe_ (optField "model" schema) <*> clientCapabilities .= (fromMaybe mempty <$> optField "capabilities" schema) - <*> clientMLSPublicKeys .= mlsPublicKeysSchema + <*> clientMLSPublicKeys .= mlsPublicKeysFieldSchema -mlsPublicKeysSchema :: ObjectSchema SwaggerDoc MLSPublicKeys -mlsPublicKeysSchema = - fmap - (fromMaybe mempty) - ( optField - "mls_public_keys" - (map_ base64Schema) - ) - -modelClient :: Doc.Model -modelClient = Doc.defineModel "Client" $ do - Doc.description "A registered client." - Doc.property "type" typeClientType $ - Doc.description "The client type." - Doc.property "id" Doc.string' $ - Doc.description "The client ID." - Doc.property "label" Doc.string' $ do - Doc.description "An optional label associated with the client." - Doc.optional - Doc.property "time" Doc.dateTime' $ - Doc.description "The date and time when this client was registered." - Doc.property "class" typeClientClass $ - Doc.description "The device class this client belongs to." - Doc.property "cookie" Doc.string' $ - Doc.description "The cookie label of this client." - Doc.property "address" Doc.string' $ do - Doc.description "IP address from which this client has been registered" - Doc.optional - Doc.property "location" (Doc.ref modelLocation) $ do - Doc.description "Location from which this client has been registered." - Doc.optional - Doc.property "model" Doc.string' $ do - Doc.description "Optional model information of this client" - Doc.optional +mlsPublicKeysFieldSchema :: ObjectSchema SwaggerDoc MLSPublicKeys +mlsPublicKeysFieldSchema = fromMaybe mempty <$> optField "mls_public_keys" mlsPublicKeysSchema -------------------------------------------------------------------------------- -- PubClient @@ -738,7 +724,7 @@ instance ToSchema NewClient where ) <*> newClientModel .= maybe_ (optField "model" schema) <*> newClientCapabilities .= maybe_ capabilitiesFieldSchema - <*> newClientMLSPublicKeys .= mlsPublicKeysSchema + <*> newClientMLSPublicKeys .= mlsPublicKeysFieldSchema <*> newClientVerificationCode .= maybe_ (optField "verification_code" schema) newClient :: ClientType -> LastPrekey -> NewClient @@ -808,7 +794,7 @@ instance ToSchema UpdateClient where schema ) <*> updateClientCapabilities .= maybe_ capabilitiesFieldSchema - <*> updateClientMLSPublicKeys .= mlsPublicKeysSchema + <*> updateClientMLSPublicKeys .= mlsPublicKeysFieldSchema modelUpdateClient :: Doc.Model modelUpdateClient = Doc.defineModel "UpdateClient" $ do diff --git a/libs/wire-api/src/Wire/API/User/IdentityProvider.hs b/libs/wire-api/src/Wire/API/User/IdentityProvider.hs index e573b19a93..ceb1200b68 100644 --- a/libs/wire-api/src/Wire/API/User/IdentityProvider.hs +++ b/libs/wire-api/src/Wire/API/User/IdentityProvider.hs @@ -190,7 +190,7 @@ instance ToSchema IdPMetadataInfo where & properties .~ properties_ & minProperties ?~ 1 & maxProperties ?~ 1 - & type_ .~ Just SwaggerObject + & type_ ?~ SwaggerObject where properties_ :: InsOrdHashMap Text (Referenced Schema) properties_ = diff --git a/libs/wire-api/src/Wire/API/User/Password.hs b/libs/wire-api/src/Wire/API/User/Password.hs index 986ef301a6..555082c573 100644 --- a/libs/wire-api/src/Wire/API/User/Password.hs +++ b/libs/wire-api/src/Wire/API/User/Password.hs @@ -28,20 +28,23 @@ module Wire.API.User.Password -- * deprecated PasswordReset (..), - - -- * Swagger - modelNewPasswordReset, - modelCompletePasswordReset, ) where -import Data.Aeson +import Control.Lens ((?~)) +import qualified Data.Aeson as A +import Data.Aeson.Types (Parser) import Data.ByteString.Conversion import Data.Misc (PlainTextPassword (..)) +import Data.Proxy (Proxy (Proxy)) import Data.Range (Ranged (..)) -import qualified Data.Swagger.Build.Api as Doc +import Data.Schema as Schema +import qualified Data.Swagger as S +import Data.Swagger.ParamSchema import Data.Text.Ascii +import Data.Tuple.Extra (fst3, snd3, thd3) import Imports +import Servant (FromHttpApiData (..)) import Wire.API.User.Identity import Wire.Arbitrary (Arbitrary, GenericUniform (..)) @@ -52,28 +55,46 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) newtype NewPasswordReset = NewPasswordReset (Either Email Phone) deriving stock (Eq, Show, Generic) deriving newtype (Arbitrary) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema NewPasswordReset -modelNewPasswordReset :: Doc.Model -modelNewPasswordReset = Doc.defineModel "NewPasswordReset" $ do - Doc.description "Data to initiate a password reset" - Doc.property "email" Doc.string' $ do - Doc.description "Email" - Doc.optional - Doc.property "phone" Doc.string' $ do - Doc.description "Phone" - Doc.optional - -instance ToJSON NewPasswordReset where - toJSON (NewPasswordReset ident) = - object - [either ("email" .=) ("phone" .=) ident] - -instance FromJSON NewPasswordReset where - parseJSON = withObject "NewPasswordReset" $ \o -> - NewPasswordReset - <$> ( (Left <$> o .: "email") - <|> (Right <$> o .: "phone") - ) +instance ToSchema NewPasswordReset where + schema = + objectWithDocModifier "NewPasswordReset" objectDesc $ + NewPasswordReset + <$> (toTuple . unNewPasswordReset) Schema..= newPasswordResetObjectSchema + where + unNewPasswordReset :: NewPasswordReset -> Either Email Phone + unNewPasswordReset (NewPasswordReset v) = v + + objectDesc :: NamedSwaggerDoc -> NamedSwaggerDoc + objectDesc = description ?~ "Data to initiate a password reset" + + newPasswordResetObjectSchema :: ObjectSchemaP SwaggerDoc (Maybe Email, Maybe Phone) (Either Email Phone) + newPasswordResetObjectSchema = withParser newPasswordResetTupleObjectSchema fromTuple + where + newPasswordResetTupleObjectSchema :: ObjectSchema SwaggerDoc (Maybe Email, Maybe Phone) + newPasswordResetTupleObjectSchema = + (,) + <$> fst .= maybe_ (optFieldWithDocModifier "email" phoneDocs schema) + <*> snd .= maybe_ (optFieldWithDocModifier "phone" emailDocs schema) + where + emailDocs :: NamedSwaggerDoc -> NamedSwaggerDoc + emailDocs = description ?~ "Email" + + phoneDocs :: NamedSwaggerDoc -> NamedSwaggerDoc + phoneDocs = description ?~ "Phone" + + fromTuple :: (Maybe Email, Maybe Phone) -> Parser (Either Email Phone) + fromTuple = \case + (Just _, Just _) -> fail "Only one of 'email' or 'phone' allowed." + (Just email, Nothing) -> pure $ Left email + (Nothing, Just phone) -> pure $ Right phone + (Nothing, Nothing) -> fail "One of 'email' or 'phone' required." + + toTuple :: Either Email Phone -> (Maybe Email, Maybe Phone) + toTuple = \case + Left e -> (Just e, Nothing) + Right p -> (Nothing, Just p) -------------------------------------------------------------------------------- -- CompletePasswordReset @@ -86,41 +107,52 @@ data CompletePasswordReset = CompletePasswordReset } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform CompletePasswordReset) - -modelCompletePasswordReset :: Doc.Model -modelCompletePasswordReset = Doc.defineModel "CompletePasswordReset" $ do - Doc.description "Data to complete a password reset." - Doc.property "key" Doc.string' $ do - Doc.description "An opaque key for a pending password reset." - Doc.optional - Doc.property "email" Doc.string' $ do - Doc.description "A known email with a pending password reset." - Doc.optional - Doc.property "phone" Doc.string' $ do - Doc.description "A known phone number with a pending password reset." - Doc.optional - Doc.property "code" Doc.string' $ - Doc.description "Password reset code" - Doc.property "password" Doc.string' $ - Doc.description "New password (6 - 1024 characters)" - -instance ToJSON CompletePasswordReset where - toJSON (CompletePasswordReset i c pw) = - object - [ident i, "code" .= c, "password" .= pw] - where - ident (PasswordResetIdentityKey k) = "key" .= k - ident (PasswordResetEmailIdentity e) = "email" .= e - ident (PasswordResetPhoneIdentity p) = "phone" .= p - -instance FromJSON CompletePasswordReset where - parseJSON = withObject "CompletePasswordReset" $ \o -> - CompletePasswordReset <$> ident o <*> o .: "code" <*> o .: "password" + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema CompletePasswordReset + +instance ToSchema CompletePasswordReset where + schema = + objectWithDocModifier "CompletePasswordReset" objectDocs $ + CompletePasswordReset + <$> (maybePasswordResetIdentityToTuple . cpwrIdent) .= maybePasswordResetIdentityObjectSchema + <*> cpwrCode .= fieldWithDocModifier "code" codeDocs schema + <*> cpwrPassword .= fieldWithDocModifier "password" pwDocs schema where - ident o = - (PasswordResetIdentityKey <$> o .: "key") - <|> (PasswordResetEmailIdentity <$> o .: "email") - <|> (PasswordResetPhoneIdentity <$> o .: "phone") + objectDocs :: NamedSwaggerDoc -> NamedSwaggerDoc + objectDocs = description ?~ "Data to complete a password reset" + + codeDocs :: NamedSwaggerDoc -> NamedSwaggerDoc + codeDocs = description ?~ "Password reset code" + + pwDocs :: NamedSwaggerDoc -> NamedSwaggerDoc + pwDocs = description ?~ "New password (6 - 1024 characters)" + + maybePasswordResetIdentityObjectSchema :: ObjectSchemaP SwaggerDoc (Maybe PasswordResetKey, Maybe Email, Maybe Phone) PasswordResetIdentity + maybePasswordResetIdentityObjectSchema = + withParser passwordResetIdentityTupleObjectSchema maybePasswordResetIdentityTargetFromTuple + where + passwordResetIdentityTupleObjectSchema :: ObjectSchema SwaggerDoc (Maybe PasswordResetKey, Maybe Email, Maybe Phone) + passwordResetIdentityTupleObjectSchema = + (,,) + <$> fst3 .= maybe_ (optFieldWithDocModifier "key" keyDocs schema) + <*> snd3 .= maybe_ (optFieldWithDocModifier "email" emailDocs schema) + <*> thd3 .= maybe_ (optFieldWithDocModifier "phone" phoneDocs schema) + where + keyDocs = description ?~ "An opaque key for a pending password reset." + emailDocs = description ?~ "A known email with a pending password reset." + phoneDocs = description ?~ "A known phone number with a pending password reset." + + maybePasswordResetIdentityTargetFromTuple :: (Maybe PasswordResetKey, Maybe Email, Maybe Phone) -> Parser PasswordResetIdentity + maybePasswordResetIdentityTargetFromTuple = \case + (Just key, _, _) -> pure $ PasswordResetIdentityKey key + (_, Just email, _) -> pure $ PasswordResetEmailIdentity email + (_, _, Just phone) -> pure $ PasswordResetPhoneIdentity phone + _ -> fail "key, email or phone must be present" + + maybePasswordResetIdentityToTuple :: PasswordResetIdentity -> (Maybe PasswordResetKey, Maybe Email, Maybe Phone) + maybePasswordResetIdentityToTuple = \case + PasswordResetIdentityKey key -> (Just key, Nothing, Nothing) + PasswordResetEmailIdentity email -> (Nothing, Just email, Nothing) + PasswordResetPhoneIdentity phone -> (Nothing, Nothing, Just phone) -------------------------------------------------------------------------------- -- PasswordResetIdentity @@ -140,7 +172,13 @@ data PasswordResetIdentity newtype PasswordResetKey = PasswordResetKey {fromPasswordResetKey :: AsciiBase64Url} deriving stock (Eq, Show) - deriving newtype (FromByteString, ToByteString, FromJSON, ToJSON, Arbitrary) + deriving newtype (ToSchema, FromByteString, ToByteString, A.FromJSON, A.ToJSON, Arbitrary) + +instance ToParamSchema PasswordResetKey where + toParamSchema _ = toParamSchema (Proxy @Text) + +instance FromHttpApiData PasswordResetKey where + parseQueryParam = fmap PasswordResetKey . parseQueryParam -------------------------------------------------------------------------------- -- PasswordResetCode @@ -149,7 +187,7 @@ newtype PasswordResetKey = PasswordResetKey newtype PasswordResetCode = PasswordResetCode {fromPasswordResetCode :: AsciiBase64Url} deriving stock (Eq, Show, Generic) - deriving newtype (FromByteString, ToByteString, FromJSON, ToJSON) + deriving newtype (ToSchema, FromByteString, ToByteString, A.FromJSON, A.ToJSON) deriving (Arbitrary) via (Ranged 6 1024 AsciiBase64Url) -------------------------------------------------------------------------------- @@ -161,9 +199,20 @@ data PasswordReset = PasswordReset } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform PasswordReset) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema PasswordReset + +instance ToSchema PasswordReset where + schema = + objectWithDocModifier "PasswordReset" objectDocs $ + PasswordReset + <$> pwrCode .= fieldWithDocModifier "code" codeDocs schema + <*> pwrPassword .= fieldWithDocModifier "password" pwDocs schema + where + objectDocs :: NamedSwaggerDoc -> NamedSwaggerDoc + objectDocs = description ?~ "Data to complete a password reset" + + codeDocs :: NamedSwaggerDoc -> NamedSwaggerDoc + codeDocs = description ?~ "Password reset code" -instance FromJSON PasswordReset where - parseJSON = withObject "PasswordReset" $ \o -> - PasswordReset - <$> o .: "code" - <*> o .: "password" + pwDocs :: NamedSwaggerDoc -> NamedSwaggerDoc + pwDocs = description ?~ "New password (6 - 1024 characters)" diff --git a/libs/wire-api/src/Wire/API/User/Scim.hs b/libs/wire-api/src/Wire/API/User/Scim.hs index 129ec63531..45fd51a09b 100644 --- a/libs/wire-api/src/Wire/API/User/Scim.hs +++ b/libs/wire-api/src/Wire/API/User/Scim.hs @@ -462,7 +462,7 @@ instance ToSchema ScimTokenInfo where pure $ NamedSchema (Just "ScimTokenInfo") $ mempty - & type_ .~ Just SwaggerObject + & type_ ?~ SwaggerObject & properties .~ [ ("team", teamSchema), ("id", idSchema), @@ -478,7 +478,7 @@ instance ToSchema CreateScimToken where pure $ NamedSchema (Just "CreateScimToken") $ mempty - & type_ .~ Just SwaggerObject + & type_ ?~ SwaggerObject & properties .~ [ ("description", textSchema), ("password", textSchema), @@ -493,7 +493,7 @@ instance ToSchema CreateScimTokenResponse where pure $ NamedSchema (Just "CreateScimTokenResponse") $ mempty - & type_ .~ Just SwaggerObject + & type_ ?~ SwaggerObject & properties .~ [ ("token", tokenSchema), ("info", infoSchema) @@ -506,7 +506,7 @@ instance ToSchema ScimTokenList where pure $ NamedSchema (Just "ScimTokenList") $ mempty - & type_ .~ Just SwaggerObject + & type_ ?~ SwaggerObject & properties .~ [ ("tokens", infoListSchema) ] diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs index 049b631f56..ccff53d727 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs @@ -1145,9 +1145,23 @@ tests = testGroup "Golden: TeamDeleteData_team" $ testObjects [(Test.Wire.API.Golden.Generated.TeamDeleteData_team.testObject_TeamDeleteData_team_1, "testObject_TeamDeleteData_team_1.json"), (Test.Wire.API.Golden.Generated.TeamDeleteData_team.testObject_TeamDeleteData_team_2, "testObject_TeamDeleteData_team_2.json"), (Test.Wire.API.Golden.Generated.TeamDeleteData_team.testObject_TeamDeleteData_team_3, "testObject_TeamDeleteData_team_3.json"), (Test.Wire.API.Golden.Generated.TeamDeleteData_team.testObject_TeamDeleteData_team_4, "testObject_TeamDeleteData_team_4.json"), (Test.Wire.API.Golden.Generated.TeamDeleteData_team.testObject_TeamDeleteData_team_5, "testObject_TeamDeleteData_team_5.json"), (Test.Wire.API.Golden.Generated.TeamDeleteData_team.testObject_TeamDeleteData_team_6, "testObject_TeamDeleteData_team_6.json"), (Test.Wire.API.Golden.Generated.TeamDeleteData_team.testObject_TeamDeleteData_team_7, "testObject_TeamDeleteData_team_7.json"), (Test.Wire.API.Golden.Generated.TeamDeleteData_team.testObject_TeamDeleteData_team_8, "testObject_TeamDeleteData_team_8.json"), (Test.Wire.API.Golden.Generated.TeamDeleteData_team.testObject_TeamDeleteData_team_9, "testObject_TeamDeleteData_team_9.json"), (Test.Wire.API.Golden.Generated.TeamDeleteData_team.testObject_TeamDeleteData_team_10, "testObject_TeamDeleteData_team_10.json"), (Test.Wire.API.Golden.Generated.TeamDeleteData_team.testObject_TeamDeleteData_team_11, "testObject_TeamDeleteData_team_11.json"), (Test.Wire.API.Golden.Generated.TeamDeleteData_team.testObject_TeamDeleteData_team_12, "testObject_TeamDeleteData_team_12.json"), (Test.Wire.API.Golden.Generated.TeamDeleteData_team.testObject_TeamDeleteData_team_13, "testObject_TeamDeleteData_team_13.json"), (Test.Wire.API.Golden.Generated.TeamDeleteData_team.testObject_TeamDeleteData_team_14, "testObject_TeamDeleteData_team_14.json"), (Test.Wire.API.Golden.Generated.TeamDeleteData_team.testObject_TeamDeleteData_team_15, "testObject_TeamDeleteData_team_15.json"), (Test.Wire.API.Golden.Generated.TeamDeleteData_team.testObject_TeamDeleteData_team_16, "testObject_TeamDeleteData_team_16.json"), (Test.Wire.API.Golden.Generated.TeamDeleteData_team.testObject_TeamDeleteData_team_17, "testObject_TeamDeleteData_team_17.json"), (Test.Wire.API.Golden.Generated.TeamDeleteData_team.testObject_TeamDeleteData_team_18, "testObject_TeamDeleteData_team_18.json"), (Test.Wire.API.Golden.Generated.TeamDeleteData_team.testObject_TeamDeleteData_team_19, "testObject_TeamDeleteData_team_19.json"), (Test.Wire.API.Golden.Generated.TeamDeleteData_team.testObject_TeamDeleteData_team_20, "testObject_TeamDeleteData_team_20.json")], testGroup "Golden: TeamConversation_team" $ - testObjects [(Test.Wire.API.Golden.Generated.TeamConversation_team.testObject_TeamConversation_team_1, "testObject_TeamConversation_team_1.json"), (Test.Wire.API.Golden.Generated.TeamConversation_team.testObject_TeamConversation_team_2, "testObject_TeamConversation_team_2.json"), (Test.Wire.API.Golden.Generated.TeamConversation_team.testObject_TeamConversation_team_3, "testObject_TeamConversation_team_3.json"), (Test.Wire.API.Golden.Generated.TeamConversation_team.testObject_TeamConversation_team_4, "testObject_TeamConversation_team_4.json"), (Test.Wire.API.Golden.Generated.TeamConversation_team.testObject_TeamConversation_team_5, "testObject_TeamConversation_team_5.json"), (Test.Wire.API.Golden.Generated.TeamConversation_team.testObject_TeamConversation_team_6, "testObject_TeamConversation_team_6.json"), (Test.Wire.API.Golden.Generated.TeamConversation_team.testObject_TeamConversation_team_7, "testObject_TeamConversation_team_7.json"), (Test.Wire.API.Golden.Generated.TeamConversation_team.testObject_TeamConversation_team_8, "testObject_TeamConversation_team_8.json"), (Test.Wire.API.Golden.Generated.TeamConversation_team.testObject_TeamConversation_team_9, "testObject_TeamConversation_team_9.json"), (Test.Wire.API.Golden.Generated.TeamConversation_team.testObject_TeamConversation_team_10, "testObject_TeamConversation_team_10.json"), (Test.Wire.API.Golden.Generated.TeamConversation_team.testObject_TeamConversation_team_11, "testObject_TeamConversation_team_11.json"), (Test.Wire.API.Golden.Generated.TeamConversation_team.testObject_TeamConversation_team_12, "testObject_TeamConversation_team_12.json"), (Test.Wire.API.Golden.Generated.TeamConversation_team.testObject_TeamConversation_team_13, "testObject_TeamConversation_team_13.json"), (Test.Wire.API.Golden.Generated.TeamConversation_team.testObject_TeamConversation_team_14, "testObject_TeamConversation_team_14.json"), (Test.Wire.API.Golden.Generated.TeamConversation_team.testObject_TeamConversation_team_15, "testObject_TeamConversation_team_15.json"), (Test.Wire.API.Golden.Generated.TeamConversation_team.testObject_TeamConversation_team_16, "testObject_TeamConversation_team_16.json"), (Test.Wire.API.Golden.Generated.TeamConversation_team.testObject_TeamConversation_team_17, "testObject_TeamConversation_team_17.json"), (Test.Wire.API.Golden.Generated.TeamConversation_team.testObject_TeamConversation_team_18, "testObject_TeamConversation_team_18.json"), (Test.Wire.API.Golden.Generated.TeamConversation_team.testObject_TeamConversation_team_19, "testObject_TeamConversation_team_19.json"), (Test.Wire.API.Golden.Generated.TeamConversation_team.testObject_TeamConversation_team_20, "testObject_TeamConversation_team_20.json")], + testObjects + [ ( Test.Wire.API.Golden.Generated.TeamConversation_team.testObject_TeamConversation_team_1, + "testObject_TeamConversation_team_1.json" + ), + ( Test.Wire.API.Golden.Generated.TeamConversation_team.testObject_TeamConversation_team_2, + "testObject_TeamConversation_team_2.json" + ) + ], testGroup "Golden: TeamConversationList_team" $ - testObjects [(Test.Wire.API.Golden.Generated.TeamConversationList_team.testObject_TeamConversationList_team_1, "testObject_TeamConversationList_team_1.json"), (Test.Wire.API.Golden.Generated.TeamConversationList_team.testObject_TeamConversationList_team_2, "testObject_TeamConversationList_team_2.json"), (Test.Wire.API.Golden.Generated.TeamConversationList_team.testObject_TeamConversationList_team_3, "testObject_TeamConversationList_team_3.json"), (Test.Wire.API.Golden.Generated.TeamConversationList_team.testObject_TeamConversationList_team_4, "testObject_TeamConversationList_team_4.json"), (Test.Wire.API.Golden.Generated.TeamConversationList_team.testObject_TeamConversationList_team_5, "testObject_TeamConversationList_team_5.json"), (Test.Wire.API.Golden.Generated.TeamConversationList_team.testObject_TeamConversationList_team_6, "testObject_TeamConversationList_team_6.json"), (Test.Wire.API.Golden.Generated.TeamConversationList_team.testObject_TeamConversationList_team_7, "testObject_TeamConversationList_team_7.json"), (Test.Wire.API.Golden.Generated.TeamConversationList_team.testObject_TeamConversationList_team_8, "testObject_TeamConversationList_team_8.json"), (Test.Wire.API.Golden.Generated.TeamConversationList_team.testObject_TeamConversationList_team_9, "testObject_TeamConversationList_team_9.json"), (Test.Wire.API.Golden.Generated.TeamConversationList_team.testObject_TeamConversationList_team_10, "testObject_TeamConversationList_team_10.json"), (Test.Wire.API.Golden.Generated.TeamConversationList_team.testObject_TeamConversationList_team_11, "testObject_TeamConversationList_team_11.json"), (Test.Wire.API.Golden.Generated.TeamConversationList_team.testObject_TeamConversationList_team_12, "testObject_TeamConversationList_team_12.json"), (Test.Wire.API.Golden.Generated.TeamConversationList_team.testObject_TeamConversationList_team_13, "testObject_TeamConversationList_team_13.json"), (Test.Wire.API.Golden.Generated.TeamConversationList_team.testObject_TeamConversationList_team_14, "testObject_TeamConversationList_team_14.json"), (Test.Wire.API.Golden.Generated.TeamConversationList_team.testObject_TeamConversationList_team_15, "testObject_TeamConversationList_team_15.json"), (Test.Wire.API.Golden.Generated.TeamConversationList_team.testObject_TeamConversationList_team_16, "testObject_TeamConversationList_team_16.json"), (Test.Wire.API.Golden.Generated.TeamConversationList_team.testObject_TeamConversationList_team_17, "testObject_TeamConversationList_team_17.json"), (Test.Wire.API.Golden.Generated.TeamConversationList_team.testObject_TeamConversationList_team_18, "testObject_TeamConversationList_team_18.json"), (Test.Wire.API.Golden.Generated.TeamConversationList_team.testObject_TeamConversationList_team_19, "testObject_TeamConversationList_team_19.json"), (Test.Wire.API.Golden.Generated.TeamConversationList_team.testObject_TeamConversationList_team_20, "testObject_TeamConversationList_team_20.json")], + testObjects + [ ( Test.Wire.API.Golden.Generated.TeamConversationList_team.testObject_TeamConversationList_team_1, + "testObject_TeamConversationList_team_1.json" + ), + ( Test.Wire.API.Golden.Generated.TeamConversationList_team.testObject_TeamConversationList_team_2, + "testObject_TeamConversationList_team_2.json" + ) + ], testGroup "Golden: WithStatusNoLock_team 1" $ testObjects [ (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_1, "testObject_WithStatusNoLock_team_1.json"), diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/InvitationList_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/InvitationList_team.hs index 5a3d0eee55..2b94790051 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/InvitationList_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/InvitationList_team.hs @@ -19,10 +19,12 @@ module Test.Wire.API.Golden.Generated.InvitationList_team where +import Data.Either.Combinators import Data.Id (Id (Id)) import Data.Json.Util (readUTCTimeMillis) import qualified Data.UUID as UUID (fromString) import Imports (Bool (False, True), Maybe (Just, Nothing), fromJust) +import URI.ByteString (parseURI, strictURIParserOptions) import Wire.API.Team.Invitation ( Invitation ( Invitation, @@ -32,6 +34,7 @@ import Wire.API.Team.Invitation inInviteeEmail, inInviteeName, inInviteePhone, + inInviteeUrl, inRole, inTeam ), @@ -49,10 +52,10 @@ testObject_InvitationList_team_2 = InvitationList { ilInvitations = [ Invitation - { inTeam = (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))), + { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001")), inRole = RoleOwner, - inInvitation = (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))), - inCreatedAt = (fromJust (readUTCTimeMillis "1864-05-08T09:28:36.729Z")), + inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000")), + inCreatedAt = fromJust (readUTCTimeMillis "1864-05-08T09:28:36.729Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))), inInviteeEmail = Email {emailLocal = "\153442", emailDomain = "w"}, inInviteeName = @@ -62,7 +65,8 @@ testObject_InvitationList_team_2 = "fuC9p\1098501A\163554\f\ENQ\SO\21027N\47326_?oCX.U\r\163744W\33096\58996\1038685\DC3\t[\37667\SYN/\8408A\145025\173325\DC4H\135001\STX\166880\EOT\165028o\DC3" } ), - inInviteePhone = Just (Phone {fromPhone = "+851333011"}) + inInviteePhone = Just (Phone {fromPhone = "+851333011"}), + inInviteeUrl = Just (fromRight' (parseURI strictURIParserOptions "https://example.com/inv14")) } ], ilHasMore = True @@ -76,10 +80,10 @@ testObject_InvitationList_team_4 = InvitationList { ilInvitations = [ Invitation - { inTeam = (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))), + { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")), inRole = RoleAdmin, - inInvitation = (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))), - inCreatedAt = (fromJust (readUTCTimeMillis "1864-05-09T19:46:50.121Z")), + inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000")), + inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T19:46:50.121Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))), inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, inInviteeName = @@ -89,13 +93,14 @@ testObject_InvitationList_team_4 = "R6\133444\134053VQ\187682\SUB\SOH\180538\&0C\1088909\ESCR\185800\125002@\38857Z?\STX\169387\1067878e}\SOH\ETB\EOTm\184898\US]\986782\189015\1059374\986508\b\DC1zfw-5\120662\CAN\1064450 \EMe\DC4|\14426Vo{\1076439\DC3#\USS\45051&zz\160719\&9\142411,\SI\f\SOHp\1025840\DLE\163178\1060369.&\997544kZ\50431u\b\50764\1109279n:\1103691D$.Q" } ), - inInviteePhone = Just (Phone {fromPhone = "+60506387292"}) + inInviteePhone = Just (Phone {fromPhone = "+60506387292"}), + inInviteeUrl = Nothing }, Invitation - { inTeam = (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000"))), + { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")), inRole = RoleAdmin, - inInvitation = (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000"))), - inCreatedAt = (fromJust (readUTCTimeMillis "1864-05-09T09:00:02.901Z")), + inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000")), + inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T09:00:02.901Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))), inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, inInviteeName = @@ -105,13 +110,14 @@ testObject_InvitationList_team_4 = "\DC2}q\CAN=SA\ETXx\t\ETX\\\v[\b)(\ESC]\135875Y\v@p\41515l\45065\157388\NUL\t\1100066\SOH1\DC1\ENQ\1021763\"i\29460\EM\b\ACK\SI\DC2v\ACK" } ), - inInviteePhone = Just (Phone {fromPhone = "+913945015"}) + inInviteePhone = Just (Phone {fromPhone = "+913945015"}), + inInviteeUrl = Nothing }, Invitation - { inTeam = (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001"))), + { inTeam = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), inRole = RoleMember, - inInvitation = (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001"))), - inCreatedAt = (fromJust (readUTCTimeMillis "1864-05-09T11:10:31.203Z")), + inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), + inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T11:10:31.203Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))), inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, inInviteeName = @@ -121,13 +127,14 @@ testObject_InvitationList_team_4 = "\58076&\1059325Ec\NUL\16147}k\1036184l\172911\USJ\EM0^.+F\DEL\NUL\f$'`!\ETB[p\1041609}>E0y\96440#4I\a\66593jc\ESCgt\22473\1093208P\DC4!\1095909E93'Y$YL\46886b\r:,\181790\SO\153247y\ETX;\1064633\1099478z4z-D\1096755a\139100\&6\164829r\1033640\987906J\DLE\48134" } ), - inInviteePhone = Just (Phone {fromPhone = "+17046334"}) + inInviteePhone = Just (Phone {fromPhone = "+17046334"}), + inInviteeUrl = Nothing }, Invitation - { inTeam = (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000"))), + { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), inRole = RoleOwner, - inInvitation = (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000"))), - inCreatedAt = (fromJust (readUTCTimeMillis "1864-05-09T23:41:34.529Z")), + inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), + inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T23:41:34.529Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))), inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, inInviteeName = @@ -137,23 +144,25 @@ testObject_InvitationList_team_4 = "Ft*O1\b&\SO\CAN<\72219\1092619m\n\DC4\DC2; \ETX\988837\DC1\1059627\"k.T\1023249[[\FS\EOT{j`\GS\997342c\1066411{\SUB\GSQY\182805\t\NAKy\t\132339j\1036225W " } ), - inInviteePhone = Nothing + inInviteePhone = Nothing, + inInviteeUrl = Nothing }, Invitation - { inTeam = (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000"))), + { inTeam = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000")), inRole = RoleAdmin, - inInvitation = (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000"))), - inCreatedAt = (fromJust (readUTCTimeMillis "1864-05-09T00:29:17.658Z")), + inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), + inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T00:29:17.658Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))), inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, inInviteeName = Nothing, - inInviteePhone = Just (Phone {fromPhone = "+918848647685283"}) + inInviteePhone = Just (Phone {fromPhone = "+918848647685283"}), + inInviteeUrl = Nothing }, Invitation - { inTeam = (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))), + { inTeam = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001")), inRole = RoleOwner, - inInvitation = (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))), - inCreatedAt = (fromJust (readUTCTimeMillis "1864-05-09T13:34:37.117Z")), + inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001")), + inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T13:34:37.117Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))), inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, inInviteeName = @@ -163,13 +172,14 @@ testObject_InvitationList_team_4 = "Lo\r\1107113 @@ -31,62 +29,66 @@ import Wire.API.User.Profile (Name (Name, fromName)) testObject_Invitation_team_1 :: Invitation testObject_Invitation_team_1 = Invitation - { inTeam = (Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000200000002"))), + { inTeam = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000200000002")), inRole = RoleAdmin, - inInvitation = (Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000200000000"))), - inCreatedAt = (fromJust (readUTCTimeMillis "1864-05-11T20:13:15.856Z")), + inInvitation = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000200000000")), + inCreatedAt = fromJust (readUTCTimeMillis "1864-05-11T20:13:15.856Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000002-0000-0000-0000-000100000001"))), inInviteeEmail = Email {emailLocal = "\FS\58114Y", emailDomain = "7"}, inInviteeName = Nothing, - inInviteePhone = Just (Phone {fromPhone = "+54687000371"}) + inInviteePhone = Just (Phone {fromPhone = "+54687000371"}), + inInviteeUrl = Nothing } testObject_Invitation_team_2 :: Invitation testObject_Invitation_team_2 = Invitation - { inTeam = (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))), + { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")), inRole = RoleExternalPartner, - inInvitation = (Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000100000002"))), - inCreatedAt = (fromJust (readUTCTimeMillis "1864-05-12T14:47:35.551Z")), + inInvitation = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000100000002")), + inCreatedAt = fromJust (readUTCTimeMillis "1864-05-12T14:47:35.551Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000200000001"))), inInviteeEmail = Email {emailLocal = "i", emailDomain = "m_:"}, inInviteeName = Just (Name {fromName = "\1067847} 2pGEW+\rT\171609p\174643\157218&\146145v0\b"}), - inInviteePhone = Nothing + inInviteePhone = Nothing, + inInviteeUrl = Nothing } testObject_Invitation_team_3 :: Invitation testObject_Invitation_team_3 = Invitation - { inTeam = (Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000100000001"))), + { inTeam = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000100000001")), inRole = RoleExternalPartner, - inInvitation = (Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000100000002"))), - inCreatedAt = (fromJust (readUTCTimeMillis "1864-05-08T22:07:35.846Z")), + inInvitation = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000100000002")), + inCreatedAt = fromJust (readUTCTimeMillis "1864-05-08T22:07:35.846Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000200000001"))), inInviteeEmail = Email {emailLocal = "", emailDomain = "\31189L"}, inInviteeName = Nothing, - inInviteePhone = Nothing + inInviteePhone = Nothing, + inInviteeUrl = Nothing } testObject_Invitation_team_4 :: Invitation testObject_Invitation_team_4 = Invitation - { inTeam = (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000"))), + { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000")), inRole = RoleAdmin, - inInvitation = (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))), - inCreatedAt = (fromJust (readUTCTimeMillis "1864-05-09T09:23:58.270Z")), + inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001")), + inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T09:23:58.270Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000200000001"))), inInviteeEmail = Email {emailLocal = "^", emailDomain = "e"}, inInviteeName = Nothing, - inInviteePhone = Nothing + inInviteePhone = Nothing, + inInviteeUrl = Nothing } testObject_Invitation_team_5 :: Invitation testObject_Invitation_team_5 = Invitation - { inTeam = (Id (fromJust (UUID.fromString "00000002-0000-0000-0000-000000000001"))), + { inTeam = Id (fromJust (UUID.fromString "00000002-0000-0000-0000-000000000001")), inRole = RoleOwner, - inInvitation = (Id (fromJust (UUID.fromString "00000000-0000-0002-0000-000000000002"))), - inCreatedAt = (fromJust (readUTCTimeMillis "1864-05-09T03:42:15.266Z")), + inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0002-0000-000000000002")), + inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T03:42:15.266Z"), inCreatedBy = Nothing, inInviteeEmail = Email {emailLocal = "\SOHV", emailDomain = "f\1086249\43462"}, inInviteeName = @@ -96,16 +98,17 @@ testObject_Invitation_team_5 = "}G_\147658`X\1028823\131485\1014942L\"\1047959e6:E\DEL\51733\993223f-$\133906Z!s2p?#\tF 8\188400\165247\1023303\EOT\1087640*\1017476\SYN\DLE%Y\167940>\1111565\1042998\1027480g\"\1055088\SUB\SUB\180703\43419\EOTv\188258,\171408(\GSQT\150160;\1063450\ENQ\ETBB\1106414H\170195\\\1040638,Y" } ), - inInviteePhone = Just (Phone {fromPhone = "+45207005641274"}) + inInviteePhone = Just (Phone {fromPhone = "+45207005641274"}), + inInviteeUrl = Nothing } testObject_Invitation_team_6 :: Invitation testObject_Invitation_team_6 = Invitation - { inTeam = (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001"))), + { inTeam = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), inRole = RoleAdmin, - inInvitation = (Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000100000000"))), - inCreatedAt = (fromJust (readUTCTimeMillis "1864-05-09T08:56:40.919Z")), + inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000100000000")), + inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T08:56:40.919Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000000"))), inInviteeEmail = Email {emailLocal = "", emailDomain = "OC"}, inInviteeName = @@ -115,16 +118,17 @@ testObject_Invitation_team_6 = "O~\DC4U\RS?V3_\191280Slh\1072236Q1\1011443j|~M7\1092762\1097596\94632\DC1K\1078140Afs\178951lGV\1113159]`o\EMf\34020InvfDDy\\DI\163761\1091945\ETBB\159212F*X\SOH\SUB\50580\ETX\DLE<\ETX\SYNc\DEL\DLE,p\v*\1005720Vn\fI\70201xS\STXV\ESC$\EMu\1002390xl>\aZ\DC44e\DC4aZ" } ), - inInviteePhone = Just (Phone {fromPhone = "+75547625285"}) + inInviteePhone = Just (Phone {fromPhone = "+75547625285"}), + inInviteeUrl = Nothing } testObject_Invitation_team_7 :: Invitation testObject_Invitation_team_7 = Invitation - { inTeam = (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000200000001"))), + { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000200000001")), inRole = RoleExternalPartner, - inInvitation = (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000002"))), - inCreatedAt = (fromJust (readUTCTimeMillis "1864-05-07T18:46:22.786Z")), + inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000002")), + inCreatedAt = fromJust (readUTCTimeMillis "1864-05-07T18:46:22.786Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0002-0000-000100000000"))), inInviteeEmail = Email {emailLocal = "oj", emailDomain = ""}, inInviteeName = @@ -134,55 +138,59 @@ testObject_Invitation_team_7 = "\CAN.\110967\1085214\DLE\f\DLE\CAN\150564o;Yay:yY $\ETX<\879%@\USre>5L'R\DC3\178035oy#]c4!\99741U\54858\26279\1042232\1062242p_>f\SO\DEL\175240\1077738\995735_Vm\US}\STXPz\r\ENQK\SO+>\991648\NUL\153467?pu?r\ESC\SUB!?\168405;\6533S\18757\a\1071148\b\1023581\996567\17385\120022\b\SUB\FS\SIF%<\125113\SIh\ESC\ETX\SI\994739\USO\NULg_\151272\47274\1026399\EOT\1058084\1089771z~%IA'R\b\1011572Hv^\1043633wrjb\t\166747\ETX" } ), - inInviteePhone = Just (Phone {fromPhone = "+518729615781"}) + inInviteePhone = Just (Phone {fromPhone = "+518729615781"}), + inInviteeUrl = Nothing } testObject_Invitation_team_12 :: Invitation testObject_Invitation_team_12 = Invitation - { inTeam = (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000002"))), + { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000002")), inRole = RoleAdmin, - inInvitation = (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000002"))), - inCreatedAt = (fromJust (readUTCTimeMillis "1864-05-12T22:47:35.829Z")), + inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000002")), + inCreatedAt = fromJust (readUTCTimeMillis "1864-05-12T22:47:35.829Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000002-0000-0002-0000-000000000000"))), inInviteeEmail = Email {emailLocal = "\1016862\141073\RS", emailDomain = ""}, inInviteeName = @@ -211,42 +220,45 @@ testObject_Invitation_team_12 = "\DLEZ+wd^\67082\1073384\&1\STXYdXt>\1081020LSB7F9\\\135148\ENQ\n\987295\"\127009|\a\61724\157754\DEL'\ESCTygU\1106772R\52822\1071584O4\1035713E9\"\1016016\DC2Re\ENQD}\1051112\161959\1104733\bV\176894%98'\RS9\ACK4yP\83405\14400\345\aw\t\1098022\v\1078003xv/Yl\1005740\158703" } ), - inInviteePhone = Just (Phone {fromPhone = "+68945103783764"}) + inInviteePhone = Just (Phone {fromPhone = "+68945103783764"}), + inInviteeUrl = Nothing } testObject_Invitation_team_13 :: Invitation testObject_Invitation_team_13 = Invitation - { inTeam = (Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000000000001"))), + { inTeam = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000000000001")), inRole = RoleMember, - inInvitation = (Id (fromJust (UUID.fromString "00000002-0000-0000-0000-000200000002"))), - inCreatedAt = (fromJust (readUTCTimeMillis "1864-05-08T01:18:31.982Z")), + inInvitation = Id (fromJust (UUID.fromString "00000002-0000-0000-0000-000200000002")), + inCreatedAt = fromJust (readUTCTimeMillis "1864-05-08T01:18:31.982Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000100000002"))), inInviteeEmail = Email {emailLocal = "", emailDomain = "\DELr"}, inInviteeName = Just (Name {fromName = "U"}), - inInviteePhone = Just (Phone {fromPhone = "+549940856897515"}) + inInviteePhone = Just (Phone {fromPhone = "+549940856897515"}), + inInviteeUrl = Nothing } testObject_Invitation_team_14 :: Invitation testObject_Invitation_team_14 = Invitation - { inTeam = (Id (fromJust (UUID.fromString "00000002-0000-0002-0000-000100000000"))), + { inTeam = Id (fromJust (UUID.fromString "00000002-0000-0002-0000-000100000000")), inRole = RoleOwner, - inInvitation = (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000200000002"))), - inCreatedAt = (fromJust (readUTCTimeMillis "1864-05-12T23:54:25.090Z")), + inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000200000002")), + inCreatedAt = fromJust (readUTCTimeMillis "1864-05-12T23:54:25.090Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000002-0000-0002-0000-000200000000"))), inInviteeEmail = Email {emailLocal = "EI", emailDomain = "{"}, inInviteeName = Nothing, - inInviteePhone = Just (Phone {fromPhone = "+89058877371"}) + inInviteePhone = Just (Phone {fromPhone = "+89058877371"}), + inInviteeUrl = Nothing } testObject_Invitation_team_15 :: Invitation testObject_Invitation_team_15 = Invitation - { inTeam = (Id (fromJust (UUID.fromString "00000000-0000-0002-0000-000100000001"))), + { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0002-0000-000100000001")), inRole = RoleOwner, - inInvitation = (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000001"))), - inCreatedAt = (fromJust (readUTCTimeMillis "1864-05-08T22:22:28.568Z")), + inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000001")), + inCreatedAt = fromJust (readUTCTimeMillis "1864-05-08T22:22:28.568Z"), inCreatedBy = Nothing, inInviteeEmail = Email {emailLocal = ".", emailDomain = "\DEL"}, inInviteeName = @@ -256,29 +268,31 @@ testObject_Invitation_team_15 = "\71448\US&KIL\DC3\1086159![\n6\1111661HEj4E\12136UL\US>2\1070931_\nJ\53410Pv\SO\SIR\30897\&8\bmS\45510mE\ag\SYN\ENQ%\14545\f!\v\US\119306\ENQ\184817\1044744\SO83!j\73854\GS\1071331,\RS\CANF\1062795\1110535U\EMJb\DC1j\EMY\92304O\1007855" } ), - inInviteePhone = Just (Phone {fromPhone = "+57741900390998"}) + inInviteePhone = Just (Phone {fromPhone = "+57741900390998"}), + inInviteeUrl = Nothing } testObject_Invitation_team_16 :: Invitation testObject_Invitation_team_16 = Invitation - { inTeam = (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000002"))), + { inTeam = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000002")), inRole = RoleExternalPartner, - inInvitation = (Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000200000001"))), - inCreatedAt = (fromJust (readUTCTimeMillis "1864-05-09T09:56:33.113Z")), + inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000200000001")), + inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T09:56:33.113Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))), inInviteeEmail = Email {emailLocal = "\\", emailDomain = "\"\DEL{"}, inInviteeName = Just (Name {fromName = "\GS\DC4Q;6/_f*7\1093966\SI+\1092810\41698\&9"}), - inInviteePhone = Nothing + inInviteePhone = Nothing, + inInviteeUrl = Nothing } testObject_Invitation_team_17 :: Invitation testObject_Invitation_team_17 = Invitation - { inTeam = (Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000000000002"))), + { inTeam = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000000000002")), inRole = RoleAdmin, - inInvitation = (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001"))), - inCreatedAt = (fromJust (readUTCTimeMillis "1864-05-08T06:30:23.239Z")), + inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), + inCreatedAt = fromJust (readUTCTimeMillis "1864-05-08T06:30:23.239Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001"))), inInviteeEmail = Email {emailLocal = "", emailDomain = "\SOH[\97119"}, inInviteeName = @@ -288,16 +302,17 @@ testObject_Invitation_team_17 = "Z\ESC9E\DEL\NAK\37708\83413}(3m\97177\97764'\1072786.WY;\RS8?v-\1100720\DC2\1015859" } ), - inInviteePhone = Nothing + inInviteePhone = Nothing, + inInviteeUrl = Nothing } testObject_Invitation_team_19 :: Invitation testObject_Invitation_team_19 = Invitation - { inTeam = (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000200000000"))), + { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000200000000")), inRole = RoleMember, - inInvitation = (Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000000000001"))), - inCreatedAt = (fromJust (readUTCTimeMillis "1864-05-07T15:08:06.796Z")), + inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000000000001")), + inCreatedAt = fromJust (readUTCTimeMillis "1864-05-07T15:08:06.796Z"), inCreatedBy = Nothing, inInviteeEmail = Email {emailLocal = "\1019726\96050\DEL", emailDomain = "(S\ETB"}, inInviteeName = @@ -326,18 +342,20 @@ testObject_Invitation_team_19 = "\38776r\111317\ETXQi\1000087\1097943\EM\170747\74323+\1067948Q?H=G-\RS;\1103719\SOq^K;a\1052250W\EM X\83384\1073320>M\980\26387jjbU-&\1040136v\NULy\181884\a|\SYNUfJCHjP\SO\1111555\27981DNA:~s" } ), - inInviteePhone = Just (Phone {fromPhone = "+05787228893"}) + inInviteePhone = Just (Phone {fromPhone = "+05787228893"}), + inInviteeUrl = Nothing } testObject_Invitation_team_20 :: Invitation testObject_Invitation_team_20 = Invitation - { inTeam = (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000"))), + { inTeam = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), inRole = RoleExternalPartner, - inInvitation = (Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000000000001"))), - inCreatedAt = (fromJust (readUTCTimeMillis "1864-05-12T08:07:17.747Z")), + inInvitation = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000000000001")), + inCreatedAt = fromJust (readUTCTimeMillis "1864-05-12T08:07:17.747Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))), inInviteeEmail = Email {emailLocal = "b", emailDomain = "u9T"}, inInviteeName = Nothing, - inInviteePhone = Just (Phone {fromPhone = "+27259486019"}) + inInviteePhone = Just (Phone {fromPhone = "+27259486019"}), + inInviteeUrl = Nothing } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamConversationList_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamConversationList_team.hs index 3bdf75b2be..ea25570d1b 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamConversationList_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamConversationList_team.hs @@ -26,459 +26,14 @@ import Wire.API.Team.Conversation (TeamConversationList, newTeamConversation, ne testObject_TeamConversationList_team_1 :: TeamConversationList testObject_TeamConversationList_team_1 = - ( newTeamConversationList - [ (newTeamConversation ((Id (fromJust (UUID.fromString "00000012-0000-0018-0000-00260000002b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000002d-0000-0063-0000-006900000013"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000002e-0000-003c-0000-00440000000e"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000067-0000-003a-0000-006100000049"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000005f-0000-0003-0000-005a00000075"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000007f-0000-0018-0000-00250000007c"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000006a-0000-0020-0000-001a00000073"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000002e-0000-006a-0000-005f00000003"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000034-0000-0021-0000-00330000005b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000048-0000-0011-0000-002a00000004"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000031-0000-0018-0000-00060000001a"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000056-0000-000e-0000-004300000028"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000067-0000-007f-0000-003600000031"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000066-0000-0053-0000-006a00000034"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000000f-0000-0071-0000-001b00000057"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000032-0000-0035-0000-00210000003b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000004-0000-000d-0000-002100000067"))))) - ] - ) + newTeamConversationList + [ newTeamConversation (Id (fromJust (UUID.fromString "00000012-0000-0018-0000-00260000002b"))), + newTeamConversation (Id (fromJust (UUID.fromString "0000002d-0000-0063-0000-006900000013"))) + ] testObject_TeamConversationList_team_2 :: TeamConversationList testObject_TeamConversationList_team_2 = - ( newTeamConversationList - [ (newTeamConversation ((Id (fromJust (UUID.fromString "00000064-0000-0045-0000-007d00000023"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000000d-0000-0080-0000-00550000001b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000004e-0000-0053-0000-004600000056"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000006e-0000-003c-0000-003200000071"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000067-0000-002f-0000-007a0000007f"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000000a-0000-0027-0000-004e0000005f"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000006-0000-0026-0000-000000000054"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000006e-0000-007e-0000-001600000035"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000002c-0000-0057-0000-007e00000070"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000074-0000-0053-0000-005f00000006"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000028-0000-005c-0000-00050000006b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000018-0000-0061-0000-004a00000024"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000005a-0000-007b-0000-000800000033"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000027-0000-0043-0000-006800000068"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000056-0000-0018-0000-003f00000001"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000014-0000-0066-0000-00440000001b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000007f-0000-0071-0000-007f0000001b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000018-0000-004d-0000-005000000080"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000018-0000-003e-0000-00140000006e"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000033-0000-005c-0000-001e0000000d"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000004d-0000-0021-0000-00360000000e"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000057-0000-003f-0000-003700000065"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000006f-0000-003e-0000-000300000051"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000038-0000-0025-0000-00030000003b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000003c-0000-0069-0000-005000000035"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000005f-0000-006b-0000-00260000004e"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000001c-0000-001c-0000-00530000000c"))))) - ] - ) - -testObject_TeamConversationList_team_3 :: TeamConversationList -testObject_TeamConversationList_team_3 = - ( newTeamConversationList - [ (newTeamConversation ((Id (fromJust (UUID.fromString "00000046-0000-0026-0000-005600000014"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000006b-0000-0042-0000-002c00000074"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000006d-0000-006d-0000-006100000027"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000079-0000-0024-0000-004600000011"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000044-0000-0005-0000-003800000008"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000052-0000-005e-0000-00200000001a"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000009-0000-0038-0000-001b00000065"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000029-0000-0045-0000-004500000078"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000001e-0000-0036-0000-006400000045"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000050-0000-0066-0000-000500000075"))))) - ] - ) - -testObject_TeamConversationList_team_4 :: TeamConversationList -testObject_TeamConversationList_team_4 = - ( newTeamConversationList - [ (newTeamConversation ((Id (fromJust (UUID.fromString "00000076-0000-0038-0000-003c00000043"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000046-0000-001f-0000-005800000080"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000023-0000-0070-0000-006f00000077"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000006-0000-0031-0000-004700000053"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000057-0000-0041-0000-001600000013"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000007b-0000-003c-0000-004800000063"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000028-0000-0009-0000-004c00000009"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000001a-0000-007b-0000-00460000007f"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000052-0000-002e-0000-001000000064"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000003d-0000-002a-0000-00290000007b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000004-0000-0033-0000-00780000005e"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000006b-0000-007f-0000-001d0000002c"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000017-0000-0079-0000-001c00000066"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000002f-0000-0024-0000-001000000074"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000010-0000-000c-0000-001700000046"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000003-0000-0049-0000-003100000022"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000011-0000-0051-0000-003300000061"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000003e-0000-0077-0000-004c00000022"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000007e-0000-0048-0000-007200000056"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000006f-0000-0007-0000-00190000004f"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000002d-0000-0048-0000-001c0000007e"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000004c-0000-0071-0000-007a00000071"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000006-0000-0002-0000-002000000068"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000002e-0000-0037-0000-005e00000027"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000056-0000-006d-0000-004d00000024"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000004a-0000-0038-0000-001e0000003b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000033-0000-001a-0000-004a0000001a"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000001a-0000-0070-0000-007000000019"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000006b-0000-0013-0000-004a00000018"))))) - ] - ) - -testObject_TeamConversationList_team_5 :: TeamConversationList -testObject_TeamConversationList_team_5 = - ( newTeamConversationList - [ (newTeamConversation ((Id (fromJust (UUID.fromString "00000073-0000-005a-0000-00250000000d"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000033-0000-005c-0000-006e00000014"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000017-0000-005d-0000-003b00000023"))))) - ] - ) - -testObject_TeamConversationList_team_6 :: TeamConversationList -testObject_TeamConversationList_team_6 = - ( newTeamConversationList - [ (newTeamConversation ((Id (fromJust (UUID.fromString "0000007c-0000-007f-0000-00730000000d"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000028-0000-0037-0000-000b00000016"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000021-0000-0064-0000-003900000002"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000064-0000-001f-0000-00350000001b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000002d-0000-007b-0000-00770000003e"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000064-0000-0068-0000-007700000068"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000061-0000-000b-0000-00170000005c"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000005c-0000-0001-0000-004e00000003"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000008-0000-002b-0000-002d00000022"))))) - ] - ) - -testObject_TeamConversationList_team_7 :: TeamConversationList -testObject_TeamConversationList_team_7 = - ( newTeamConversationList - [ (newTeamConversation ((Id (fromJust (UUID.fromString "00000068-0000-0010-0000-002700000004"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000006d-0000-0036-0000-000e00000080"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000003-0000-0068-0000-000000000006"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000024-0000-0018-0000-005d00000050"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000040-0000-0001-0000-00670000002e"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000002-0000-0016-0000-004300000052"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000007b-0000-0073-0000-002700000048"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000003b-0000-0048-0000-002500000015"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000055-0000-007c-0000-001500000051"))))) - ] - ) - -testObject_TeamConversationList_team_8 :: TeamConversationList -testObject_TeamConversationList_team_8 = - ( newTeamConversationList - [ (newTeamConversation ((Id (fromJust (UUID.fromString "00000026-0000-0066-0000-00170000007b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000064-0000-0015-0000-001f00000071"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000063-0000-0049-0000-004100000018"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000050-0000-002b-0000-000300000001"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000035-0000-006e-0000-002f00000057"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000006f-0000-0064-0000-003b0000002d"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000003e-0000-0009-0000-00630000001d"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000002a-0000-004d-0000-001b00000036"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000002d-0000-0073-0000-007d00000010"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000016-0000-0007-0000-00690000002d"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000043-0000-001f-0000-007500000002"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000002-0000-0012-0000-006200000028"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000019-0000-003a-0000-002300000023"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000050-0000-006d-0000-00610000000c"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000068-0000-0048-0000-003200000004"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000003-0000-0024-0000-002000000015"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000027-0000-0003-0000-007600000028"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000074-0000-005d-0000-00100000005d"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000071-0000-0075-0000-000a0000002c"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000012-0000-0071-0000-004d00000010"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000006f-0000-003f-0000-005a00000026"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000016-0000-0069-0000-00500000000a"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000033-0000-000b-0000-003000000046"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000002d-0000-005f-0000-007f0000001b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000057-0000-0050-0000-002100000074"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000077-0000-0063-0000-00360000000e"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000058-0000-0011-0000-001200000005"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000004a-0000-0037-0000-003000000034"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000029-0000-0043-0000-006700000030"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000039-0000-003e-0000-008000000051"))))) - ] - ) - -testObject_TeamConversationList_team_9 :: TeamConversationList -testObject_TeamConversationList_team_9 = - ( newTeamConversationList - [ (newTeamConversation ((Id (fromJust (UUID.fromString "00000058-0000-007c-0000-002a0000005f"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000080-0000-0009-0000-006500000038"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000004e-0000-000a-0000-004e00000039"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000062-0000-001e-0000-004c00000058"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000004-0000-0021-0000-00670000000a"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000004f-0000-0063-0000-004a0000004b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000044-0000-0017-0000-006300000067"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000006a-0000-0070-0000-002e0000000a"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000049-0000-0080-0000-006000000025"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000007d-0000-0040-0000-001700000066"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000057-0000-0045-0000-00610000006c"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000001-0000-0042-0000-005b00000057"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000048-0000-0032-0000-000000000069"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000003-0000-0022-0000-00370000005b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000007-0000-0068-0000-00150000001f"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000003a-0000-0067-0000-00060000003e"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000001e-0000-0043-0000-002800000065"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000053-0000-001f-0000-001700000006"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000068-0000-0024-0000-004900000037"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000005-0000-0019-0000-00670000005c"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000029-0000-0003-0000-00520000004c"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000080-0000-002f-0000-002b0000006f"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000021-0000-002e-0000-004f0000005e"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000006a-0000-0023-0000-00560000001b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000066-0000-007b-0000-00160000005c"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000004e-0000-0008-0000-006b00000049"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000005b-0000-0020-0000-005000000006"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000052-0000-0038-0000-003400000074"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000067-0000-006f-0000-00370000002e"))))) - ] - ) - -testObject_TeamConversationList_team_10 :: TeamConversationList -testObject_TeamConversationList_team_10 = - ( newTeamConversationList - [ (newTeamConversation ((Id (fromJust (UUID.fromString "00000070-0000-007d-0000-001400000009"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000065-0000-0057-0000-00190000004a"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000049-0000-0030-0000-006b00000005"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000007c-0000-0065-0000-001100000066"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000057-0000-0039-0000-000400000071"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000003e-0000-0053-0000-007f0000003c"))))) - ] - ) - -testObject_TeamConversationList_team_11 :: TeamConversationList -testObject_TeamConversationList_team_11 = - ( newTeamConversationList - [ (newTeamConversation ((Id (fromJust (UUID.fromString "00000038-0000-0030-0000-006700000067"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000000-0000-006a-0000-00220000007c"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000055-0000-004f-0000-005500000047"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000064-0000-003d-0000-006500000060"))))) - ] - ) - -testObject_TeamConversationList_team_12 :: TeamConversationList -testObject_TeamConversationList_team_12 = - ( newTeamConversationList - [ (newTeamConversation ((Id (fromJust (UUID.fromString "00000056-0000-0042-0000-00120000004e"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000010-0000-002b-0000-002600000066"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000006b-0000-0054-0000-005300000004"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000000e-0000-006f-0000-000c00000038"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000038-0000-0021-0000-005500000008"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000005b-0000-007a-0000-00230000002d"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000078-0000-000e-0000-004300000065"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000036-0000-0003-0000-000500000011"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000043-0000-0032-0000-005200000069"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000000c-0000-0003-0000-001400000018"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000002a-0000-0020-0000-005200000053"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000047-0000-007b-0000-00670000000b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000001a-0000-005b-0000-00250000000c"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000004b-0000-005b-0000-004200000001"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000057-0000-0073-0000-003d00000006"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000053-0000-0038-0000-006600000048"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000000d-0000-0022-0000-00800000006f"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000005e-0000-0023-0000-000700000012"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000046-0000-0071-0000-005f00000070"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000056-0000-0024-0000-003400000018"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000054-0000-0056-0000-007000000058"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000046-0000-0011-0000-001500000007"))))) - ] - ) - -testObject_TeamConversationList_team_13 :: TeamConversationList -testObject_TeamConversationList_team_13 = - ( newTeamConversationList - [ (newTeamConversation ((Id (fromJust (UUID.fromString "0000006a-0000-0043-0000-007f00000048"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000007d-0000-005f-0000-000a00000024"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000007a-0000-0046-0000-003800000023"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000023-0000-006b-0000-002000000068"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000041-0000-0000-0000-007000000005"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000007a-0000-0075-0000-00200000007a"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000038-0000-0023-0000-001a00000022"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000035-0000-004f-0000-000400000072"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000065-0000-001a-0000-00680000004d"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000002f-0000-0037-0000-00020000000f"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000023-0000-0040-0000-005b0000001c"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000000-0000-0074-0000-007b00000019"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000004e-0000-0025-0000-006900000014"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000063-0000-0000-0000-002100000043"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000004e-0000-0018-0000-004d0000003a"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000052-0000-004e-0000-002700000075"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000046-0000-0014-0000-000100000040"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000049-0000-0004-0000-00280000000a"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000004-0000-0012-0000-00150000006e"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000009-0000-003c-0000-006400000055"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000008-0000-003d-0000-003c00000003"))))) - ] - ) - -testObject_TeamConversationList_team_14 :: TeamConversationList -testObject_TeamConversationList_team_14 = - ( newTeamConversationList - [ (newTeamConversation ((Id (fromJust (UUID.fromString "00000039-0000-005c-0000-000e00000044"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000080-0000-0061-0000-005d00000066"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000011-0000-0009-0000-006c00000065"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000002a-0000-0026-0000-001e00000007"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000023-0000-005e-0000-007300000058"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000056-0000-006a-0000-004100000045"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000006d-0000-0027-0000-00080000000d"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000024-0000-0028-0000-007700000051"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000004-0000-001c-0000-004c00000073"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000006f-0000-002f-0000-003400000023"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000005d-0000-0057-0000-00580000006a"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000034-0000-0016-0000-002500000036"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000033-0000-006c-0000-00420000003d"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000008-0000-005d-0000-004600000002"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000006a-0000-002b-0000-005800000035"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000006e-0000-0007-0000-005800000075"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000047-0000-002b-0000-000100000080"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000013-0000-001b-0000-003200000000"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000006-0000-0013-0000-004d0000006e"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000074-0000-0041-0000-007200000079"))))) - ] - ) - -testObject_TeamConversationList_team_15 :: TeamConversationList -testObject_TeamConversationList_team_15 = - ( newTeamConversationList - [ (newTeamConversation ((Id (fromJust (UUID.fromString "0000001a-0000-0013-0000-006400000036"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000070-0000-007e-0000-002f00000057"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000002-0000-006e-0000-006800000040"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000080-0000-005a-0000-000e00000024"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000069-0000-007c-0000-00550000002f"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000068-0000-0041-0000-000e0000003e"))))) - ] - ) - -testObject_TeamConversationList_team_16 :: TeamConversationList -testObject_TeamConversationList_team_16 = - ( newTeamConversationList - [ (newTeamConversation ((Id (fromJust (UUID.fromString "00000012-0000-0066-0000-003800000061"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000049-0000-0007-0000-003f0000001d"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000045-0000-0038-0000-005f00000072"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000032-0000-0069-0000-005b00000011"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000053-0000-0073-0000-00280000005d"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000046-0000-0068-0000-004f00000042"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000003-0000-0056-0000-00780000000f"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000006b-0000-0064-0000-001b00000024"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000004e-0000-0052-0000-004000000072"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000052-0000-0080-0000-005100000029"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000079-0000-0018-0000-000600000047"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000009-0000-0029-0000-003100000043"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000048-0000-002e-0000-00220000005b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000003b-0000-004d-0000-001700000055"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000006c-0000-0028-0000-002100000076"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000033-0000-0052-0000-003300000080"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000004c-0000-005f-0000-00390000004d"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000007a-0000-004b-0000-00440000003e"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000052-0000-007a-0000-003d00000036"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000018-0000-0058-0000-003700000019"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000034-0000-0011-0000-007c00000011"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000056-0000-0057-0000-00630000002b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000051-0000-0018-0000-00590000007a"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000004b-0000-0011-0000-002100000014"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000060-0000-0003-0000-00490000001b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000042-0000-006e-0000-001e0000001a"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000005d-0000-0065-0000-004b00000045"))))) - ] - ) - -testObject_TeamConversationList_team_17 :: TeamConversationList -testObject_TeamConversationList_team_17 = - ( newTeamConversationList - [ (newTeamConversation ((Id (fromJust (UUID.fromString "00000053-0000-0070-0000-007f0000001c"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000036-0000-0017-0000-002a00000076"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000065-0000-004f-0000-00710000002d"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000067-0000-0037-0000-004d0000007b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000004-0000-0071-0000-000800000015"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000047-0000-0062-0000-002900000024"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000072-0000-0027-0000-001300000046"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000000f-0000-0034-0000-00720000000f"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000021-0000-005d-0000-003300000024"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000023-0000-000b-0000-00160000000d"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000022-0000-0042-0000-003400000043"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000020-0000-0033-0000-00780000006b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000074-0000-0067-0000-005f00000042"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000000f-0000-0079-0000-00630000007e"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000001a-0000-0045-0000-003900000053"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000000-0000-003e-0000-003d00000000"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000039-0000-0052-0000-000500000034"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000004e-0000-002d-0000-00030000005c"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000036-0000-0067-0000-007400000054"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000047-0000-0075-0000-001200000054"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000002e-0000-003d-0000-000700000080"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000005d-0000-0006-0000-00010000001a"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000012-0000-0073-0000-002000000058"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000073-0000-0015-0000-005e0000006e"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000047-0000-0019-0000-00510000005a"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000004b-0000-0074-0000-007000000021"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000007a-0000-0040-0000-006f00000075"))))) - ] - ) - -testObject_TeamConversationList_team_18 :: TeamConversationList -testObject_TeamConversationList_team_18 = - ( newTeamConversationList - [ (newTeamConversation ((Id (fromJust (UUID.fromString "00000049-0000-000d-0000-007600000068"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000002a-0000-0033-0000-006400000019"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000080-0000-0075-0000-00400000004e"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000062-0000-0073-0000-002a00000051"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000003b-0000-004b-0000-005c00000064"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000016-0000-001a-0000-00430000003d"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000002f-0000-0005-0000-004f00000031"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000000-0000-0043-0000-001a0000000c"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000003-0000-001c-0000-003a0000002b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000001c-0000-007b-0000-00170000000a"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000073-0000-0073-0000-000000000074"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000005b-0000-0069-0000-00490000002d"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000003c-0000-0012-0000-000400000000"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000016-0000-004e-0000-003800000057"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000008-0000-0022-0000-002000000004"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000070-0000-0011-0000-00260000004a"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000002c-0000-007a-0000-00340000006e"))))) - ] - ) - -testObject_TeamConversationList_team_19 :: TeamConversationList -testObject_TeamConversationList_team_19 = - ( newTeamConversationList - [ (newTeamConversation ((Id (fromJust (UUID.fromString "00000000-0000-0041-0000-007b00000060"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000003f-0000-0059-0000-000700000073"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000065-0000-0056-0000-007e00000066"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000002b-0000-000b-0000-007a00000065"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000071-0000-003a-0000-001b00000027"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000070-0000-004f-0000-008000000008"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000003c-0000-000d-0000-00510000005a"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000045-0000-006e-0000-004200000072"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000001b-0000-003b-0000-007900000004"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000002d-0000-0077-0000-006400000054"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000001a-0000-005e-0000-003e00000012"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000057-0000-000c-0000-00370000003b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000031-0000-0010-0000-006500000077"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000028-0000-004b-0000-00460000007b"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000005-0000-0040-0000-006400000024"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000042-0000-005b-0000-002d00000031"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000065-0000-0067-0000-00610000006d"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000007f-0000-0036-0000-00770000000d"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000058-0000-0042-0000-003700000054"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000002a-0000-0001-0000-000700000015"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000002f-0000-003c-0000-003b00000000"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "00000065-0000-0049-0000-00720000006c"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000000f-0000-0021-0000-004c00000055"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000005b-0000-002e-0000-00140000003d"))))) - ] - ) - -testObject_TeamConversationList_team_20 :: TeamConversationList -testObject_TeamConversationList_team_20 = - ( newTeamConversationList - [ (newTeamConversation ((Id (fromJust (UUID.fromString "00000007-0000-0017-0000-007500000074"))))), - (newTeamConversation ((Id (fromJust (UUID.fromString "0000003b-0000-0055-0000-003f00000059"))))) - ] - ) + newTeamConversationList + [ newTeamConversation (Id (fromJust (UUID.fromString "00000064-0000-0045-0000-007d00000023"))), + newTeamConversation (Id (fromJust (UUID.fromString "0000000d-0000-0080-0000-00550000001b"))) + ] diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamConversation_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamConversation_team.hs index 8f519c4f5f..cbcce2a5b7 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamConversation_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamConversation_team.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE OverloadedLists #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -26,80 +24,8 @@ import Wire.API.Team.Conversation (TeamConversation, newTeamConversation) testObject_TeamConversation_team_1 :: TeamConversation testObject_TeamConversation_team_1 = - (newTeamConversation ((Id (fromJust (UUID.fromString "00000054-0000-0032-0000-001d0000003e"))))) + newTeamConversation (Id (fromJust (UUID.fromString "00000054-0000-0032-0000-001d0000003e"))) testObject_TeamConversation_team_2 :: TeamConversation testObject_TeamConversation_team_2 = - (newTeamConversation ((Id (fromJust (UUID.fromString "00000021-0000-0059-0000-00390000004c"))))) - -testObject_TeamConversation_team_3 :: TeamConversation -testObject_TeamConversation_team_3 = - (newTeamConversation ((Id (fromJust (UUID.fromString "00000020-0000-0022-0000-00550000003b"))))) - -testObject_TeamConversation_team_4 :: TeamConversation -testObject_TeamConversation_team_4 = - (newTeamConversation ((Id (fromJust (UUID.fromString "0000002d-0000-0034-0000-004600000023"))))) - -testObject_TeamConversation_team_5 :: TeamConversation -testObject_TeamConversation_team_5 = - (newTeamConversation ((Id (fromJust (UUID.fromString "0000007d-0000-005d-0000-003d00000076"))))) - -testObject_TeamConversation_team_6 :: TeamConversation -testObject_TeamConversation_team_6 = - (newTeamConversation ((Id (fromJust (UUID.fromString "0000000a-0000-0013-0000-00420000002e"))))) - -testObject_TeamConversation_team_7 :: TeamConversation -testObject_TeamConversation_team_7 = - (newTeamConversation ((Id (fromJust (UUID.fromString "0000005d-0000-0080-0000-002800000080"))))) - -testObject_TeamConversation_team_8 :: TeamConversation -testObject_TeamConversation_team_8 = - (newTeamConversation ((Id (fromJust (UUID.fromString "0000002e-0000-006d-0000-003700000042"))))) - -testObject_TeamConversation_team_9 :: TeamConversation -testObject_TeamConversation_team_9 = - (newTeamConversation ((Id (fromJust (UUID.fromString "0000000d-0000-001b-0000-006800000047"))))) - -testObject_TeamConversation_team_10 :: TeamConversation -testObject_TeamConversation_team_10 = - (newTeamConversation ((Id (fromJust (UUID.fromString "00000023-0000-0024-0000-003200000067"))))) - -testObject_TeamConversation_team_11 :: TeamConversation -testObject_TeamConversation_team_11 = - (newTeamConversation ((Id (fromJust (UUID.fromString "00000003-0000-0041-0000-002600000041"))))) - -testObject_TeamConversation_team_12 :: TeamConversation -testObject_TeamConversation_team_12 = - (newTeamConversation ((Id (fromJust (UUID.fromString "0000007d-0000-0049-0000-001f00000034"))))) - -testObject_TeamConversation_team_13 :: TeamConversation -testObject_TeamConversation_team_13 = - (newTeamConversation ((Id (fromJust (UUID.fromString "00000025-0000-003c-0000-003d00000032"))))) - -testObject_TeamConversation_team_14 :: TeamConversation -testObject_TeamConversation_team_14 = - (newTeamConversation ((Id (fromJust (UUID.fromString "0000005b-0000-0065-0000-002a00000060"))))) - -testObject_TeamConversation_team_15 :: TeamConversation -testObject_TeamConversation_team_15 = - (newTeamConversation ((Id (fromJust (UUID.fromString "0000001f-0000-0037-0000-005a0000004d"))))) - -testObject_TeamConversation_team_16 :: TeamConversation -testObject_TeamConversation_team_16 = - (newTeamConversation ((Id (fromJust (UUID.fromString "00000044-0000-000a-0000-007f0000001d"))))) - -testObject_TeamConversation_team_17 :: TeamConversation -testObject_TeamConversation_team_17 = - (newTeamConversation ((Id (fromJust (UUID.fromString "00000009-0000-0060-0000-005c00000049"))))) - -testObject_TeamConversation_team_18 :: TeamConversation -testObject_TeamConversation_team_18 = - (newTeamConversation ((Id (fromJust (UUID.fromString "0000005f-0000-0051-0000-003d00000026"))))) - -testObject_TeamConversation_team_19 :: TeamConversation -testObject_TeamConversation_team_19 = - (newTeamConversation ((Id (fromJust (UUID.fromString "0000003d-0000-0025-0000-00170000002e"))))) - -testObject_TeamConversation_team_20 :: TeamConversation -testObject_TeamConversation_team_20 = - (newTeamConversation ((Id (fromJust (UUID.fromString "00000007-0000-0053-0000-001500000035"))))) + newTeamConversation (Id (fromJust (UUID.fromString "00000021-0000-0059-0000-00390000004c"))) diff --git a/libs/wire-api/test/golden/fromJSON/testObject_NewConvManaged_user_2.json b/libs/wire-api/test/golden/fromJSON/testObject_NewConvManaged_user_2.json deleted file mode 100644 index f3820d40cc..0000000000 --- a/libs/wire-api/test/golden/fromJSON/testObject_NewConvManaged_user_2.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "access": [ - "private", - "invite", - "link" - ], - "access_role": "non_activated", - "conversation_role": "bewzponl1a3c_l6ou", - "message_timer": 5509522199847054, - "name": "󳂣\u001a5", - "qualified_users": [ - { - "domain": "test.example.com", - "id": "00000000-0000-0000-0000-000100000001" - } - ], - "team": { - "managed": false, - "teamid": "00000002-0000-0002-0000-000200000002" - }, - "users": [ - "00000002-0000-0001-0000-000400000000" - ] -} diff --git a/libs/wire-api/test/golden/fromJSON/testObject_NewConvUnmanaged_user_1.json b/libs/wire-api/test/golden/fromJSON/testObject_NewConvUnmanaged_user_1.json deleted file mode 100644 index f58dad1b1a..0000000000 --- a/libs/wire-api/test/golden/fromJSON/testObject_NewConvUnmanaged_user_1.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "access": [ - "private", - "invite" - ], - "access_role_v2": [ - "team_member", - "guest" - ], - "users": [ - "00000001-0000-0000-0000-000000000001", - "00000000-0000-0000-0000-000000000000" - ], - "conversation_role": "8tp2gs7b6", - "team": { - "managed": false, - "teamid": "00000000-0000-0001-0000-000000000000" - }, - "receipt_mode": 1, - "message_timer": 3320987366258987, - "qualified_users": [] -} diff --git a/libs/wire-api/test/golden/fromJSON/testObject_NewConvUnmanaged_user_2.json b/libs/wire-api/test/golden/fromJSON/testObject_NewConvUnmanaged_user_2.json deleted file mode 100644 index aae8702ff6..0000000000 --- a/libs/wire-api/test/golden/fromJSON/testObject_NewConvUnmanaged_user_2.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "access": [], - "conversation_role": "vmao7psxph3fenvbpsu1u57fns5pfo53d67k98om378rnxr0crcpak_mpspn8q_3m1b02n2n133s1d7q5w3qgmt_5e_dgtvzon8an7dtauiecd32", - "message_timer": 2406292360203739, - "name": "😏􃉷", - "qualified_users": [ - { - "domain": "testdomain.example.com", - "id": "00000000-0000-0000-0000-000100000001" - } - ], - "receipt_mode": -1, - "team": { - "managed": true, - "teamid": "00000000-0000-0001-0000-000000000001" - }, - "users": [] -} diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_10.json b/libs/wire-api/test/golden/testObject_InvitationList_team_10.json index f5a607418e..c06f56c3cf 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_10.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_10.json @@ -9,7 +9,8 @@ "name": "P𥖧\u0006'e\u0010\u001d\"\u0011K󽗨Fcvm[\"Sc}U𑊒􂌨󿔟~!E􀖇\u000bV", "phone": null, "role": "member", - "team": "00000000-0000-0001-0000-000100000000" + "team": "00000000-0000-0001-0000-000100000000", + "url": null } ] } diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_11.json b/libs/wire-api/test/golden/testObject_InvitationList_team_11.json index 621efe7906..3b5e6bce7f 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_11.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_11.json @@ -9,7 +9,8 @@ "name": "G\\,\u0000=ෝI-w󠀹}𠉭抳-92\u0013@\u0006\u001f\\F\u001a\"-r꒫6\u000fඬ\u001f*}c󼘹\u001f\u0007T8m@旅M\u0012#MIq\r4nW􍦐y\u0005Ud룫#𫶒5\n\u0002V]𨡀\"󶂃𩫘0:ﲼ𮭩+\u0001\u000bP󹎷X镟􅔧.\u0019N\"𬋻", "phone": "+872574694", "role": "admin", - "team": "00000000-0000-0001-0000-000100000000" + "team": "00000000-0000-0001-0000-000100000000", + "url" :null }, { "created_at": "1864-05-09T23:06:13.648Z", @@ -19,7 +20,8 @@ "name": "叕5q}B\u0001𦌜`イw\\X@󼶝𢼈7Mw,*z{𠚷&~", "phone": "+143031479742", "role": "partner", - "team": "00000000-0000-0001-0000-000000000001" + "team": "00000000-0000-0001-0000-000000000001", + "url" :null }, { "created_at": "1864-05-09T10:37:03.809Z", @@ -29,7 +31,8 @@ "name": "V􈫮\u0010qYヒCU\u000e􄕀fQJ\u0005ਓq+\u0007\u0016󱊸\u0011@𤠼`坟qh+𬾬A7𦄡Y \u0011Tㅎ1_􈩇#B<􂡁;a6o=", "phone": "+236346166386230", "role": "partner", - "team": "00000001-0000-0000-0000-000000000000" + "team": "00000001-0000-0000-0000-000000000000", + "url" :null }, { "created_at": "1864-05-09T04:46:03.504Z", @@ -39,7 +42,8 @@ "name": ",􃠾{ս\u000c𬕻Uh죙\t\u001b\u0004\u0001O@\u001a_\u0002D􎰥𦀛\u0016g}", "phone": "+80162248", "role": "admin", - "team": "00000001-0000-0001-0000-000100000001" + "team": "00000001-0000-0001-0000-000100000001", + "url" :null }, { "created_at": "1864-05-09T12:53:52.047Z", @@ -49,7 +53,8 @@ "name": null, "phone": null, "role": "owner", - "team": "00000000-0000-0001-0000-000100000001" + "team": "00000000-0000-0001-0000-000100000001", + "url" :null } ] } diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_16.json b/libs/wire-api/test/golden/testObject_InvitationList_team_16.json index fc14ac96bf..535fe0678e 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_16.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_16.json @@ -9,7 +9,8 @@ "name": "E𝘆YM<󾪤j􆢆\r􇳗O󴟴MCU\u001eI󳊃m𔒷hG\u0012|:P􅛽Vj\u001c\u0000ffgG)K{􁇏7x5󱟰𪔘\n\u000clT􆊞", "phone": "+36515555", "role": "owner", - "team": "00000001-0000-0001-0000-000100000001" + "team": "00000001-0000-0001-0000-000100000001", + "url": null } ] } diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_17.json b/libs/wire-api/test/golden/testObject_InvitationList_team_17.json index c2c9ba044a..eba7991502 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_17.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_17.json @@ -9,7 +9,8 @@ "name": null, "phone": null, "role": "partner", - "team": "00000001-0000-0000-0000-000100000000" + "team": "00000001-0000-0000-0000-000100000000", + "url": null } ] } diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_2.json b/libs/wire-api/test/golden/testObject_InvitationList_team_2.json index e2f2601fb1..076b78a0d4 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_2.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_2.json @@ -9,7 +9,8 @@ "name": "fuC9p􌌅A𧻢\u000c\u0005\u000e刣N룞_?oCX.U\r𧾠W腈󽥝\u0013\t[錣\u0016/⃘A𣚁𪔍\u0014H𠽙\u0002𨯠\u0004𨒤o\u0013", "phone": "+851333011", "role": "owner", - "team": "00000000-0000-0000-0000-000000000001" + "team": "00000000-0000-0000-0000-000000000001", + "url": "https://example.com/inv14" } ] } diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_20.json b/libs/wire-api/test/golden/testObject_InvitationList_team_20.json index 1b50ca8071..26a5ab0134 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_20.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_20.json @@ -9,7 +9,8 @@ "name": null, "phone": "+745177056001783", "role": "partner", - "team": "00000001-0000-0001-0000-000000000000" + "team": "00000001-0000-0001-0000-000000000000", + "url": null }, { "created_at": "1864-05-09T18:56:29.712Z", @@ -19,7 +20,8 @@ "name": "YPf╞:\u0005Ỉ&\u0018\u0011󽧛%ꦡk𪯋􅥏:Q\u0005F+\u0008b8Jh􌎓K\u0007\u001dY\u0004􃏡\u000f󽝰\u0016 􁗠6>I󾉩B$z?𤢾wECB\u001e𥼬덄\"W𗤞󲴂@\u001eg)\u0001m!-U􇧦󵜰o\u0006a\u0004𭂢;R􂪧kgT􍆈f\u0004\u001e\rp𓎎󿉊X/􄂲)\u00025.Ym󵳬n싟N\u0013𫅄]?'𠴺a4\"󳟾!i5\u001e\u001dC14", "phone": null, "role": "owner", - "team": "00000001-0000-0000-0000-000100000000" + "team": "00000001-0000-0000-0000-000100000000", + "url": null } ] } diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_4.json b/libs/wire-api/test/golden/testObject_InvitationList_team_4.json index e41e76da52..3063b4fdeb 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_4.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_4.json @@ -9,7 +9,8 @@ "name": "R6𠥄𠮥VQ𭴢\u001a\u0001𬄺0C􉶍\u001bR𭗈𞡊@韉Z?\u0002𩖫􄭦e}\u0001\u0017\u0004m𭉂\u001f]󰺞𮉗􂨮󰶌\u0008\u0011zfw-5𝝖\u0018􃸂 \u0019e\u0014|㡚Vo{􆳗\u0013#\u001fS꿻&zz𧏏9𢱋,\u000f\u000c\u0001p󺜰\u0010𧵪􂸑.&󳢨kZ쓿u\u0008왌􎴟n:􍝋D$.Q", "phone": "+60506387292", "role": "admin", - "team": "00000000-0000-0001-0000-000000000000" + "team": "00000000-0000-0001-0000-000000000000", + "url": null }, { "created_at": "1864-05-09T09:00:02.901Z", @@ -19,7 +20,8 @@ "name": "\u0012}q\u0018=SA\u0003x\t\u0003\\\u000b[\u0008)(\u001b]𡋃Y\u000b@pꈫl뀉𦛌\u0000\t􌤢\u00011\u0011\u0005󹝃\"i猔\u0019\u0008\u0006\u000f\u0012v\u0006", "phone": "+913945015", "role": "admin", - "team": "00000000-0000-0001-0000-000100000000" + "team": "00000000-0000-0001-0000-000100000000", + "url": null }, { "created_at": "1864-05-09T11:10:31.203Z", @@ -29,7 +31,8 @@ "name": "&􂧽Ec\u0000㼓}k󼾘l𪍯\u001fJ\u00190^.+F\u0000\u000c$'`!\u0017[p󾓉}>E0y𗢸#4I\u0007𐐡jc\u001bgt埉􊹘P\u0014!􋣥E93'Y$YL뜦b\r:,𬘞\u000e𥚟y\u0003;􃺹􌛖z4z-D􋰳a𡽜6𨏝r󼖨󱌂J\u0010밆", "phone": "+17046334", "role": "member", - "team": "00000001-0000-0000-0000-000000000001" + "team": "00000001-0000-0000-0000-000000000001", + "url": null }, { "created_at": "1864-05-09T23:41:34.529Z", @@ -39,7 +42,8 @@ "name": "Ft*O1\u0008&\u000e\u0018<𑨛􊰋m\n\u0014\u0012; \u0003󱚥\u0011􂬫\"k.T󹴑[[\u001c\u0004{j`\u001d󳟞c􄖫{\u001a\u001dQY𬨕\t\u0015y\t𠓳j󼿁W ", "phone": null, "role": "owner", - "team": "00000000-0000-0000-0000-000000000000" + "team": "00000000-0000-0000-0000-000000000000", + "url": null }, { "created_at": "1864-05-09T00:29:17.658Z", @@ -49,7 +53,8 @@ "name": null, "phone": "+918848647685283", "role": "admin", - "team": "00000001-0000-0000-0000-000100000000" + "team": "00000001-0000-0000-0000-000100000000", + "url": null }, { "created_at": "1864-05-09T13:34:37.117Z", @@ -59,7 +64,8 @@ "name": "Lo\r􎒩B𗚰_v󰔢􆍶󻀬􊽦9\u0002vyQ🖰&W󻟑𠸘􇹬'􁔫:𤟗𡶘􏹠}-o󿜊le8Zp󺩐􋾙)nK\u00140⛟0DE\u0015K$io\u001e|Ip2ClnU𬖍", "phone": "+2239859474784", "role": "owner", - "team": "00000001-0000-0001-0000-000100000000" + "team": "00000001-0000-0001-0000-000100000000", + "url": null } ] } diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_6.json b/libs/wire-api/test/golden/testObject_InvitationList_team_6.json index 2285c8dc7a..03aa3d0485 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_6.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_6.json @@ -9,7 +9,8 @@ "name": null, "phone": null, "role": "admin", - "team": "00000001-0000-0001-0000-000100000000" + "team": "00000001-0000-0001-0000-000100000000", + "url": null }, { "created_at": "1864-05-09T11:26:36.672Z", @@ -19,7 +20,8 @@ "name": null, "phone": "+85999765", "role": "admin", - "team": "00000000-0000-0000-0000-000100000000" + "team": "00000000-0000-0000-0000-000100000000", + "url": null }, { "created_at": "1864-05-09T00:31:56.241Z", @@ -29,7 +31,8 @@ "name": null, "phone": "+150835819626453", "role": "owner", - "team": "00000001-0000-0000-0000-000100000000" + "team": "00000001-0000-0000-0000-000100000000", + "url": null }, { "created_at": "1864-05-09T21:10:47.237Z", @@ -39,7 +42,8 @@ "name": "YBc\r웶8{\\\n􋸓+\u0008\u0016'<\u0004􈄿Z\u0007nOb􋨴􌸖𩮤}2o@v/", "phone": "+787465997389", "role": "member", - "team": "00000000-0000-0001-0000-000100000000" + "team": "00000000-0000-0001-0000-000100000000", + "url": null } ] } diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_1.json b/libs/wire-api/test/golden/testObject_Invitation_team_1.json index 9d611aa22e..ee4489e209 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_1.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_1.json @@ -6,5 +6,6 @@ "name": null, "phone": "+54687000371", "role": "admin", - "team": "00000002-0000-0001-0000-000200000002" + "team": "00000002-0000-0001-0000-000200000002", + "url": null } diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_10.json b/libs/wire-api/test/golden/testObject_Invitation_team_10.json index 447daf009c..9c189f7c13 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_10.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_10.json @@ -6,5 +6,6 @@ "name": null, "phone": "+957591063736", "role": "partner", - "team": "00000002-0000-0001-0000-000100000001" + "team": "00000002-0000-0001-0000-000100000001", + "url": null } diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_11.json b/libs/wire-api/test/golden/testObject_Invitation_team_11.json index 09e6c67ff7..a1d4b2e572 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_11.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_11.json @@ -6,5 +6,6 @@ "name": "􄘬,􍁨緌sC\nD\u001e󱫂*\u0011𧲍\u0011󲾁a󽌳𗿸{.熿𭒪빝𡨶9/ಇ<;$𭣘𠪹Z\u0005'󺠞!F􎉼󼪟n\"\n8\u001dH󼯢9𐪜z:d\u0010F𧕰y_w\ri轭!>󳓗䏩𝓖\u0008\u001a\u001c\u000fF%<𞢹\u000fh\u001b\u0003\u000f󲶳\u001fO\u0000g_𤻨뢪󺥟\u0004􂔤􊃫z~%IA'R\u0008󶽴Hv^󾲱wrjb\t𨭛\u0003", "phone": "+518729615781", "role": "admin", - "team": "00000001-0000-0001-0000-000100000000" + "team": "00000001-0000-0001-0000-000100000000", + "url": null } diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_12.json b/libs/wire-api/test/golden/testObject_Invitation_team_12.json index 866b59a789..ece82b4d17 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_12.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_12.json @@ -6,5 +6,6 @@ "name": "\u0010Z+wd^𐘊􆃨1\u0002YdXt>􇺼LSB7F9\\𠿬\u0005\n󱂟\"🀡|\u0007𦠺'\u001bTygU􎍔R칖􅧠O4󼷁E9\"󸃐\u0012Re\u0005D}􀧨𧢧􍭝\u0008V𫋾%98'\u001e9\u00064yP𔗍㡀ř\u0007w\t􌄦\u000b􇋳xv/Yl󵢬𦯯", "phone": "+68945103783764", "role": "admin", - "team": "00000000-0000-0000-0000-000000000002" + "team": "00000000-0000-0000-0000-000000000002", + "url": null } diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_13.json b/libs/wire-api/test/golden/testObject_Invitation_team_13.json index 789b4a3297..f12163f667 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_13.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_13.json @@ -6,5 +6,6 @@ "name": "U", "phone": "+549940856897515", "role": "member", - "team": "00000002-0000-0001-0000-000000000001" + "team": "00000002-0000-0001-0000-000000000001", + "url": null } diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_14.json b/libs/wire-api/test/golden/testObject_Invitation_team_14.json index 1f1ac31cc4..7b5764a687 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_14.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_14.json @@ -6,5 +6,6 @@ "name": null, "phone": "+89058877371", "role": "owner", - "team": "00000002-0000-0002-0000-000100000000" + "team": "00000002-0000-0002-0000-000100000000", + "url": null } diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_15.json b/libs/wire-api/test/golden/testObject_Invitation_team_15.json index 8ec11e965f..7d5215c782 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_15.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_15.json @@ -6,5 +6,6 @@ "name": "𑜘\u001f&KIL\u0013􉋏![\n6􏙭HEj4E⽨UL\u001f>2􅝓_\nJ킢Pv\u000e\u000fR碱8\u0008mS뇆mE\u0007g\u0016\u0005%㣑\u000c!\u000b\u001f𝈊\u0005𭇱󿄈\u000e83!j𒁾\u001d􅣣,\u001e\u0018F􃞋􏈇U\u0019Jb\u0011j\u0019Y𖢐O󶃯", "phone": "+57741900390998", "role": "owner", - "team": "00000000-0000-0002-0000-000100000001" + "team": "00000000-0000-0002-0000-000100000001", + "url": null } diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_16.json b/libs/wire-api/test/golden/testObject_Invitation_team_16.json index 1ade470dd6..853aab3be7 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_16.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_16.json @@ -6,5 +6,6 @@ "name": "\u001d\u0014Q;6/_f*7􋅎\u000f+􊳊ꋢ9", "phone": null, "role": "partner", - "team": "00000001-0000-0001-0000-000100000002" + "team": "00000001-0000-0001-0000-000100000002", + "url": null } diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_17.json b/libs/wire-api/test/golden/testObject_Invitation_team_17.json index ffaa39fdfc..d7ae310a54 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_17.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_17.json @@ -6,5 +6,6 @@ "name": "Z\u001b9E\u0015鍌𔗕}(3m𗮙𗷤'􅺒.WY;\u001e8?v-􌮰\u0012󸀳", "phone": null, "role": "admin", - "team": "00000000-0000-0001-0000-000100000000" + "team": "00000000-0000-0001-0000-000100000000", + "url": null } diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_19.json b/libs/wire-api/test/golden/testObject_Invitation_team_19.json index 4f087c6be4..aaa9b35ce0 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_19.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_19.json @@ -6,5 +6,6 @@ "name": "靸r𛋕\u0003Qi󴊗􌃗\u0019𩫻𒉓+􄮬Q?H=G-\u001e;􍝧\u000eq^K;a􀹚W\u0019 X𔖸􆂨>Mϔ朓jjbU-&󽼈v\u0000y𬙼\u0007|\u0016UfJCHjP\u000e􏘃浍DNA:~s", "phone": "+05787228893", "role": "member", - "team": "00000000-0000-0000-0000-000200000000" + "team": "00000000-0000-0000-0000-000200000000", + "url": null } diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_2.json b/libs/wire-api/test/golden/testObject_Invitation_team_2.json index c5227405c9..393eaccd4f 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_2.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_2.json @@ -6,5 +6,6 @@ "name": "􄭇} 2pGEW+\rT𩹙p𪨳𦘢&𣫡v0\u0008", "phone": null, "role": "partner", - "team": "00000000-0000-0001-0000-000000000000" + "team": "00000000-0000-0001-0000-000000000000", + "url": null } diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_20.json b/libs/wire-api/test/golden/testObject_Invitation_team_20.json index 8a036b8aff..653fafc89e 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_20.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_20.json @@ -6,5 +6,6 @@ "name": null, "phone": "+27259486019", "role": "partner", - "team": "00000001-0000-0000-0000-000000000000" + "team": "00000001-0000-0000-0000-000000000000", + "url": null } diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_3.json b/libs/wire-api/test/golden/testObject_Invitation_team_3.json index 8111542049..6222659d12 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_3.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_3.json @@ -6,5 +6,6 @@ "name": null, "phone": null, "role": "partner", - "team": "00000002-0000-0001-0000-000100000001" + "team": "00000002-0000-0001-0000-000100000001", + "url": null } diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_4.json b/libs/wire-api/test/golden/testObject_Invitation_team_4.json index 76282c4654..8e8dedc4a4 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_4.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_4.json @@ -6,5 +6,6 @@ "name": null, "phone": null, "role": "admin", - "team": "00000000-0000-0000-0000-000100000000" + "team": "00000000-0000-0000-0000-000100000000", + "url": null } diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_5.json b/libs/wire-api/test/golden/testObject_Invitation_team_5.json index 44a5cd464d..ce4196efbb 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_5.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_5.json @@ -6,5 +6,6 @@ "name": "}G_𤃊`X󻋗𠆝󷲞L\"󿶗e6:E쨕󲟇f-$𠬒Z!s2p?#\tF 8𭿰𨕿󹵇\u0004􉢘*󸚄\u0016\u0010%Y𩀄>􏘍󾨶󺶘g\"􁥰\u001a\u001a𬇟ꦛ\u0004v𭽢,𩶐(\u001dQT𤪐;􃨚\u0005\u0017B􎇮H𩣓\\󾃾,Y", "phone": "+45207005641274", "role": "owner", - "team": "00000002-0000-0000-0000-000000000001" + "team": "00000002-0000-0000-0000-000000000001", + "url": null } diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_6.json b/libs/wire-api/test/golden/testObject_Invitation_team_6.json index c847dcf4a2..37e3f45bdc 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_6.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_6.json @@ -6,5 +6,6 @@ "name": "O~\u0014U\u001e?V3_𮬰Slh􅱬Q1󶻳j|~M7􊲚􋽼𗆨\u0011K􇍼Afs𫬇lGV􏱇]`o\u0019f蓤InvfDDy\\DI𧾱􊥩\u0017B𦷬F*X\u0001\u001a얔\u0003\u0010<\u0003\u0016c\u0010,p\u000b*󵢘Vn\u000cI𑈹xS\u0002V\u001b$\u0019u󴮖xl>\u0007Z\u00144e\u0014aZ", "phone": "+75547625285", "role": "admin", - "team": "00000001-0000-0000-0000-000000000001" + "team": "00000001-0000-0000-0000-000000000001", + "url": null } diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_7.json b/libs/wire-api/test/golden/testObject_Invitation_team_7.json index d4699fe74d..844522e716 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_7.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_7.json @@ -6,5 +6,6 @@ "name": "\u0018.𛅷􈼞\u0010\u000c\u0010\u0018𤰤o;Yay:yY $\u0003<ͯ%@\u001fre>5L'R\u0013𫝳oy#]c4!𘖝U홊暧󾜸􃕢p_>f\u000e𪲈􇇪󳆗_Vm\u001f}\u0002Pz\r\u0005K\u000e+>󲆠\u0000𥝻?pu?r\u001b\u001a!?𩇕;ᦅS䥅\u0007􅠬\u0008󹹝 do secretKey <- Ed25519.generateSecretKey let publicKey = Ed25519.toPublic secretKey - let message = fromJust (mkRemoveProposalMessage secretKey publicKey gid (Epoch 1) (fromJust (kpRef' kp))) + let message = mkSignedMessage secretKey publicKey gid (Epoch 1) (ProposalMessage (mkRemoveProposal (fromJust (kpRef' kp)))) + let messageFilename = "signed-message.mls" BS.writeFile (tmp messageFilename) (rmRaw (mkRawMLS message)) let signerKeyFilename = "signer-key.bin" BS.writeFile (tmp signerKeyFilename) (convert publicKey) - void . liftIO $ spawn (cli qcid tmp ["check-signature", "--group", tmp groupFilename, "--message", tmp messageFilename, "--signer-key", tmp signerKeyFilename]) Nothing + void . liftIO $ + spawn + ( cli + qcid + tmp + [ "consume", + "--group", + tmp groupFilename, + "--signer-key", + tmp signerKeyFilename, + tmp messageFilename + ] + ) + Nothing + +testParseGroupInfoBundle :: IO () +testParseGroupInfoBundle = withSystemTempDirectory "mls" $ \tmp -> do + qcid <- do + let c = newClientId 0x3ae58155 + usr <- flip Qualified (Domain "example.com") <$> (Id <$> UUID.nextRandom) + pure (userClientQid usr c) + void . liftIO $ spawn (cli qcid tmp ["init", qcid]) Nothing + + qcid2 <- do + let c = newClientId 0x4ae58157 + usr <- flip Qualified (Domain "example.com") <$> (Id <$> UUID.nextRandom) + pure (userClientQid usr c) + void . liftIO $ spawn (cli qcid2 tmp ["init", qcid2]) Nothing + kp :: RawMLS KeyPackage <- liftIO $ decodeMLSError <$> spawn (cli qcid2 tmp ["key-package", "create"]) Nothing + liftIO $ BS.writeFile (tmp qcid2) (rmRaw kp) + + let groupFilename = "group" + let gid = GroupId "abcd" + createGroup tmp qcid groupFilename gid + + void $ + liftIO $ + spawn + ( cli + qcid + tmp + [ "member", + "add", + "--group", + tmp groupFilename, + "--in-place", + tmp qcid2, + "--group-state-out", + tmp "group-info-bundle" + ] + ) + Nothing + + bundleBS <- BS.readFile (tmp "group-info-bundle") + case decodeMLS' @PublicGroupState bundleBS of + Left err -> assertFailure ("Failed parsing PublicGroupState: " <> T.unpack err) + Right _ -> pure () createGroup :: FilePath -> String -> String -> GroupId -> IO () createGroup tmp store groupName gid = do diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/MLS.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/MLS.hs index 34aaeeb9ff..c64845a252 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/MLS.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/MLS.hs @@ -28,7 +28,9 @@ import Wire.API.MLS.Extension import Wire.API.MLS.KeyPackage import Wire.API.MLS.Message import Wire.API.MLS.Proposal +import Wire.API.MLS.PublicGroupState import Wire.API.MLS.Serialisation +import Wire.API.MLS.Welcome tests :: T.TestTree tests = @@ -38,7 +40,11 @@ tests = testRoundTrip @RemoveProposalMessage, testRoundTrip @RemoveProposalPayload, testRoundTrip @AppAckProposalTest, - testRoundTrip @ExtensionVector + testRoundTrip @ExtensionVector, + testRoundTrip @PublicGroupStateTBS, + testRoundTrip @PublicGroupState, + testRoundTrip @Welcome, + testRoundTrip @OpaquePublicGroupState ] testRoundTrip :: diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 622041a252..ac49c0a5a4 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -41,15 +41,18 @@ library Wire.API.Message.Proto Wire.API.MLS.CipherSuite Wire.API.MLS.Commit + Wire.API.MLS.CommitBundle Wire.API.MLS.Context Wire.API.MLS.Credential Wire.API.MLS.Epoch Wire.API.MLS.Extension Wire.API.MLS.Group + Wire.API.MLS.GroupInfoBundle Wire.API.MLS.KeyPackage Wire.API.MLS.Keys Wire.API.MLS.Message Wire.API.MLS.Proposal + Wire.API.MLS.PublicGroupState Wire.API.MLS.Serialisation Wire.API.MLS.Servant Wire.API.MLS.Welcome @@ -566,6 +569,7 @@ test-suite wire-api-golden-tests , containers >=0.5 , currency-codes , directory + , either , filepath , hscim , imports diff --git a/libs/zauth/main/Main.hs b/libs/zauth/main/Main.hs index e09be731bf..5f486acc79 100644 --- a/libs/zauth/main/Main.hs +++ b/libs/zauth/main/Main.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} @@ -180,7 +181,7 @@ options = <> metavar "STRING" <> help "token data" toMode = - readerAsk >>= \s -> case s of + readerAsk >>= \case "create-user" -> pure CreateUser "create-session" -> pure CreateSession "create-access" -> pure CreateAccess diff --git a/nix/default.nix b/nix/default.nix index ce8cecf1cc..8ad4517f70 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -84,7 +84,8 @@ let pkgs.gnused pkgs.helm pkgs.helmfile - pkgs.hlint + pkgs.haskellPackages.hlint_3_4_1 + pkgs.haskellPackages.apply-refact pkgs.jq pkgs.kind pkgs.kubectl diff --git a/nix/pkgs/mls_test_cli/default.nix b/nix/pkgs/mls_test_cli/default.nix index 4a9c64213d..a9a9657d73 100644 --- a/nix/pkgs/mls_test_cli/default.nix +++ b/nix/pkgs/mls_test_cli/default.nix @@ -9,17 +9,17 @@ rustPlatform.buildRustPackage rec { name = "mls-test-cli-${version}"; - version = "0.4.0"; + version = "0.6.0"; nativeBuildInputs = [ pkg-config perl ]; buildInputs = [ libsodium ]; src = fetchFromGitHub { owner = "wireapp"; repo = "mls-test-cli"; - sha256 = "sha256-6G01eONZb/61MrO/Py+ix7Psz+jl+3Cn7xUMez3osxw="; - rev = "d01258a290546a01a62dca21ba3d0e3863a288b4"; + sha256 = "sha256-xYL9KNcirCARb1Rp41einOpq0ut5adlqMIAEiwYXkzg="; + rev = "d46624fb49c900facc8853fa86e3ecf51fd0dcdb"; }; doCheck = false; - cargoSha256 = "sha256-frzVXP0lxXhPhfNL4zleHj2WSMwmQfCdTqkTbHXBFEI="; + cargoSha256 = "sha256-FGFyS/tLlD+3JQX7vkKq4nW+WQI1FFnpugzfFBi/eQE="; cargoDepsHook = '' mkdir -p mls-test-cli-${version}-vendor.tar.gz/ring/.git ''; diff --git a/nix/pkgs/zauth/default.nix b/nix/pkgs/zauth/default.nix index 1f256b7c9e..5ae2d991a6 100644 --- a/nix/pkgs/zauth/default.nix +++ b/nix/pkgs/zauth/default.nix @@ -15,7 +15,7 @@ rustPlatform.buildRustPackage rec { src = nix-gitignore.gitignoreSourcePure [ ../../../.gitignore ] ../../../libs/libzauth; sourceRoot = "libzauth/libzauth-c"; - cargoSha256 = "0p81bjbwchq8v0ybvx8r1xcxsah7fjdq2fc2dy4l4k2v18hi9z91"; + cargoSha256 = "sha256-od+O5dhAVC1KhDUz8U2fhjyqjXkqHjeEEhvVE0N9orI="; patchLibs = lib.optionalString stdenv.isDarwin '' install_name_tool -id $out/lib/libzauth.dylib $out/lib/libzauth.dylib diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 661b91679e..95f744d763 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -58,8 +58,14 @@ library Brig.Effects.BlacklistPhonePrefixStore.Cassandra Brig.Effects.BlacklistStore Brig.Effects.BlacklistStore.Cassandra + Brig.Effects.CodeStore + Brig.Effects.CodeStore.Cassandra Brig.Effects.Delay + Brig.Effects.PasswordResetStore + Brig.Effects.PasswordResetStore.CodeStore Brig.Effects.SFT + Brig.Effects.UserPendingActivationStore + Brig.Effects.UserPendingActivationStore.Cassandra Brig.Email Brig.Federation.Client Brig.Index.Eval @@ -85,17 +91,12 @@ library Brig.Queue.Types Brig.RPC Brig.Run - Brig.Sem.CodeStore - Brig.Sem.CodeStore.Cassandra - Brig.Sem.PasswordResetStore - Brig.Sem.PasswordResetStore.CodeStore - Brig.Sem.UserPendingActivationStore - Brig.Sem.UserPendingActivationStore.Cassandra Brig.SMTP Brig.Team.API Brig.Team.DB Brig.Team.Email Brig.Team.Template + Brig.Team.Types Brig.Team.Util Brig.Template Brig.Unique @@ -204,6 +205,7 @@ library , errors >=1.4 , exceptions >=0.5 , extended + , extra , file-embed , file-embed-lzma , filepath >=1.3 diff --git a/services/brig/src/Brig/API.hs b/services/brig/src/Brig/API.hs index d724135c50..9b7ce571d2 100644 --- a/services/brig/src/Brig/API.hs +++ b/services/brig/src/Brig/API.hs @@ -25,9 +25,9 @@ import qualified Brig.API.Internal as Internal import qualified Brig.API.Public as Public import Brig.Effects.BlacklistPhonePrefixStore (BlacklistPhonePrefixStore) import Brig.Effects.BlacklistStore (BlacklistStore) -import Brig.Sem.CodeStore -import Brig.Sem.PasswordResetStore (PasswordResetStore) -import Brig.Sem.UserPendingActivationStore (UserPendingActivationStore) +import Brig.Effects.CodeStore +import Brig.Effects.PasswordResetStore (PasswordResetStore) +import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import qualified Data.Swagger.Build.Api as Doc import Network.Wai.Routing (Routes) import Polysemy diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 691bc36060..a705eda0ae 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -56,7 +56,9 @@ import Brig.Federation.Client (getUserClients) import qualified Brig.Federation.Client as Federation import Brig.IO.Intra (guardLegalhold) import qualified Brig.IO.Intra as Intra +import qualified Brig.InternalEvent.Types as Internal import qualified Brig.Options as Opt +import qualified Brig.Queue as Queue import Brig.Types.Intra import Brig.Types.Team.LegalHold (LegalHoldClientRequest (..)) import Brig.Types.User.Event @@ -166,8 +168,9 @@ addClientWithReAuthPolicy policy u con ip new = do let usr = accountUser acc lift $ do for_ old $ execDelete u con - wrapHttp $ Intra.newClient u (clientId clt) - Intra.onClientEvent u con (ClientAdded u clt) + wrapHttp $ do + Intra.newClient u (clientId clt) + Intra.onClientEvent u con (ClientAdded u clt) when (clientType clt == LegalHoldClientType) $ wrapHttpClient $ Intra.onUserEvent u con (UserLegalHoldEnabled u) when (count > 1) $ for_ (userEmail usr) $ @@ -373,13 +376,12 @@ claimLocalMultiPrekeyBundles protectee userClients = do -- Utilities --- | Perform an orderly deletion of an existing client. +-- | Enqueue an orderly deletion of an existing client. execDelete :: UserId -> Maybe ConnId -> Client -> (AppT r) () execDelete u con c = do - wrapHttp $ Intra.rmClient u (clientId c) for_ (clientCookie c) $ \l -> wrapClient $ Auth.revokeCookies u [] [l] - Intra.onClientEvent u con (ClientRemoved u c) - wrapClient $ Data.rmClient u (clientId c) + queue <- view internalEvents + Queue.enqueue queue (Internal.DeleteClient (clientId c) u con) -- | Defensive measure when no prekey is found for a -- requested client: Ensure that the client does indeed diff --git a/services/brig/src/Brig/API/Connection.hs b/services/brig/src/Brig/API/Connection.hs index c455e8c1de..4a222ec790 100644 --- a/services/brig/src/Brig/API/Connection.hs +++ b/services/brig/src/Brig/API/Connection.hs @@ -435,9 +435,7 @@ updateConnectionInternal = \case handleConns (resultList page) case resultList page of (conn : rest) -> - if resultHasMore page - then go (Just (maximum (qUnqualified . ucTo <$> (conn : rest)))) - else pure () + when (resultHasMore page) $ go (Just (maximum (qUnqualified . ucTo <$> (conn : rest)))) [] -> pure () unblockDirected :: UserConnection -> UserConnection -> ExceptT ConnectionError (AppT r) () diff --git a/services/brig/src/Brig/API/Error.hs b/services/brig/src/Brig/API/Error.hs index c3e8341498..acab9f94a7 100644 --- a/services/brig/src/Brig/API/Error.hs +++ b/services/brig/src/Brig/API/Error.hs @@ -83,15 +83,15 @@ actError (InvalidActivationEmail _ _) = StdError (errorToWai @'E.InvalidEmail) actError (InvalidActivationPhone _) = StdError (errorToWai @'E.InvalidPhone) pwResetError :: PasswordResetError -> Error -pwResetError InvalidPasswordResetKey = StdError invalidPwResetKey -pwResetError InvalidPasswordResetCode = StdError invalidPwResetCode -pwResetError (PasswordResetInProgress Nothing) = StdError duplicatePwResetCode +pwResetError InvalidPasswordResetKey = StdError (errorToWai @'E.InvalidPasswordResetKey) +pwResetError InvalidPasswordResetCode = StdError (errorToWai @'E.InvalidPasswordResetCode) +pwResetError (PasswordResetInProgress Nothing) = StdError (errorToWai @'E.PasswordResetInProgress) pwResetError (PasswordResetInProgress (Just t)) = RichError - duplicatePwResetCode + (errorToWai @'E.PasswordResetInProgress) () [("Retry-After", toByteString' t)] -pwResetError ResetPasswordMustDiffer = StdError resetPasswordMustDiffer +pwResetError ResetPasswordMustDiffer = StdError (errorToWai @'E.ResetPasswordMustDiffer) sendLoginCodeError :: SendLoginCodeError -> Error sendLoginCodeError (SendLoginInvalidPhone _) = StdError (errorToWai @'E.InvalidPhone) @@ -182,6 +182,8 @@ clientDataError (ClientReAuthError e) = reauthError e clientDataError ClientMissingAuth = StdError (errorToWai @'E.MissingAuth) clientDataError MalformedPrekeys = StdError (errorToWai @'E.MalformedPrekeys) clientDataError MLSPublicKeyDuplicate = StdError (errorToWai @'E.MLSDuplicatePublicKey) +clientDataError KeyPackageDecodingError = StdError (errorToWai @'E.KeyPackageDecodingError) +clientDataError InvalidKeyPackageRef = StdError (errorToWai @'E.InvalidKeyPackageRef) deleteUserError :: DeleteUserError -> Error deleteUserError DeleteUserInvalid = StdError (errorToWai @'E.InvalidUser) @@ -233,18 +235,6 @@ clientCapabilitiesCannotBeRemoved = Wai.mkError status409 "client-capabilities-c noEmail :: Wai.Error noEmail = Wai.mkError status403 "no-email" "This operation requires the user to have a verified email address." -invalidPwResetKey :: Wai.Error -invalidPwResetKey = Wai.mkError status400 "invalid-key" "Invalid email or mobile number for password reset." - -resetPasswordMustDiffer :: Wai.Error -resetPasswordMustDiffer = Wai.mkError status409 "password-must-differ" "For password reset, new and old password must be different." - -invalidPwResetCode :: Wai.Error -invalidPwResetCode = Wai.mkError status400 "invalid-code" "Invalid password reset code." - -duplicatePwResetCode :: Wai.Error -duplicatePwResetCode = Wai.mkError status409 "code-exists" "A password reset is already in progress." - emailExists :: Wai.Error emailExists = Wai.mkError status409 "email-exists" "The given e-mail address is in use." diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 83bf3e28ce..a69ebeac66 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -28,6 +28,7 @@ import qualified Brig.API.Client as API import qualified Brig.API.Connection as API import Brig.API.Error import Brig.API.Handler +import Brig.API.MLS.KeyPackages.Validation import Brig.API.Types import qualified Brig.API.User as API import qualified Brig.API.User as Api @@ -41,14 +42,15 @@ import qualified Brig.Data.MLS.KeyPackage as Data import qualified Brig.Data.User as Data import Brig.Effects.BlacklistPhonePrefixStore (BlacklistPhonePrefixStore) import Brig.Effects.BlacklistStore (BlacklistStore) +import Brig.Effects.CodeStore (CodeStore) +import Brig.Effects.PasswordResetStore (PasswordResetStore) +import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import qualified Brig.IO.Intra as Intra import Brig.Options hiding (internalEvents, sesQueue) import qualified Brig.Provider.API as Provider -import Brig.Sem.CodeStore (CodeStore) -import Brig.Sem.PasswordResetStore (PasswordResetStore) -import Brig.Sem.UserPendingActivationStore (UserPendingActivationStore) import qualified Brig.Team.API as Team import Brig.Team.DB (lookupInvitationByEmail) +import Brig.Team.Types (ShowOrHideInvitationUrl (..)) import Brig.Types.Connection import Brig.Types.Intra import Brig.Types.Team.LegalHold (LegalHoldClientRequest (..)) @@ -86,7 +88,8 @@ import Wire.API.Error import qualified Wire.API.Error.Brig as E import Wire.API.MLS.Credential import Wire.API.MLS.KeyPackage -import Wire.API.Routes.Internal.Brig (NewKeyPackageRef) +import Wire.API.MLS.Serialisation +import Wire.API.Routes.Internal.Brig import qualified Wire.API.Routes.Internal.Brig as BrigIRoutes import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Named @@ -130,6 +133,7 @@ mlsAPI = ) :<|> getMLSClients :<|> mapKeyPackageRefsInternal + :<|> Named @"put-key-package-add" upsertKeyPackage accountAPI :: Members @@ -184,6 +188,39 @@ getConvIdByKeyPackageRef = runMaybeT . mapMaybeT wrapClientE . Data.keyPackageRe postKeyPackageRef :: KeyPackageRef -> KeyPackageRef -> Handler r () postKeyPackageRef ref = lift . wrapClient . Data.updateKeyPackageRef ref +-- Used by galley to update key package refs and also validate +upsertKeyPackage :: NewKeyPackage -> Handler r NewKeyPackageResult +upsertKeyPackage nkp = do + kp <- + either + (const $ mlsProtocolError "upsertKeyPackage: Cannot decocode KeyPackage") + pure + $ decodeMLS' @(RawMLS KeyPackage) (kpData . nkpKeyPackage $ nkp) + ref <- kpRef' kp & noteH "upsertKeyPackage: Unsupported CipherSuite" + + identity <- + either + (const $ mlsProtocolError "upsertKeyPackage: Cannot decode ClientIdentity") + pure + $ kpIdentity (rmValue kp) + mp <- lift . wrapClient . runMaybeT $ Data.derefKeyPackage ref + when (isNothing mp) $ do + void $ validateKeyPackage identity kp + lift . wrapClient $ + Data.addKeyPackageRef + ref + ( NewKeyPackageRef + (fst <$> cidQualifiedClient identity) + (ciClient identity) + (nkpConversation nkp) + ) + + pure $ NewKeyPackageResult identity ref + where + noteH :: Text -> Maybe a -> Handler r a + noteH errMsg Nothing = mlsProtocolError errMsg + noteH _ (Just y) = pure y + getMLSClients :: UserId -> SignatureSchemeTag -> Handler r (Set ClientInfo) getMLSClients usr _ss = do -- FUTUREWORK: check existence of key packages with a given ciphersuite @@ -198,8 +235,8 @@ getMLSClients usr _ss = do | otherwise = getResult rs getValidity lusr cid = - fmap ((cid,) . (> 0)) $ - Data.countKeyPackages lusr cid + (cid,) . (> 0) + <$> Data.countKeyPackages lusr cid mapKeyPackageRefsInternal :: KeyPackageBundle -> Handler r () mapKeyPackageRefsInternal bundle = do @@ -250,7 +287,7 @@ sitemap = do -- This endpoint will lead to the following events being sent: -- - UserDeleted event to all of its contacts -- - MemberLeave event to members for all conversations the user was in (via galley) - delete "/i/users/:uid" (continue deleteUserNoVerifyH) $ + delete "/i/users/:uid" (continue deleteUserNoAuthH) $ capture "uid" put "/i/connections/connection-update" (continue updateConnectionInternalH) $ @@ -472,16 +509,13 @@ createUserNoVerifySpar uData = in API.activate key code (Just uid) !>> CreateUserSparRegistrationError . activationErrorToRegisterError pure . SelfProfile $ usr -deleteUserNoVerifyH :: UserId -> (Handler r) Response -deleteUserNoVerifyH uid = do - setStatus status202 empty <$ deleteUserNoVerify uid - -deleteUserNoVerify :: UserId -> (Handler r) () -deleteUserNoVerify uid = do - void $ - lift (wrapClient $ API.lookupAccount uid) - >>= ifNothing (errorToWai @'E.UserNotFound) - lift $ API.deleteUserNoVerify uid +deleteUserNoAuthH :: UserId -> (Handler r) Response +deleteUserNoAuthH uid = do + r <- lift $ wrapHttp $ API.ensureAccountDeleted uid + case r of + NoUser -> throwStd (errorToWai @'E.UserNotFound) + AccountAlreadyDeleted -> pure $ setStatus ok200 empty + AccountDeleted -> pure $ setStatus accepted202 empty changeSelfEmailMaybeSendH :: Member BlacklistStore r => UserId ::: Bool ::: JsonRequest EmailUpdate -> (Handler r) Response changeSelfEmailMaybeSendH (u ::: validate ::: req) = do @@ -523,7 +557,7 @@ listActivatedAccounts elh includePendingInvitations = do case (accountStatus account, includePendingInvitations, emailIdentity ident) of (PendingInvitation, False, _) -> pure False (PendingInvitation, True, Just email) -> do - hasInvitation <- isJust <$> wrapClient (lookupInvitationByEmail email) + hasInvitation <- isJust <$> wrapClient (lookupInvitationByEmail HideInvitationUrl email) unless hasInvitation $ do -- user invited via scim should expire together with its invitation API.deleteUserNoVerify (userId . accountUser $ account) @@ -560,7 +594,7 @@ getPasswordResetCodeH :: JSON ::: Either Email Phone -> (Handler r) Response getPasswordResetCodeH (_ ::: emailOrPhone) = do - maybe (throwStd invalidPwResetKey) (pure . json) =<< lift (getPasswordResetCode emailOrPhone) + maybe (throwStd (errorToWai @'E.InvalidPasswordResetKey)) (pure . json) =<< lift (getPasswordResetCode emailOrPhone) getPasswordResetCode :: Members '[CodeStore, PasswordResetStore] r => @@ -760,8 +794,3 @@ getContactListH :: JSON ::: UserId -> (Handler r) Response getContactListH (_ ::: uid) = do contacts <- lift . wrapClient $ API.lookupContactList uid pure $ json $ UserIds contacts - --- Utilities - -ifNothing :: Utilities.Error -> Maybe a -> (Handler r) a -ifNothing e = maybe (throwStd e) pure diff --git a/services/brig/src/Brig/API/MLS/KeyPackages.hs b/services/brig/src/Brig/API/MLS/KeyPackages.hs index 342eafd953..49cd60de86 100644 --- a/services/brig/src/Brig/API/MLS/KeyPackages.hs +++ b/services/brig/src/Brig/API/MLS/KeyPackages.hs @@ -42,6 +42,7 @@ import Wire.API.Federation.API import Wire.API.Federation.API.Brig import Wire.API.MLS.Credential import Wire.API.MLS.KeyPackage +import Wire.API.MLS.Serialisation import Wire.API.Team.LegalHold import Wire.API.User.Client @@ -57,12 +58,11 @@ claimKeyPackages :: Maybe ClientId -> Handler r KeyPackageBundle claimKeyPackages lusr target skipOwn = - withExceptT clientError $ - foldQualified - lusr - (claimLocalKeyPackages (qUntagged lusr) skipOwn) - (claimRemoteKeyPackages lusr) - target + foldQualified + lusr + (withExceptT clientError . claimLocalKeyPackages (qUntagged lusr) skipOwn) + (claimRemoteKeyPackages lusr) + target claimLocalKeyPackages :: Qualified UserId -> @@ -96,11 +96,12 @@ claimLocalKeyPackages qusr skipOwn target = do claimRemoteKeyPackages :: Local UserId -> Remote UserId -> - ExceptT ClientError (AppT r) KeyPackageBundle + Handler r KeyPackageBundle claimRemoteKeyPackages lusr target = do bundle <- - (handleFailure =<<) $ - withExceptT ClientFederationError $ + withExceptT clientError + . (handleFailure =<<) + $ withExceptT ClientFederationError $ runBrigFederatorClient (tDomain target) $ fedClient @'Brig @"claim-key-packages" $ ClaimKeyPackageRequest @@ -108,10 +109,22 @@ claimRemoteKeyPackages lusr target = do ckprTarget = tUnqualified target } - -- set up mappings for all claimed key packages - wrapClientE $ - for_ (kpbEntries bundle) $ \e -> - Data.mapKeyPackageRef (kpbeRef e) (kpbeUser e) (kpbeClient e) + -- validate and set up mappings for all claimed key packages + for_ (kpbEntries bundle) $ \e -> do + let cid = mkClientIdentity (kpbeUser e) (kpbeClient e) + kpRaw <- + withExceptT (const . clientDataError $ KeyPackageDecodingError) + . except + . decodeMLS' + . kpData + . kpbeKeyPackage + $ e + (refVal, _) <- validateKeyPackage cid kpRaw + unless (refVal == kpbeRef e) + . throwE + . clientDataError + $ InvalidKeyPackageRef + wrapClientE $ Data.mapKeyPackageRef (kpbeRef e) (kpbeUser e) (kpbeClient e) pure bundle where diff --git a/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs b/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs index 0c5408e455..186328a7f1 100644 --- a/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs +++ b/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs @@ -19,6 +19,7 @@ module Brig.API.MLS.KeyPackages.Validation ( -- * Main key package validation function validateKeyPackage, reLifetime, + mlsProtocolError, -- * Exported for unit tests findExtensions, @@ -34,6 +35,7 @@ import Brig.Options import Control.Applicative import Control.Lens (view) import qualified Data.ByteString.Lazy as LBS +import Data.Qualified import Data.Time.Clock import Data.Time.Clock.POSIX import Imports @@ -45,8 +47,12 @@ import Wire.API.MLS.Extension import Wire.API.MLS.KeyPackage import Wire.API.MLS.Serialisation -validateKeyPackage :: ClientIdentity -> RawMLS KeyPackage -> Handler r (KeyPackageRef, KeyPackageData) +validateKeyPackage :: + ClientIdentity -> + RawMLS KeyPackage -> + Handler r (KeyPackageRef, KeyPackageData) validateKeyPackage identity (RawMLS (KeyPackageData -> kpd) kp) = do + loc <- qualifyLocal () -- get ciphersuite cs <- maybe @@ -59,19 +65,32 @@ validateKeyPackage identity (RawMLS (KeyPackageData -> kpd) kp) = do when (signatureScheme ss /= bcSignatureScheme (kpCredential kp)) $ mlsProtocolError "Signature scheme incompatible with ciphersuite" - -- authenticate signature key - key <- - fmap LBS.toStrict $ - maybe - (mlsProtocolError "No key associated to the given identity and signature scheme") - pure - =<< lift (wrapClient (Data.lookupMLSPublicKey (ciUser identity) (ciClient identity) ss)) - when (key /= bcSignatureKey (kpCredential kp)) $ - mlsProtocolError "Unrecognised signature key" + -- Authenticate signature key. This is performed only upon uploading a key + -- package for a local client. + foldQualified + loc + ( \_ -> do + key <- + fmap LBS.toStrict $ + maybe + (mlsProtocolError "No key associated to the given identity and signature scheme") + pure + =<< lift (wrapClient (Data.lookupMLSPublicKey (ciUser identity) (ciClient identity) ss)) + when (key /= bcSignatureKey (kpCredential kp)) $ + mlsProtocolError "Unrecognised signature key" + ) + (pure . const ()) + (cidQualifiedClient identity) -- validate signature - unless (csVerifySignature cs key (rmRaw (kpTBS kp)) (kpSignature kp)) $ - mlsProtocolError "Invalid signature" + unless + ( csVerifySignature + cs + (bcSignatureKey (kpCredential kp)) + (rmRaw (kpTBS kp)) + (kpSignature kp) + ) + $ mlsProtocolError "Invalid signature" -- validate protocol version maybe (mlsProtocolError "Unsupported protocol version") diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index fa51d1b925..b6eb96598c 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -46,12 +46,12 @@ import qualified Brig.Data.User as Data import qualified Brig.Data.UserKey as UserKey import Brig.Effects.BlacklistPhonePrefixStore (BlacklistPhonePrefixStore) import Brig.Effects.BlacklistStore (BlacklistStore) +import Brig.Effects.CodeStore (CodeStore) +import Brig.Effects.PasswordResetStore (PasswordResetStore) +import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import qualified Brig.IO.Intra as Intra import Brig.Options hiding (internalEvents, sesQueue) import qualified Brig.Provider.API as Provider -import Brig.Sem.CodeStore (CodeStore) -import Brig.Sem.PasswordResetStore (PasswordResetStore) -import Brig.Sem.UserPendingActivationStore (UserPendingActivationStore) import qualified Brig.Team.API as Team import qualified Brig.Team.Email as Team import Brig.Types.Activation (ActivationPair) @@ -95,14 +95,10 @@ import qualified Data.ZAuth.Token as ZAuth import FileEmbedLzma import Galley.Types.Teams (HiddenPerm (..), hasPermission) import Imports hiding (head) -import Network.HTTP.Types.Status -import Network.Wai import Network.Wai.Predicate hiding (result, setStatus) import Network.Wai.Routing import Network.Wai.Utilities as Utilities -import Network.Wai.Utilities.Swagger (document, mkSwaggerApi) -import qualified Network.Wai.Utilities.Swagger as Doc -import Network.Wai.Utilities.ZAuth (zauthUserId) +import Network.Wai.Utilities.Swagger (mkSwaggerApi) import Polysemy import Servant hiding (Handler, JSON, addHeader, respond) import qualified Servant @@ -190,7 +186,9 @@ servantSitemap :: Members '[ BlacklistStore, BlacklistPhonePrefixStore, - UserPendingActivationStore p + UserPendingActivationStore p, + PasswordResetStore, + CodeStore ] r => ServerT BrigAPI (Handler r) @@ -222,7 +220,16 @@ servantSitemap = userAPI :<|> selfAPI :<|> accountAPI :<|> clientAPI :<|> prekey :<|> Named @"change-handle" changeHandle accountAPI :: ServerT AccountAPI (Handler r) - accountAPI = Named @"register" createUser + accountAPI = + Named @"register" createUser + :<|> Named @"verify-delete" verifyDeleteUser + :<|> Named @"get-activate" activate + :<|> Named @"post-activate" activateKey + :<|> Named @"post-activate-send" sendActivationCode + :<|> Named @"post-password-reset" beginPasswordReset + :<|> Named @"post-password-reset-complete" completePasswordReset + :<|> Named @"post-password-reset-key-deprecated" deprecatedCompletePasswordReset + :<|> Named @"onboarding" deprecatedOnboarding clientAPI :: ServerT ClientAPI (Handler r) clientAPI = @@ -309,120 +316,6 @@ sitemap :: r => Routes Doc.ApiBuilder (Handler r) () sitemap = do - -- This endpoint can lead to the following events being sent: - -- UserDeleted event to contacts of deleted user - -- MemberLeave event to members for all conversations the user was in (via galley) - post "/delete" (continue verifyDeleteUserH) $ - jsonRequest @Public.VerifyDeleteUser - .&. accept "application" "json" - document "POST" "verifyDeleteUser" $ do - Doc.summary "Verify account deletion with a code." - Doc.body (Doc.ref Public.modelVerifyDelete) $ - Doc.description "JSON body" - Doc.response 200 "Deletion is initiated." Doc.end - Doc.errorResponse (errorToWai @'E.InvalidCode) - - -- TODO: put delete here, too? - -- /activate, /password-reset ---------------------------------- - - -- This endpoint can lead to the following events being sent: - -- - UserActivated event to the user, if account gets activated - -- - UserIdentityUpdated event to the user, if email or phone get activated - get "/activate" (continue activateH) $ - query "key" - .&. query "code" - document "GET" "activate" $ do - Doc.summary "Activate (i.e. confirm) an email address or phone number." - Doc.notes "See also 'POST /activate' which has a larger feature set." - Doc.parameter Doc.Query "key" Doc.bytes' $ - Doc.description "Activation key" - Doc.parameter Doc.Query "code" Doc.bytes' $ - Doc.description "Activation code" - Doc.returns (Doc.ref Public.modelActivationResponse) - Doc.response 200 "Activation successful." Doc.end - Doc.response 204 "A recent activation was already successful." Doc.end - Doc.errorResponse activationCodeNotFound - - -- docs/reference/user/activation.md {#RefActivationSubmit} - -- - -- This endpoint can lead to the following events being sent: - -- - UserActivated event to the user, if account gets activated - -- - UserIdentityUpdated event to the user, if email or phone get activated - post "/activate" (continue activateKeyH) $ - accept "application" "json" - .&. jsonRequest @Public.Activate - document "POST" "activate" $ do - Doc.summary "Activate (i.e. confirm) an email address or phone number." - Doc.notes - "Activation only succeeds once and the number of \ - \failed attempts for a valid key is limited." - Doc.body (Doc.ref Public.modelActivate) $ - Doc.description "JSON body" - Doc.returns (Doc.ref Public.modelActivationResponse) - Doc.response 200 "Activation successful." Doc.end - Doc.response 204 "A recent activation was already successful." Doc.end - Doc.errorResponse activationCodeNotFound - - -- docs/reference/user/activation.md {#RefActivationRequest} - post "/activate/send" (continue sendActivationCodeH) $ - jsonRequest @Public.SendActivationCode - document "POST" "sendActivationCode" $ do - Doc.summary "Send (or resend) an email or phone activation code." - Doc.body (Doc.ref Public.modelSendActivationCode) $ - Doc.description "JSON body" - Doc.response 200 "Activation code sent." Doc.end - Doc.errorResponse (errorToWai @'E.InvalidEmail) - Doc.errorResponse (errorToWai @'E.InvalidPhone) - Doc.errorResponse (errorToWai @'E.UserKeyExists) - Doc.errorResponse blacklistedEmail - Doc.errorResponse (errorToWai @'E.BlacklistedPhone) - Doc.errorResponse (customerExtensionBlockedDomain (either undefined id $ mkDomain "example.com")) - - post "/password-reset" (continue beginPasswordResetH) $ - accept "application" "json" - .&. jsonRequest @Public.NewPasswordReset - document "POST" "beginPasswordReset" $ do - Doc.summary "Initiate a password reset." - Doc.body (Doc.ref Public.modelNewPasswordReset) $ - Doc.description "JSON body" - Doc.response 201 "Password reset code created and sent by email." Doc.end - Doc.errorResponse invalidPwResetKey - Doc.errorResponse duplicatePwResetCode - - post "/password-reset/complete" (continue completePasswordResetH) $ - accept "application" "json" - .&. jsonRequest @Public.CompletePasswordReset - document "POST" "completePasswordReset" $ do - Doc.summary "Complete a password reset." - Doc.body (Doc.ref Public.modelCompletePasswordReset) $ - Doc.description "JSON body" - Doc.response 200 "Password reset successful." Doc.end - Doc.errorResponse invalidPwResetCode - - post "/password-reset/:key" (continue deprecatedCompletePasswordResetH) $ - accept "application" "json" - .&. capture "key" - .&. jsonRequest @Public.PasswordReset - document "POST" "deprecatedCompletePasswordReset" $ do - Doc.deprecated - Doc.summary "Complete a password reset." - Doc.notes "DEPRECATED: Use 'POST /password-reset/complete'." - - -- This endpoint is used to test /i/metrics, when this is servantified, please - -- make sure some other endpoint is used to test that routes defined in this - -- function are recorded and reported correctly in /i/metrics. - -- see test/integration/API/Metrics.hs - post "/onboarding/v3" (continue deprecatedOnboardingH) $ - accept "application" "json" - .&. zauthUserId - .&. jsonRequest @Value - document "POST" "onboardingV3" $ do - Doc.deprecated - Doc.summary "Upload contacts and invoke matching." - Doc.notes - "DEPRECATED: the feature has been turned off, the end-point does \ - \nothing and always returns '{\"results\":[],\"auto-connects\":[]}'." - Provider.routesPublic Auth.routesPublic Team.routesPublic @@ -832,13 +725,6 @@ changeHandle u conn (Public.HandleUpdate h) = lift . exceptTToMaybe $ do handle <- maybe (throwError Public.ChangeHandleInvalid) pure $ parseHandle h API.changeHandle u (Just conn) handle API.ForbidSCIMUpdates -beginPasswordResetH :: - Members '[PasswordResetStore] r => - JSON ::: JsonRequest Public.NewPasswordReset -> - (Handler r) Response -beginPasswordResetH (_ ::: req) = - setStatus status201 empty <$ (beginPasswordReset =<< parseJsonBody req) - beginPasswordReset :: Members '[PasswordResetStore] r => Public.NewPasswordReset -> @@ -851,25 +737,12 @@ beginPasswordReset (Public.NewPasswordReset target) = do Left email -> sendPasswordResetMail email pair loc Right phone -> wrapClient $ sendPasswordResetSms phone pair loc -completePasswordResetH :: +completePasswordReset :: Members '[CodeStore, PasswordResetStore] r => - JSON ::: JsonRequest Public.CompletePasswordReset -> - (Handler r) Response -completePasswordResetH (_ ::: req) = do - Public.CompletePasswordReset {..} <- parseJsonBody req - API.completePasswordReset cpwrIdent cpwrCode cpwrPassword !>> pwResetError - pure empty - -sendActivationCodeH :: - Members - '[ BlacklistStore, - BlacklistPhonePrefixStore - ] - r => - JsonRequest Public.SendActivationCode -> - (Handler r) Response -sendActivationCodeH req = - empty <$ (sendActivationCode =<< parseJsonBody req) + Public.CompletePasswordReset -> + (Handler r) () +completePasswordReset req = do + API.completePasswordReset (Public.cpwrIdent req) (Public.cpwrCode req) (Public.cpwrPassword req) !>> pwResetError -- docs/reference/user/activation.md {#RefActivationRequest} -- docs/reference/user/registration.md {#RefRegistration} @@ -989,11 +862,8 @@ deleteSelfUser :: deleteSelfUser u body = API.deleteSelfUser u (Public.deleteUserPassword body) !>> deleteUserError -verifyDeleteUserH :: JsonRequest Public.VerifyDeleteUser ::: JSON -> (Handler r) Response -verifyDeleteUserH (r ::: _) = do - body <- parseJsonBody r - API.verifyDeleteUser body !>> deleteUserError - pure (setStatus status200 empty) +verifyDeleteUser :: Public.VerifyDeleteUser -> Handler r () +verifyDeleteUser body = API.verifyDeleteUser body !>> deleteUserError updateUserEmail :: Member BlacklistStore r => UserId -> UserId -> Public.EmailUpdate -> (Handler r) () updateUserEmail zuserId emailOwnerId (Public.EmailUpdate email) = do @@ -1018,32 +888,14 @@ updateUserEmail zuserId emailOwnerId (Public.EmailUpdate email) = do -- activation -data ActivationRespWithStatus - = ActivationResp Public.ActivationResponse - | ActivationRespDryRun - | ActivationRespPass - | ActivationRespSuccessNoIdent - -respFromActivationRespWithStatus :: ActivationRespWithStatus -> Response -respFromActivationRespWithStatus = \case - ActivationResp aresp -> json aresp - ActivationRespDryRun -> empty - ActivationRespPass -> setStatus status204 empty - ActivationRespSuccessNoIdent -> empty - --- docs/reference/user/activation.md {#RefActivationSubmit} -activateKeyH :: JSON ::: JsonRequest Public.Activate -> (Handler r) Response -activateKeyH (_ ::: req) = do - activationRequest <- parseJsonBody req - respFromActivationRespWithStatus <$> activate activationRequest - -activateH :: Public.ActivationKey ::: Public.ActivationCode -> (Handler r) Response -activateH (k ::: c) = do +activate :: Public.ActivationKey -> Public.ActivationCode -> (Handler r) ActivationRespWithStatus +activate k c = do let activationRequest = Public.Activate (Public.ActivateKey k) c False - respFromActivationRespWithStatus <$> activate activationRequest + activateKey activationRequest -activate :: Public.Activate -> (Handler r) ActivationRespWithStatus -activate (Public.Activate tgt code dryrun) +-- docs/reference/user/activation.md {#RefActivationSubmit} +activateKey :: Public.Activate -> (Handler r) ActivationRespWithStatus +activateKey (Public.Activate tgt code dryrun) | dryrun = do wrapClientE (API.preverify tgt code) !>> actError pure ActivationRespDryRun @@ -1096,30 +948,20 @@ sendVerificationCode req = do -- Deprecated -deprecatedOnboardingH :: JSON ::: UserId ::: JsonRequest Value -> (Handler r) Response -deprecatedOnboardingH (_ ::: _ ::: _) = pure $ json DeprecatedMatchingResult - -data DeprecatedMatchingResult = DeprecatedMatchingResult +deprecatedOnboarding :: UserId -> JsonValue -> (Handler r) DeprecatedMatchingResult +deprecatedOnboarding _ _ = pure DeprecatedMatchingResult -instance ToJSON DeprecatedMatchingResult where - toJSON DeprecatedMatchingResult = - object - [ "results" .= ([] :: [()]), - "auto-connects" .= ([] :: [()]) - ] - -deprecatedCompletePasswordResetH :: +deprecatedCompletePasswordReset :: Members '[CodeStore, PasswordResetStore] r => - JSON ::: Public.PasswordResetKey ::: JsonRequest Public.PasswordReset -> - (Handler r) Response -deprecatedCompletePasswordResetH (_ ::: k ::: req) = do - pwr <- parseJsonBody req + Public.PasswordResetKey -> + Public.PasswordReset -> + (Handler r) () +deprecatedCompletePasswordReset k pwr = do API.completePasswordReset (Public.PasswordResetIdentityKey k) (Public.pwrCode pwr) (Public.pwrPassword pwr) !>> pwResetError - pure empty -- Utilities diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index afd82173ec..ea3932461f 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -54,6 +54,7 @@ module Brig.API.User deleteUsersNoVerify, deleteSelfUser, verifyDeleteUser, + ensureAccountDeleted, deleteAccount, checkHandles, isBlacklistedHandle, @@ -100,6 +101,7 @@ import qualified Brig.Code as Code import Brig.Data.Activation (ActivationEvent (..), activationErrorToRegisterError) import qualified Brig.Data.Activation as Data import qualified Brig.Data.Client as Data +import Brig.Data.Connection (countConnections) import qualified Brig.Data.Connection as Data import qualified Brig.Data.Properties as Data import Brig.Data.User @@ -110,25 +112,26 @@ import Brig.Effects.BlacklistPhonePrefixStore (BlacklistPhonePrefixStore) import qualified Brig.Effects.BlacklistPhonePrefixStore as BlacklistPhonePrefixStore import Brig.Effects.BlacklistStore (BlacklistStore) import qualified Brig.Effects.BlacklistStore as BlacklistStore +import Brig.Effects.CodeStore (CodeStore) +import qualified Brig.Effects.CodeStore as E +import Brig.Effects.PasswordResetStore (PasswordResetStore) +import qualified Brig.Effects.PasswordResetStore as E +import Brig.Effects.UserPendingActivationStore (UserPendingActivation (..), UserPendingActivationStore) +import qualified Brig.Effects.UserPendingActivationStore as UserPendingActivationStore import qualified Brig.Federation.Client as Federation import qualified Brig.IO.Intra as Intra import qualified Brig.InternalEvent.Types as Internal import Brig.Options hiding (Timeout, internalEvents) import Brig.Password import qualified Brig.Queue as Queue -import Brig.Sem.CodeStore (CodeStore) -import qualified Brig.Sem.CodeStore as E -import Brig.Sem.PasswordResetStore (PasswordResetStore) -import qualified Brig.Sem.PasswordResetStore as E -import Brig.Sem.UserPendingActivationStore (UserPendingActivation (..), UserPendingActivationStore) -import qualified Brig.Sem.UserPendingActivationStore as UserPendingActivationStore import qualified Brig.Team.DB as Team +import Brig.Team.Types (ShowOrHideInvitationUrl (..)) import Brig.Types.Activation (ActivationPair) import Brig.Types.Connection import Brig.Types.Intra import Brig.Types.User (HavePendingInvitations (..), ManagedByUpdate (..), PasswordResetPair) import Brig.Types.User.Event -import Brig.User.Auth.Cookie (revokeAllCookies) +import Brig.User.Auth.Cookie (listCookies, revokeAllCookies) import Brig.User.Email import Brig.User.Handle import Brig.User.Handle.Blacklist @@ -147,6 +150,7 @@ import Data.Handle (Handle (fromHandle), parseHandle) import Data.Id as Id import Data.Json.Util import Data.LegalHold (UserLegalHoldStatus (..), defUserLegalHoldStatus) +import Data.List.Extra import Data.List1 as List1 (List1, singleton) import qualified Data.Map.Strict as Map import qualified Data.Metrics as Metrics @@ -410,7 +414,7 @@ createUser new = do findTeamInvitation (Just e) c = lift (wrapClient $ Team.lookupInvitationInfo c) >>= \case Just ii -> do - inv <- lift . wrapClient $ Team.lookupInvitation (Team.iiTeam ii) (Team.iiInvId ii) + inv <- lift . wrapClient $ Team.lookupInvitation HideInvitationUrl (Team.iiTeam ii) (Team.iiInvId ii) case (inv, Team.inInviteeEmail <$> inv) of (Just invite, Just em) | e == userEmailKey em -> do @@ -1230,10 +1234,57 @@ verifyDeleteUser d = do for_ account $ lift . wrapHttpClient . deleteAccount lift . wrapClient $ Code.delete key Code.AccountDeletion --- | Internal deletion without validation. Called via @delete /i/user/:uid@, or indirectly --- via deleting self. --- Team owners can be deleted if the team is not orphaned, i.e. there is at least one --- other owner left. +-- | Check if `deleteAccount` succeeded and run it again if needed. +-- Called via @delete /i/user/:uid@. +ensureAccountDeleted :: + ( MonadLogger m, + MonadCatch m, + MonadThrow m, + MonadIndexIO m, + MonadReader Env m, + MonadIO m, + MonadMask m, + MonadHttp m, + HasRequestId m, + MonadUnliftIO m, + MonadClient m, + MonadReader Env m + ) => + UserId -> + m DeleteUserResult +ensureAccountDeleted uid = do + mbAcc <- lookupAccount uid + case mbAcc of + Nothing -> pure NoUser + Just acc -> do + probs <- Data.lookupPropertyKeysAndValues uid + + let accIsDeleted = accountStatus acc == Deleted + clients <- Data.lookupClients uid + + localUid <- qualifyLocal uid + conCount <- countConnections localUid [(minBound @Relation) .. maxBound] + cookies <- listCookies uid [] + + if notNull probs + || not accIsDeleted + || notNull clients + || conCount > 0 + || notNull cookies + then do + deleteAccount acc + pure AccountDeleted + else pure AccountAlreadyDeleted + +-- | Internal deletion without validation. +-- +-- Called via @delete /i/user/:uid@ (through `ensureAccountDeleted`), or +-- indirectly via deleting self. Team owners can be deleted if the team is not +-- orphaned, i.e. there is at least one other owner left. +-- +-- N.B.: As Cassandra doesn't support transactions, the order of database +-- statements matters! Other functions reason upon some states to imply other +-- states. Please change this order only with care! deleteAccount :: ( MonadLogger m, MonadIndexIO m, @@ -1250,8 +1301,8 @@ deleteAccount account@(accountUser -> user) = do let uid = userId user Log.info $ field "user" (toByteString uid) . msg (val "Deleting account") -- Free unique keys - for_ (userEmail user) $ deleteKey . userEmailKey - for_ (userPhone user) $ deleteKey . userPhoneKey + for_ (userEmail user) $ deleteKeyForUser uid . userEmailKey + for_ (userPhone user) $ deleteKeyForUser uid . userPhoneKey for_ (userHandle user) $ freeHandle (userId user) -- Wipe data Data.clearProperties uid diff --git a/services/brig/src/Brig/AWS.hs b/services/brig/src/Brig/AWS.hs index 640bb3c830..33e55bb6d4 100644 --- a/services/brig/src/Brig/AWS.hs +++ b/services/brig/src/Brig/AWS.hs @@ -200,8 +200,8 @@ enqueueFIFO url group dedup m = retrying retry5x (const canRetry) (const (sendCa where req = SQS.newSendMessage url (Text.decodeLatin1 (BL.toStrict m)) - & SQS.sendMessage_messageGroupId .~ Just group - & SQS.sendMessage_messageDeduplicationId .~ Just (toText dedup) + & SQS.sendMessage_messageGroupId ?~ group + & SQS.sendMessage_messageDeduplicationId ?~ toText dedup ------------------------------------------------------------------------------- -- SES diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index aa6765dcef..4a18a47ec8 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -387,8 +387,8 @@ initCassandra o g = do c <- maybe (Cas.initialContactsPlain (Opt.cassandra o ^. casEndpoint . epHost)) - (Cas.initialContactsDisco "cassandra_brig") - (unpack <$> Opt.discoUrl o) + (Cas.initialContactsDisco "cassandra_brig" . unpack) + (Opt.discoUrl o) p <- Cas.init $ Cas.setLogger (Cas.mkLogger (Log.clone (Just "cassandra.brig") g)) diff --git a/services/brig/src/Brig/Calling.hs b/services/brig/src/Brig/Calling.hs index 48f892c64e..49ee66e677 100644 --- a/services/brig/src/Brig/Calling.hs +++ b/services/brig/src/Brig/Calling.hs @@ -180,9 +180,7 @@ discoverSRVRecords domain = srvDiscoveryLoop :: Members [DNSLookup, TinyLog, Delay] r => DNS.Domain -> Int -> (NonEmpty SrvEntry -> Sem r ()) -> Sem r () srvDiscoveryLoop domain discoveryInterval saveAction = forever $ do servers <- discoverSRVRecords domain - case servers of - Nothing -> pure () - Just es -> saveAction es + forM_ servers saveAction delay discoveryInterval mkSFTDomain :: SFTOptions -> DNS.Domain diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 6460a79c42..37b22f9233 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -5,12 +5,12 @@ import Brig.Effects.BlacklistPhonePrefixStore (BlacklistPhonePrefixStore) import Brig.Effects.BlacklistPhonePrefixStore.Cassandra (interpretBlacklistPhonePrefixStoreToCassandra) import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.BlacklistStore.Cassandra (interpretBlacklistStoreToCassandra) -import Brig.Sem.CodeStore (CodeStore) -import Brig.Sem.CodeStore.Cassandra (codeStoreToCassandra, interpretClientToIO) -import Brig.Sem.PasswordResetStore (PasswordResetStore) -import Brig.Sem.PasswordResetStore.CodeStore (passwordResetStoreToCodeStore) -import Brig.Sem.UserPendingActivationStore (UserPendingActivationStore) -import Brig.Sem.UserPendingActivationStore.Cassandra (userPendingActivationStoreToCassandra) +import Brig.Effects.CodeStore (CodeStore) +import Brig.Effects.CodeStore.Cassandra (codeStoreToCassandra, interpretClientToIO) +import Brig.Effects.PasswordResetStore (PasswordResetStore) +import Brig.Effects.PasswordResetStore.CodeStore (passwordResetStoreToCodeStore) +import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) +import Brig.Effects.UserPendingActivationStore.Cassandra (userPendingActivationStoreToCassandra) import qualified Cassandra as Cas import Control.Lens ((^.)) import Imports diff --git a/services/brig/src/Brig/Data/Activation.hs b/services/brig/src/Brig/Data/Activation.hs index 71efc8a17f..3de71d3982 100644 --- a/services/brig/src/Brig/Data/Activation.hs +++ b/services/brig/src/Brig/Data/Activation.hs @@ -34,9 +34,9 @@ where import Brig.App (Env) import Brig.Data.User import Brig.Data.UserKey +import qualified Brig.Effects.CodeStore as E +import Brig.Effects.CodeStore.Cassandra import Brig.Options -import qualified Brig.Sem.CodeStore as E -import Brig.Sem.CodeStore.Cassandra import Brig.Types.Intra import Cassandra import Control.Error diff --git a/services/brig/src/Brig/Data/Client.hs b/services/brig/src/Brig/Data/Client.hs index 9453071a5f..480f8bebf7 100644 --- a/services/brig/src/Brig/Data/Client.hs +++ b/services/brig/src/Brig/Data/Client.hs @@ -97,6 +97,8 @@ data ClientDataError | ClientMissingAuth | MalformedPrekeys | MLSPublicKeyDuplicate + | KeyPackageDecodingError + | InvalidKeyPackageRef -- | Re-authentication policy. -- diff --git a/services/brig/src/Brig/Data/Instances.hs b/services/brig/src/Brig/Data/Instances.hs index cea0641b7f..4ec4e3890c 100644 --- a/services/brig/src/Brig/Data/Instances.hs +++ b/services/brig/src/Brig/Data/Instances.hs @@ -31,20 +31,16 @@ import Control.Error (note) import Data.Aeson (eitherDecode, encode) import qualified Data.Aeson as JSON import Data.ByteString.Conversion -import qualified Data.ByteString.Lazy as LBS import Data.Domain (Domain, domainText, mkDomain) import Data.Handle (Handle (..)) import Data.Id () import Data.Range () import Data.String.Conversions (LBS, ST, cs) -import qualified Data.Text as T import Data.Text.Ascii () import Data.Text.Encoding (encodeUtf8) import Imports import Wire.API.Asset (AssetKey, assetKeyToText, nilAssetKey) import Wire.API.Connection (RelationWithHistory (..)) -import Wire.API.MLS.Credential -import Wire.API.MLS.KeyPackage import Wire.API.Properties import Wire.API.User import Wire.API.User.Activation @@ -283,26 +279,6 @@ instance Cql Domain where fromCql (CqlText txt) = mkDomain txt fromCql _ = Left "Domain: Text expected" -instance Cql SignatureSchemeTag where - ctype = Tagged TextColumn - toCql = CqlText . signatureSchemeName - fromCql (CqlText name) = - note ("Unexpected signature scheme: " <> T.unpack name) $ - signatureSchemeFromName name - fromCql _ = Left "SignatureScheme: Text expected" - -instance Cql KeyPackageRef where - ctype = Tagged BlobColumn - toCql = CqlBlob . LBS.fromStrict . unKeyPackageRef - fromCql (CqlBlob b) = pure . KeyPackageRef . LBS.toStrict $ b - fromCql _ = Left "Expected CqlBlob" - -instance Cql KeyPackageData where - ctype = Tagged BlobColumn - toCql = CqlBlob . LBS.fromStrict . kpData - fromCql (CqlBlob b) = pure . KeyPackageData . LBS.toStrict $ b - fromCql _ = Left "Expected CqlBlob" - instance Cql SearchVisibilityInbound where ctype = Tagged IntColumn diff --git a/services/brig/src/Brig/Data/UserKey.hs b/services/brig/src/Brig/Data/UserKey.hs index 1fbd4cb87c..28c4212435 100644 --- a/services/brig/src/Brig/Data/UserKey.hs +++ b/services/brig/src/Brig/Data/UserKey.hs @@ -29,6 +29,7 @@ module Brig.Data.UserKey keyAvailable, lookupKey, deleteKey, + deleteKeyForUser, lookupPhoneHashes, ) where @@ -164,6 +165,21 @@ deleteKey k = do retry x5 $ write deleteHashed (params LocalQuorum (Identity hk)) retry x5 $ write keyDelete (params LocalQuorum (Identity $ keyText k)) +-- | Delete `UserKey` for `UserId` +-- +-- This function ensures that keys of other users aren't accidentally deleted. +-- E.g. the email address or phone number of a partially deleted user could +-- already belong to a new user. To not interrupt deletion flows (that may be +-- executed several times due to cassandra not supporting transactions) +-- `deleteKeyForUser` does not fail for missing keys or keys that belong to +-- another user: It always returns `()` as result. +deleteKeyForUser :: (MonadClient m, MonadReader Env m) => UserId -> UserKey -> m () +deleteKeyForUser uid k = do + mbKeyUid <- lookupKey k + case mbKeyUid of + Just keyUid | keyUid == uid -> deleteKey k + _ -> pure () + hashKey :: MonadReader Env m => UserKey -> m UserKeyHash hashKey uk = do d <- view digestSHA256 diff --git a/services/brig/src/Brig/Effects/BlacklistPhonePrefixStore/Cassandra.hs b/services/brig/src/Brig/Effects/BlacklistPhonePrefixStore/Cassandra.hs index 0fc2d05c67..e8c1713f91 100644 --- a/services/brig/src/Brig/Effects/BlacklistPhonePrefixStore/Cassandra.hs +++ b/services/brig/src/Brig/Effects/BlacklistPhonePrefixStore/Cassandra.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE TemplateHaskell #-} - module Brig.Effects.BlacklistPhonePrefixStore.Cassandra ( interpretBlacklistPhonePrefixStoreToCassandra, ) diff --git a/services/brig/src/Brig/Effects/BlacklistStore/Cassandra.hs b/services/brig/src/Brig/Effects/BlacklistStore/Cassandra.hs index 06746441d6..995926b704 100644 --- a/services/brig/src/Brig/Effects/BlacklistStore/Cassandra.hs +++ b/services/brig/src/Brig/Effects/BlacklistStore/Cassandra.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE TemplateHaskell #-} - module Brig.Effects.BlacklistStore.Cassandra ( interpretBlacklistStoreToCassandra, ) diff --git a/services/brig/src/Brig/Sem/CodeStore.hs b/services/brig/src/Brig/Effects/CodeStore.hs similarity index 97% rename from services/brig/src/Brig/Sem/CodeStore.hs rename to services/brig/src/Brig/Effects/CodeStore.hs index 7c449c61ab..96f3e7c63b 100644 --- a/services/brig/src/Brig/Sem/CodeStore.hs +++ b/services/brig/src/Brig/Effects/CodeStore.hs @@ -16,7 +16,7 @@ -- with this program. If not, see . {-# LANGUAGE TemplateHaskell #-} -module Brig.Sem.CodeStore where +module Brig.Effects.CodeStore where import Data.Id import Data.Time.Clock diff --git a/services/brig/src/Brig/Sem/CodeStore/Cassandra.hs b/services/brig/src/Brig/Effects/CodeStore/Cassandra.hs similarity index 98% rename from services/brig/src/Brig/Sem/CodeStore/Cassandra.hs rename to services/brig/src/Brig/Effects/CodeStore/Cassandra.hs index 786fc63643..e6cae09099 100644 --- a/services/brig/src/Brig/Sem/CodeStore/Cassandra.hs +++ b/services/brig/src/Brig/Effects/CodeStore/Cassandra.hs @@ -16,14 +16,14 @@ -- with this program. If not, see . {-# LANGUAGE RecordWildCards #-} -module Brig.Sem.CodeStore.Cassandra +module Brig.Effects.CodeStore.Cassandra ( codeStoreToCassandra, interpretClientToIO, ) where import Brig.Data.Instances () -import Brig.Sem.CodeStore +import Brig.Effects.CodeStore import Cassandra import Data.ByteString.Conversion (toByteString') import Data.Id diff --git a/services/brig/src/Brig/Sem/PasswordResetStore.hs b/services/brig/src/Brig/Effects/PasswordResetStore.hs similarity index 96% rename from services/brig/src/Brig/Sem/PasswordResetStore.hs rename to services/brig/src/Brig/Effects/PasswordResetStore.hs index fe696473f1..aab8274893 100644 --- a/services/brig/src/Brig/Sem/PasswordResetStore.hs +++ b/services/brig/src/Brig/Effects/PasswordResetStore.hs @@ -16,7 +16,7 @@ -- with this program. If not, see . {-# LANGUAGE TemplateHaskell #-} -module Brig.Sem.PasswordResetStore where +module Brig.Effects.PasswordResetStore where import Brig.Types.User (PasswordResetPair) import Data.Id diff --git a/services/brig/src/Brig/Sem/PasswordResetStore/CodeStore.hs b/services/brig/src/Brig/Effects/PasswordResetStore/CodeStore.hs similarity index 95% rename from services/brig/src/Brig/Sem/PasswordResetStore/CodeStore.hs rename to services/brig/src/Brig/Effects/PasswordResetStore/CodeStore.hs index 2104d01ee4..c0248aa4e5 100644 --- a/services/brig/src/Brig/Sem/PasswordResetStore/CodeStore.hs +++ b/services/brig/src/Brig/Effects/PasswordResetStore/CodeStore.hs @@ -15,13 +15,13 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Brig.Sem.PasswordResetStore.CodeStore +module Brig.Effects.PasswordResetStore.CodeStore ( passwordResetStoreToCodeStore, ) where -import Brig.Sem.CodeStore -import Brig.Sem.PasswordResetStore +import Brig.Effects.CodeStore +import Brig.Effects.PasswordResetStore import Brig.Types.User (PasswordResetPair) import Data.Id import Data.Time diff --git a/services/brig/src/Brig/Sem/UserPendingActivationStore.hs b/services/brig/src/Brig/Effects/UserPendingActivationStore.hs similarity index 93% rename from services/brig/src/Brig/Sem/UserPendingActivationStore.hs rename to services/brig/src/Brig/Effects/UserPendingActivationStore.hs index a23f1d5a87..69a1db7397 100644 --- a/services/brig/src/Brig/Sem/UserPendingActivationStore.hs +++ b/services/brig/src/Brig/Effects/UserPendingActivationStore.hs @@ -1,6 +1,6 @@ {-# LANGUAGE TemplateHaskell #-} -module Brig.Sem.UserPendingActivationStore where +module Brig.Effects.UserPendingActivationStore where import Data.Id import Data.Time.Clock diff --git a/services/brig/src/Brig/Sem/UserPendingActivationStore/Cassandra.hs b/services/brig/src/Brig/Effects/UserPendingActivationStore/Cassandra.hs similarity index 90% rename from services/brig/src/Brig/Sem/UserPendingActivationStore/Cassandra.hs rename to services/brig/src/Brig/Effects/UserPendingActivationStore/Cassandra.hs index 9521af20d4..f3c4f8835e 100644 --- a/services/brig/src/Brig/Sem/UserPendingActivationStore/Cassandra.hs +++ b/services/brig/src/Brig/Effects/UserPendingActivationStore/Cassandra.hs @@ -1,9 +1,9 @@ -module Brig.Sem.UserPendingActivationStore.Cassandra +module Brig.Effects.UserPendingActivationStore.Cassandra ( userPendingActivationStoreToCassandra, ) where -import Brig.Sem.UserPendingActivationStore +import Brig.Effects.UserPendingActivationStore import Cassandra import Data.Id (UserId) import Data.Time (UTCTime) @@ -21,7 +21,7 @@ userPendingActivationStoreToCassandra = interpretH $ liftT . embed @Client . \case Add upa -> usersPendingActivationAdd upa - List Nothing -> (flip PC.mkInternalPage pure) =<< usersPendingActivationList + List Nothing -> flip PC.mkInternalPage pure =<< usersPendingActivationList List (Just ps) -> PC.ipNext ps RemoveMultiple uids -> usersPendingActivationRemoveMultiple uids diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index c22c8a178a..c5c4b65042 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -59,6 +59,7 @@ module Brig.IO.Intra getTeamSearchVisibility, getAllFeatureConfigsForUser, getVerificationCodeEnabled, + getTeamExposeInvitationURLsToTeamAdmin, -- * Legalhold guardLegalhold, @@ -80,6 +81,7 @@ import qualified Brig.Data.Connection as Data import Brig.Federation.Client (notifyUserDeleted) import qualified Brig.IO.Journal as Journal import Brig.RPC +import Brig.Team.Types (ShowOrHideInvitationUrl (..)) import Brig.Types.User.Event import Brig.User.Search.Index (MonadIndexIO) import qualified Brig.User.Search.Index as Search @@ -195,20 +197,27 @@ onPropertyEvent orig conn e = (pure $ list1 orig []) onClientEvent :: + ( MonadIO m, + Log.MonadLogger m, + MonadReader Env m, + MonadMask m, + MonadHttp m, + HasRequestId m + ) => -- | Originator of the event. UserId -> -- | Client connection ID. Maybe ConnId -> -- | The event. ClientEvent -> - (AppT r) () + m () onClientEvent orig conn e = do let events = singleton (ClientEvent e) let rcps = list1 orig [] -- Synchronous push for better delivery guarantees of these -- events and to make sure new clients have a first notification -- in the stream. - wrapHttp $ push events rcps orig Push.RouteAny conn + push events rcps orig Push.RouteAny conn updateSearchIndex :: ( MonadClient m, @@ -1360,6 +1369,28 @@ getTeamSearchVisibility tid = paths ["i", "teams", toByteString' tid, "search-visibility"] . expect2xx +getTeamExposeInvitationURLsToTeamAdmin :: + ( MonadLogger m, + MonadReader Env m, + MonadIO m, + MonadMask m, + MonadHttp m, + HasRequestId m + ) => + TeamId -> + m ShowOrHideInvitationUrl +getTeamExposeInvitationURLsToTeamAdmin tid = do + debug $ remote "galley" . msg (val "Get expose invitation URLs to team admin settings") + response <- galleyRequest GET req + status <- wsStatus <$> decodeBody @(WithStatus ExposeInvitationURLsToTeamAdminConfig) "galley" response + case status of + FeatureStatusEnabled -> pure ShowInvitationUrl + FeatureStatusDisabled -> pure HideInvitationUrl + where + req = + paths ["i", "teams", toByteString' tid, "features", featureNameBS @ExposeInvitationURLsToTeamAdminConfig] + . expect2xx + getVerificationCodeEnabled :: ( MonadReader Env m, MonadIO m, diff --git a/services/brig/src/Brig/InternalEvent/Process.hs b/services/brig/src/Brig/InternalEvent/Process.hs index abfd920e89..abd6a3902b 100644 --- a/services/brig/src/Brig/InternalEvent/Process.hs +++ b/services/brig/src/Brig/InternalEvent/Process.hs @@ -24,9 +24,13 @@ import Bilge.IO (MonadHttp) import Bilge.RPC (HasRequestId) import qualified Brig.API.User as API import Brig.App +import qualified Brig.Data.Client as Data +import Brig.IO.Intra (rmClient) +import qualified Brig.IO.Intra as Intra import Brig.InternalEvent.Types import Brig.Options (defDeleteThrottleMillis, setDeleteThrottleMillis) import qualified Brig.Provider.API as API +import Brig.Types.User.Event import Brig.User.Search.Index (MonadIndexIO) import Cassandra (MonadClient) import Control.Lens (view) @@ -54,6 +58,12 @@ onEvent :: InternalNotification -> m () onEvent n = handleTimeout $ case n of + DeleteClient cid uid mcon -> do + mc <- Data.lookupClient uid cid + for_ mc $ \c -> do + rmClient uid cid + Data.rmClient uid cid + Intra.onClientEvent uid mcon (ClientRemoved uid c) DeleteUser uid -> do Log.info $ msg (val "Processing user delete event") diff --git a/services/brig/src/Brig/InternalEvent/Types.hs b/services/brig/src/Brig/InternalEvent/Types.hs index c9ac85e3ac..fdb52fdf5b 100644 --- a/services/brig/src/Brig/InternalEvent/Types.hs +++ b/services/brig/src/Brig/InternalEvent/Types.hs @@ -25,22 +25,26 @@ import Data.Aeson import Data.Id data InternalNotification - = DeleteUser !UserId + = DeleteClient !ClientId !UserId !(Maybe ConnId) + | DeleteUser !UserId | DeleteService !ProviderId !ServiceId deriving (Eq, Show) data InternalNotificationType - = UserDeletion + = ClientDeletion + | UserDeletion | ServiceDeletion deriving (Eq, Show) instance FromJSON InternalNotificationType where parseJSON = \case + "client.delete" -> pure ClientDeletion "user.delete" -> pure UserDeletion "service.delete" -> pure ServiceDeletion x -> fail $ "InternalNotificationType: Unknown type " <> show x instance ToJSON InternalNotificationType where + toJSON ClientDeletion = "client.delete" toJSON UserDeletion = "user.delete" toJSON ServiceDeletion = "service.delete" @@ -48,10 +52,18 @@ instance FromJSON InternalNotification where parseJSON = withObject "InternalNotification" $ \o -> do t <- o .: "type" case (t :: InternalNotificationType) of + ClientDeletion -> DeleteClient <$> o .: "client" <*> o .: "user" <*> o .: "connection" UserDeletion -> DeleteUser <$> o .: "user" ServiceDeletion -> DeleteService <$> o .: "provider" <*> o .: "service" instance ToJSON InternalNotification where + toJSON (DeleteClient cid uid con) = + object + [ "client" .= cid, + "user" .= uid, + "connection" .= con, + "type" .= ClientDeletion + ] toJSON (DeleteUser uid) = object [ "user" .= uid, diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 061cdf4011..f02a1d5bb2 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -100,7 +100,7 @@ import Wire.API.Conversation.Bot import qualified Wire.API.Conversation.Bot as Public import Wire.API.Conversation.Role import Wire.API.Error -import Wire.API.Error.Brig +import qualified Wire.API.Error.Brig as E import qualified Wire.API.Event.Conversation as Public (Event) import Wire.API.Provider import qualified Wire.API.Provider as Public @@ -338,7 +338,7 @@ newAccount :: Public.NewProvider -> (Handler r) Public.NewProviderResponse newAccount new = do email <- case validateEmail (Public.newProviderEmail new) of Right em -> pure em - Left _ -> throwStd (errorToWai @'InvalidEmail) + Left _ -> throwStd (errorToWai @'E.InvalidEmail) let name = Public.newProviderName new let pass = Public.newProviderPassword new let descr = fromRange (Public.newProviderDescr new) @@ -376,7 +376,7 @@ activateAccountKey key val = do c <- wrapClientE (Code.verify key Code.IdentityVerification val) >>= maybeInvalidCode (pid, email) <- case (Code.codeAccount c, Code.codeForEmail c) of (Just p, Just e) -> pure (Id p, e) - _ -> throwStd (errorToWai @'InvalidCode) + _ -> throwStd (errorToWai @'E.InvalidCode) (name, memail, _url, _descr) <- wrapClientE (DB.lookupAccountData pid) >>= maybeInvalidCode case memail of Just email' | email == email' -> pure Nothing @@ -402,7 +402,7 @@ getActivationCode :: Public.Email -> (Handler r) FoundActivationCode getActivationCode e = do email <- case validateEmail e of Right em -> pure em - Left _ -> throwStd (errorToWai @'InvalidEmail) + Left _ -> throwStd (errorToWai @'E.InvalidEmail) gen <- Code.mkGen (Code.ForEmail email) code <- wrapClientE $ Code.lookup (Code.genKey gen) Code.IdentityVerification maybe (throwStd activationKeyNotFound) (pure . FoundActivationCode) code @@ -427,7 +427,7 @@ approveAccountKey key val = do (name, _, _, _) <- wrapClientE (DB.lookupAccountData (Id pid)) >>= maybeInvalidCode activate (Id pid) Nothing email lift $ sendApprovalConfirmMail name email - _ -> throwStd (errorToWai @'InvalidCode) + _ -> throwStd (errorToWai @'E.InvalidCode) loginH :: JsonRequest Public.ProviderLogin -> (Handler r) Response loginH req = do @@ -440,7 +440,7 @@ login l = do pid <- wrapClientE (DB.lookupKey (mkEmailKey (providerLoginEmail l))) >>= maybeBadCredentials pass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials unless (verifyPassword (providerLoginPassword l) pass) $ - throwStd (errorToWai @'BadCredentials) + throwStd (errorToWai @'E.BadCredentials) ZAuth.newProviderToken pid beginPasswordResetH :: JsonRequest Public.PasswordReset -> (Handler r) Response @@ -520,7 +520,7 @@ updateAccountEmail :: ProviderId -> Public.EmailUpdate -> (Handler r) () updateAccountEmail pid (Public.EmailUpdate new) = do email <- case validateEmail new of Right em -> pure em - Left _ -> throwStd (errorToWai @'InvalidEmail) + Left _ -> throwStd (errorToWai @'E.InvalidEmail) let emailKey = mkEmailKey email wrapClientE (DB.lookupKey emailKey) >>= mapM_ (const $ throwStd emailExists) gen <- Code.mkGen (Code.ForEmail email) @@ -543,7 +543,7 @@ updateAccountPassword :: ProviderId -> Public.PasswordChange -> (Handler r) () updateAccountPassword pid upd = do pass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials unless (verifyPassword (cpOldPassword upd) pass) $ - throwStd (errorToWai @'BadCredentials) + throwStd (errorToWai @'E.BadCredentials) when (verifyPassword (cpNewPassword upd) pass) $ throwStd newPasswordMustDiffer wrapClientE $ DB.updateAccountPassword pid (cpNewPassword upd) @@ -628,7 +628,7 @@ updateServiceConn :: ProviderId -> ServiceId -> Public.UpdateServiceConn -> (Han updateServiceConn pid sid upd = do pass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials unless (verifyPassword (updateServiceConnPassword upd) pass) $ - throwStd (errorToWai @'BadCredentials) + throwStd (errorToWai @'E.BadCredentials) scon <- wrapClientE (DB.lookupServiceConn pid sid) >>= maybeServiceNotFound svc <- wrapClientE (DB.lookupServiceProfile pid sid) >>= maybeServiceNotFound let newBaseUrl = updateServiceConnUrl upd @@ -679,7 +679,7 @@ deleteService :: ProviderId -> ServiceId -> Public.DeleteService -> (Handler r) deleteService pid sid del = do pass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials unless (verifyPassword (deleteServicePassword del) pass) $ - throwStd (errorToWai @'BadCredentials) + throwStd (errorToWai @'E.BadCredentials) _ <- wrapClientE (DB.lookupService pid sid) >>= maybeServiceNotFound -- Disable the service wrapClientE $ DB.updateServiceConn pid sid Nothing Nothing Nothing (Just False) @@ -741,7 +741,7 @@ deleteAccount pid del = do prov <- DB.lookupAccount pid >>= maybeInvalidProvider pass <- DB.lookupPassword pid >>= maybeBadCredentials unless (verifyPassword (deleteProviderPassword del) pass) $ - throwStd (errorToWai @'BadCredentials) + throwStd (errorToWai @'E.BadCredentials) svcs <- DB.listServices pid forM_ svcs $ \svc -> do let sid = serviceId svc @@ -990,12 +990,12 @@ botGetSelfH bot = do botGetSelf :: BotId -> (Handler r) Public.UserProfile botGetSelf bot = do p <- lift $ wrapClient $ User.lookupUser NoPendingInvitations (botUserId bot) - maybe (throwStd (errorToWai @'UserNotFound)) (pure . (`Public.publicProfile` UserLegalHoldNoConsent)) p + maybe (throwStd (errorToWai @'E.UserNotFound)) (pure . (`Public.publicProfile` UserLegalHoldNoConsent)) p botGetClientH :: BotId -> (Handler r) Response botGetClientH bot = do mapExceptT wrapHttp $ guardSecondFactorDisabled (Just (botUserId bot)) - maybe (throwStd (errorToWai @'ClientNotFound)) (pure . json) =<< lift (botGetClient bot) + maybe (throwStd (errorToWai @'E.ClientNotFound)) (pure . json) =<< lift (botGetClient bot) botGetClient :: BotId -> (AppT r) (Maybe Public.Client) botGetClient bot = @@ -1022,7 +1022,7 @@ botUpdatePrekeys :: BotId -> Public.UpdateBotPrekeys -> (Handler r) () botUpdatePrekeys bot upd = do clt <- lift $ listToMaybe <$> wrapClient (User.lookupClients (botUserId bot)) case clt of - Nothing -> throwStd (errorToWai @'ClientNotFound) + Nothing -> throwStd (errorToWai @'E.ClientNotFound) Just c -> do let pks = updateBotPrekeyList upd wrapClientE (User.updatePrekeys (botUserId bot) (clientId c) pks) !>> clientDataError @@ -1036,7 +1036,7 @@ botClaimUsersPrekeys :: Public.UserClients -> (Handler r) Public.UserClientPreke botClaimUsersPrekeys body = do maxSize <- fromIntegral . setMaxConvSize <$> view settings when (Map.size (Public.userClients body) > maxSize) $ - throwStd (errorToWai @'TooManyClients) + throwStd (errorToWai @'E.TooManyClients) Client.claimLocalMultiPrekeyBundles UnprotectedBot body !>> clientError botListUserProfilesH :: List UserId -> (Handler r) Response @@ -1184,7 +1184,7 @@ maybeInvalidProvider :: Monad m => Maybe a -> (ExceptT Error m) a maybeInvalidProvider = maybe (throwStd invalidProvider) pure maybeInvalidCode :: Monad m => Maybe a -> (ExceptT Error m) a -maybeInvalidCode = maybe (throwStd (errorToWai @'InvalidCode)) pure +maybeInvalidCode = maybe (throwStd (errorToWai @'E.InvalidCode)) pure maybeServiceNotFound :: Monad m => Maybe a -> (ExceptT Error m) a maybeServiceNotFound = maybe (throwStd (notFound "Service not found")) pure @@ -1196,7 +1196,7 @@ maybeConvNotFound :: Monad m => Maybe a -> (ExceptT Error m) a maybeConvNotFound = maybe (throwStd (notFound "Conversation not found")) pure maybeBadCredentials :: Monad m => Maybe a -> (ExceptT Error m) a -maybeBadCredentials = maybe (throwStd (errorToWai @'BadCredentials)) pure +maybeBadCredentials = maybe (throwStd (errorToWai @'E.BadCredentials)) pure maybeInvalidServiceKey :: Monad m => Maybe a -> (ExceptT Error m) a maybeInvalidServiceKey = maybe (throwStd invalidServiceKey) pure @@ -1205,7 +1205,7 @@ maybeInvalidBot :: Monad m => Maybe a -> (ExceptT Error m) a maybeInvalidBot = maybe (throwStd invalidBot) pure maybeInvalidUser :: Monad m => Maybe a -> (ExceptT Error m) a -maybeInvalidUser = maybe (throwStd (errorToWai @'InvalidUser)) pure +maybeInvalidUser = maybe (throwStd (errorToWai @'E.InvalidUser)) pure rangeChecked :: (Within a n m, Monad monad) => a -> (ExceptT Error monad) (Range n m a) rangeChecked = either (throwStd . invalidRange . fromString) pure . checkedEither diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index 8a5eba0a1c..d435284e4a 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -36,11 +36,11 @@ import qualified Brig.AWS.SesNotification as SesNotification import Brig.App import qualified Brig.Calling as Calling import Brig.CanonicalInterpreter +import Brig.Effects.UserPendingActivationStore (UserPendingActivation (UserPendingActivation), UserPendingActivationStore) +import qualified Brig.Effects.UserPendingActivationStore as UsersPendingActivationStore import qualified Brig.InternalEvent.Process as Internal import Brig.Options hiding (internalEvents, sesQueue) import qualified Brig.Queue as Queue -import Brig.Sem.UserPendingActivationStore (UserPendingActivation (UserPendingActivation), UserPendingActivationStore) -import qualified Brig.Sem.UserPendingActivationStore as UsersPendingActivationStore import Brig.Types.Intra (AccountStatus (PendingInvitation)) import Brig.Version import qualified Control.Concurrent.Async as Async @@ -102,7 +102,7 @@ run o = do authMetrics <- Async.async (runBrigToIO e collectAuthMetrics) pendingActivationCleanupAsync <- Async.async (runBrigToIO e pendingActivationCleanup) - runSettingsWithShutdown s app 5 `finally` do + runSettingsWithShutdown s app Nothing `finally` do mapM_ Async.cancel emailListener Async.cancel internalEventListener mapM_ Async.cancel sftDiscovery diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 9afa8b0a36..f00cd9ae1b 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -31,13 +31,14 @@ import Brig.Data.UserKey import qualified Brig.Data.UserKey as Data import Brig.Effects.BlacklistStore (BlacklistStore) import qualified Brig.Effects.BlacklistStore as BlacklistStore +import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import qualified Brig.Email as Email import qualified Brig.IO.Intra as Intra import Brig.Options (setMaxTeamSize, setTeamInvitationTimeout) import qualified Brig.Phone as Phone -import Brig.Sem.UserPendingActivationStore (UserPendingActivationStore) import qualified Brig.Team.DB as DB import Brig.Team.Email +import Brig.Team.Types (ShowOrHideInvitationUrl (..)) import Brig.Team.Util (ensurePermissionToAddUser, ensurePermissions) import Brig.Types.Intra (AccountStatus (..), NewUserScimInvitation (..), UserAccount (..)) import Brig.Types.Team (TeamSize) @@ -377,6 +378,7 @@ createInvitation' tid inviteeRole mbInviterUid fromEmail body = do let locale = irLocale body let inviteeName = irInviteeName body + showInvitationUrl <- lift $ wrapHttp $ Intra.getTeamExposeInvitationURLsToTeamAdmin tid lift $ do iid <- liftIO DB.mkInvitationId @@ -385,6 +387,7 @@ createInvitation' tid inviteeRole mbInviterUid fromEmail body = do (newInv, code) <- wrapClient $ DB.insertInvitation + showInvitationUrl iid tid inviteeRole @@ -412,7 +415,8 @@ listInvitationsH (_ ::: uid ::: tid ::: start ::: size) = do listInvitations :: UserId -> TeamId -> Maybe InvitationId -> Range 1 500 Int32 -> (Handler r) Public.InvitationList listInvitations uid tid start size = do ensurePermissions uid tid [AddTeamMember] - rs <- lift $ wrapClient $ DB.lookupInvitations tid start size + showInvitationUrl <- lift $ wrapHttp $ Intra.getTeamExposeInvitationURLsToTeamAdmin tid + rs <- lift $ wrapClient $ DB.lookupInvitations showInvitationUrl tid start size pure $! Public.InvitationList (DB.resultList rs) (DB.resultHasMore rs) getInvitationH :: JSON ::: UserId ::: TeamId ::: InvitationId -> (Handler r) Response @@ -425,7 +429,8 @@ getInvitationH (_ ::: uid ::: tid ::: iid) = do getInvitation :: UserId -> TeamId -> InvitationId -> (Handler r) (Maybe Public.Invitation) getInvitation uid tid iid = do ensurePermissions uid tid [AddTeamMember] - lift $ wrapClient $ DB.lookupInvitation tid iid + showInvitationUrl <- lift $ wrapHttp $ Intra.getTeamExposeInvitationURLsToTeamAdmin tid + lift $ wrapClient $ DB.lookupInvitation showInvitationUrl tid iid getInvitationByCodeH :: JSON ::: Public.InvitationCode -> (Handler r) Response getInvitationByCodeH (_ ::: c) = do @@ -433,7 +438,7 @@ getInvitationByCodeH (_ ::: c) = do getInvitationByCode :: Public.InvitationCode -> (Handler r) Public.Invitation getInvitationByCode c = do - inv <- lift . wrapClient $ DB.lookupInvitationByCode c + inv <- lift . wrapClient $ DB.lookupInvitationByCode HideInvitationUrl c maybe (throwStd $ errorToWai @'E.InvalidInvitationCode) pure inv headInvitationByEmailH :: JSON ::: Email -> (Handler r) Response @@ -453,7 +458,7 @@ getInvitationByEmailH (_ ::: email) = getInvitationByEmail :: Email -> (Handler r) Public.Invitation getInvitationByEmail email = do - inv <- lift $ wrapClient $ DB.lookupInvitationByEmail email + inv <- lift $ wrapClient $ DB.lookupInvitationByEmail HideInvitationUrl email maybe (throwStd (notFound "Invitation not found")) pure inv suspendTeamH :: JSON ::: TeamId -> (Handler r) Response diff --git a/services/brig/src/Brig/Team/DB.hs b/services/brig/src/Brig/Team/DB.hs index 0f6063c4d6..e52a511372 100644 --- a/services/brig/src/Brig/Team/DB.hs +++ b/services/brig/src/Brig/Team/DB.hs @@ -37,21 +37,29 @@ module Brig.Team.DB ) where +import Brig.App as App import Brig.Data.Instances () import Brig.Data.Types as T import Brig.Options +import Brig.Team.Template +import Brig.Team.Types (ShowOrHideInvitationUrl (..)) +import Brig.Template (renderTextWithBranding) import Cassandra as C +import Control.Lens (view) import Data.Conduit (runConduit, (.|)) import qualified Data.Conduit.List as C import Data.Id import Data.Json.Util (UTCTimeMillis, toUTCTimeMillis) import Data.Range -import Data.Text.Ascii (encodeBase64Url) +import Data.Text.Ascii (encodeBase64Url, toText) +import Data.Text.Encoding +import Data.Text.Lazy (toStrict) import Data.Time.Clock import Data.UUID.V4 import Imports import OpenSSL.Random (randBytes) import qualified System.Logger.Class as Log +import URI.ByteString import UnliftIO.Async (pooledMapConcurrentlyN_) import Wire.API.Team.Invitation import Wire.API.Team.Role @@ -76,7 +84,11 @@ data InvitationByEmail | InvitationByEmailMoreThanOne insertInvitation :: - MonadClient m => + ( Log.MonadLogger m, + MonadReader Env m, + MonadClient m + ) => + ShowOrHideInvitationUrl -> InvitationId -> TeamId -> Role -> @@ -88,9 +100,10 @@ insertInvitation :: -- | The timeout for the invitation code. Timeout -> m (Invitation, InvitationCode) -insertInvitation iid t role (toUTCTimeMillis -> now) minviter email inviteeName phone timeout = do +insertInvitation showUrl iid t role (toUTCTimeMillis -> now) minviter email inviteeName phone timeout = do code <- liftIO mkInvitationCode - let inv = Invitation t role iid now minviter email inviteeName phone + url <- mkInviteUrl showUrl t code + let inv = Invitation t role iid now minviter email inviteeName phone url retry x5 . batch $ do setType BatchLogged setConsistency LocalQuorum @@ -107,18 +120,33 @@ insertInvitation iid t role (toUTCTimeMillis -> now) minviter email inviteeName cqlInvitationByEmail :: PrepQuery W (Email, TeamId, InvitationId, InvitationCode, Int32) () cqlInvitationByEmail = "INSERT INTO team_invitation_email (email, team, invitation, code) VALUES (?, ?, ?, ?) USING TTL ?" -lookupInvitation :: MonadClient m => TeamId -> InvitationId -> m (Maybe Invitation) -lookupInvitation t r = - fmap toInvitation - <$> retry x1 (query1 cqlInvitation (params LocalQuorum (t, r))) +lookupInvitation :: + ( MonadClient m, + MonadReader Env m, + Log.MonadLogger m + ) => + ShowOrHideInvitationUrl -> + TeamId -> + InvitationId -> + m (Maybe Invitation) +lookupInvitation showUrl t r = do + inv <- retry x1 (query1 cqlInvitation (params LocalQuorum (t, r))) + traverse (toInvitation showUrl) inv where - cqlInvitation :: PrepQuery R (TeamId, InvitationId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, Email, Maybe Name, Maybe Phone) - cqlInvitation = "SELECT team, role, id, created_at, created_by, email, name, phone FROM team_invitation WHERE team = ? AND id = ?" + cqlInvitation :: PrepQuery R (TeamId, InvitationId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, Email, Maybe Name, Maybe Phone, InvitationCode) + cqlInvitation = "SELECT team, role, id, created_at, created_by, email, name, phone, code FROM team_invitation WHERE team = ? AND id = ?" -lookupInvitationByCode :: MonadClient m => InvitationCode -> m (Maybe Invitation) -lookupInvitationByCode i = +lookupInvitationByCode :: + ( Log.MonadLogger m, + MonadReader Env m, + MonadClient m + ) => + ShowOrHideInvitationUrl -> + InvitationCode -> + m (Maybe Invitation) +lookupInvitationByCode showUrl i = lookupInvitationInfo i >>= \case - Just InvitationInfo {..} -> lookupInvitation iiTeam iiInvId + Just InvitationInfo {..} -> lookupInvitation showUrl iiTeam iiInvId _ -> pure Nothing lookupInvitationCode :: MonadClient m => TeamId -> InvitationId -> m (Maybe InvitationCode) @@ -135,12 +163,21 @@ lookupInvitationCodeEmail t r = retry x1 (query1 cqlInvitationCodeEmail (params cqlInvitationCodeEmail :: PrepQuery R (TeamId, InvitationId) (InvitationCode, Email) cqlInvitationCodeEmail = "SELECT code, email FROM team_invitation WHERE team = ? AND id = ?" -lookupInvitations :: MonadClient m => TeamId -> Maybe InvitationId -> Range 1 500 Int32 -> m (ResultPage Invitation) -lookupInvitations team start (fromRange -> size) = do +lookupInvitations :: + ( Log.MonadLogger m, + MonadReader Env m, + MonadClient m + ) => + ShowOrHideInvitationUrl -> + TeamId -> + Maybe InvitationId -> + Range 1 500 Int32 -> + m (ResultPage Invitation) +lookupInvitations showUrl team start (fromRange -> size) = do page <- case start of Just ref -> retry x1 $ paginate cqlSelectFrom (paramsP LocalQuorum (team, ref) (size + 1)) Nothing -> retry x1 $ paginate cqlSelect (paramsP LocalQuorum (Identity team) (size + 1)) - pure $ toResult (hasMore page) $ map toInvitation (trim page) + toResult (hasMore page) <$> traverse (toInvitation showUrl) (trim page) where trim p = take (fromIntegral size) (result p) toResult more invs = @@ -149,10 +186,10 @@ lookupInvitations team start (fromRange -> size) = do { result = invs, hasMore = more } - cqlSelect :: PrepQuery R (Identity TeamId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, Email, Maybe Name, Maybe Phone) - cqlSelect = "SELECT team, role, id, created_at, created_by, email, name, phone FROM team_invitation WHERE team = ? ORDER BY id ASC" - cqlSelectFrom :: PrepQuery R (TeamId, InvitationId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, Email, Maybe Name, Maybe Phone) - cqlSelectFrom = "SELECT team, role, id, created_at, created_by, email, name, phone FROM team_invitation WHERE team = ? AND id > ? ORDER BY id ASC" + cqlSelect :: PrepQuery R (Identity TeamId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, Email, Maybe Name, Maybe Phone, InvitationCode) + cqlSelect = "SELECT team, role, id, created_at, created_by, email, name, phone, code FROM team_invitation WHERE team = ? ORDER BY id ASC" + cqlSelectFrom :: PrepQuery R (TeamId, InvitationId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, Email, Maybe Name, Maybe Phone, InvitationCode) + cqlSelectFrom = "SELECT team, role, id, created_at, created_by, email, name, phone, code FROM team_invitation WHERE team = ? AND id > ? ORDER BY id ASC" deleteInvitation :: MonadClient m => TeamId -> InvitationId -> m () deleteInvitation t i = do @@ -195,10 +232,17 @@ lookupInvitationInfo ic@(InvitationCode c) cqlInvitationInfo :: PrepQuery R (Identity InvitationCode) (TeamId, InvitationId) cqlInvitationInfo = "SELECT team, id FROM team_invitation_info WHERE code = ?" -lookupInvitationByEmail :: (Log.MonadLogger m, MonadClient m) => Email -> m (Maybe Invitation) -lookupInvitationByEmail e = +lookupInvitationByEmail :: + ( Log.MonadLogger m, + MonadReader Env m, + MonadClient m + ) => + ShowOrHideInvitationUrl -> + Email -> + m (Maybe Invitation) +lookupInvitationByEmail showUrl e = lookupInvitationInfoByEmail e >>= \case - InvitationByEmail InvitationInfo {..} -> lookupInvitation iiTeam iiInvId + InvitationByEmail InvitationInfo {..} -> lookupInvitation showUrl iiTeam iiInvId _ -> pure Nothing lookupInvitationInfoByEmail :: (Log.MonadLogger m, MonadClient m) => Email -> m InvitationByEmail @@ -230,6 +274,10 @@ countInvitations t = -- | brig used to not store the role, so for migration we allow this to be empty and fill in the -- default here. toInvitation :: + ( MonadReader Env m, + Log.MonadLogger m + ) => + ShowOrHideInvitationUrl -> ( TeamId, Maybe Role, InvitationId, @@ -237,8 +285,42 @@ toInvitation :: Maybe UserId, Email, Maybe Name, - Maybe Phone + Maybe Phone, + InvitationCode ) -> - Invitation -toInvitation (t, r, i, tm, minviter, e, inviteeName, p) = - Invitation t (fromMaybe defaultRole r) i tm minviter e inviteeName p + m Invitation +toInvitation showUrl (t, r, i, tm, minviter, e, inviteeName, p, code) = do + url <- mkInviteUrl showUrl t code + pure $ Invitation t (fromMaybe defaultRole r) i tm minviter e inviteeName p url + +mkInviteUrl :: + ( MonadReader Env m, + Log.MonadLogger m + ) => + ShowOrHideInvitationUrl -> + TeamId -> + InvitationCode -> + m (Maybe (URIRef Absolute)) +mkInviteUrl HideInvitationUrl _ _ = pure Nothing +mkInviteUrl ShowInvitationUrl team (InvitationCode c) = do + template <- invitationEmailUrl . invitationEmail . snd <$> teamTemplates Nothing + branding <- view App.templateBranding + let url = toStrict $ renderTextWithBranding template replace branding + parseHttpsUrl url + where + replace "team" = idToText team + replace "code" = toText c + replace x = x + + parseHttpsUrl :: Log.MonadLogger m => Text -> m (Maybe (URIRef Absolute)) + parseHttpsUrl url = + either (\e -> logError url e >> pure Nothing) (pure . Just) $ + parseURI laxURIParserOptions (encodeUtf8 url) + + logError :: (Log.MonadLogger m, Show e) => Text -> e -> m () + logError url e = + Log.err $ + Log.msg + (Log.val "Unable to create invitation url. Please check configuration.") + . Log.field "url" url + . Log.field "error" (show e) diff --git a/services/brig/src/Brig/Team/Types.hs b/services/brig/src/Brig/Team/Types.hs new file mode 100644 index 0000000000..e85bc4eb5b --- /dev/null +++ b/services/brig/src/Brig/Team/Types.hs @@ -0,0 +1,23 @@ +-- 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 Brig.Team.Types where + +import Imports + +data ShowOrHideInvitationUrl = ShowInvitationUrl | HideInvitationUrl + deriving (Eq, Show) diff --git a/services/brig/src/Brig/User/API/Auth.hs b/services/brig/src/Brig/User/API/Auth.hs index a528297b1b..2ea7a8d86f 100644 --- a/services/brig/src/Brig/User/API/Auth.hs +++ b/services/brig/src/Brig/User/API/Auth.hs @@ -110,6 +110,10 @@ routesPublic = do Doc.errorResponse passwordExists Doc.errorResponse' loginCodePending Doc.pendingLoginError + -- This endpoint is used to test /i/metrics, when this is servantified, please + -- make sure some other wai-route endpoint is used to test that routes defined in + -- this function ('Brig.API.Public.sitemap') are recorded and reported correctly in /i/metrics. + -- see test/integration/API/Metrics.hs post "/login" (continue loginH) $ jsonRequest @Public.Login .&. def False (query "persist") diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index d85d778670..712da1d6b0 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -194,7 +194,8 @@ verifyCode mbCode action uid = do featureEnabled <- lift $ do mbFeatureEnabled <- Intra.getVerificationCodeEnabled `traverse` mbTeamId pure $ fromMaybe (Public.wsStatus (Public.defFeatureStatus @Public.SndFactorPasswordChallengeConfig) == Public.FeatureStatusEnabled) mbFeatureEnabled - when featureEnabled $ do + isSsoUser <- Data.isSamlUser uid + when (featureEnabled && not isSsoUser) $ do case (mbCode, mbEmail) of (Just code, Just email) -> do key <- Code.mkKey $ Code.ForEmail email diff --git a/services/brig/src/Brig/User/Handle.hs b/services/brig/src/Brig/User/Handle.hs index 710f61affc..256337cc9e 100644 --- a/services/brig/src/Brig/User/Handle.hs +++ b/services/brig/src/Brig/User/Handle.hs @@ -59,9 +59,13 @@ claimHandle uid oldHandle newHandle = -- | Free a 'Handle', making it available to be claimed again. freeHandle :: MonadClient m => UserId -> Handle -> m () freeHandle uid h = do - retry x5 $ write handleDelete (params LocalQuorum (Identity h)) - let key = "@" <> fromHandle h - deleteClaim uid key (30 # Minute) + mbHandleUid <- lookupHandle h + case mbHandleUid of + Just handleUid | handleUid == uid -> do + retry x5 $ write handleDelete (params LocalQuorum (Identity h)) + let key = "@" <> fromHandle h + deleteClaim uid key (30 # Minute) + _ -> pure () -- this shouldn't happen, the call side should always check that `h` and `uid` belong to the same account. -- | Lookup the current owner of a 'Handle'. lookupHandle :: MonadClient m => Handle -> m (Maybe UserId) diff --git a/services/brig/test/integration/API/Internal.hs b/services/brig/test/integration/API/Internal.hs index 93d0eebb75..f6b7f3f4d4 100644 --- a/services/brig/test/integration/API/Internal.hs +++ b/services/brig/test/integration/API/Internal.hs @@ -363,7 +363,7 @@ testAddKeyPackageRef brig = do getFeatureConfig :: forall cfg m. (MonadIO m, MonadHttp m, HasCallStack, ApiFt.IsFeatureConfig cfg, KnownSymbol (ApiFt.FeatureSymbol cfg)) => (Request -> Request) -> UserId -> m ResponseLBS getFeatureConfig galley uid = do - get $ galley . paths ["feature-configs", featureNameBS @cfg] . zUser uid + get $ apiVersion "v1" . galley . paths ["feature-configs", featureNameBS @cfg] . zUser uid getAllFeatureConfigs :: (MonadIO m, MonadHttp m, HasCallStack) => (Request -> Request) -> UserId -> m ResponseLBS getAllFeatureConfigs galley uid = do diff --git a/services/brig/test/integration/API/MLS.hs b/services/brig/test/integration/API/MLS.hs index b71cca2873..cbd5fa5697 100644 --- a/services/brig/test/integration/API/MLS.hs +++ b/services/brig/test/integration/API/MLS.hs @@ -23,7 +23,6 @@ import Bilge.Assert import Brig.Options import Control.Timeout import qualified Data.Aeson as Aeson -import qualified Data.ByteString as BS import Data.ByteString.Conversion import Data.Default import Data.Id @@ -32,7 +31,6 @@ import qualified Data.Set as Set import Data.Timeout import Federation.Util import Imports -import Test.QuickCheck hiding ((===)) import Test.Tasty import Test.Tasty.HUnit import UnliftIO.Temporary @@ -40,6 +38,7 @@ import Util import Web.HttpApiData import Wire.API.MLS.Credential import Wire.API.MLS.KeyPackage +import Wire.API.MLS.Serialisation import Wire.API.User import Wire.API.User.Client @@ -186,13 +185,18 @@ testKeyPackageRemoteClaim opts brig = do u' <- userQualifiedId <$> randomUser brig - entries <- - liftIO . replicateM 2 . generate $ - -- claimed key packages are not validated by the backend, so it is fine to - -- make up some random data here - KeyPackageBundleEntry u <$> arbitrary - <*> (KeyPackageRef . BS.pack <$> vector 32) - <*> (KeyPackageData . BS.pack <$> vector 64) + qcid <- mkClientIdentity u <$> randomClient + entries <- withSystemTempDirectory "mls" $ \tmp -> do + initStore tmp qcid + replicateM 2 $ do + (r, kp) <- generateKeyPackage tmp qcid Nothing + pure $ + KeyPackageBundleEntry + { kpbeUser = u, + kpbeClient = ciClient qcid, + kpbeRef = kp, + kpbeKeyPackage = KeyPackageData . rmRaw $ r + } let mockBundle = KeyPackageBundle (Set.fromList entries) (bundle :: KeyPackageBundle, _reqs) <- liftIO . withTempMockFederator opts (Aeson.encode mockBundle) $ diff --git a/services/brig/test/integration/API/MLS/Util.hs b/services/brig/test/integration/API/MLS/Util.hs index 65dae9bdec..0671af1c65 100644 --- a/services/brig/test/integration/API/MLS/Util.hs +++ b/services/brig/test/integration/API/MLS/Util.hs @@ -23,19 +23,20 @@ import Bilge.Assert import Data.Aeson (object, toJSON, (.=)) import Data.ByteString.Conversion import Data.Default -import Data.Domain import Data.Id import Data.Json.Util import qualified Data.Map as Map import Data.Qualified -import qualified Data.Text as T +import qualified Data.Text as Text import Data.Timeout import Imports import System.FilePath import System.Process +import Test.Tasty.HUnit import Util import Wire.API.MLS.Credential import Wire.API.MLS.KeyPackage +import Wire.API.MLS.Serialisation import Wire.API.User.Client data SetKey = SetKey | DontSetKey @@ -49,6 +50,39 @@ data KeyingInfo = KeyingInfo instance Default KeyingInfo where def = KeyingInfo SetKey Nothing +cliCmd :: FilePath -> ClientIdentity -> [String] +cliCmd tmp qcid = + ["mls-test-cli", "--store", tmp (show qcid <> ".db")] + +initStore :: + HasCallStack => + MonadIO m => + FilePath -> + ClientIdentity -> + m () +initStore tmp qcid = do + let cmd0 = cliCmd tmp qcid + void . liftIO . flip spawn Nothing . shell . unwords $ + cmd0 <> ["init", show qcid] + +generateKeyPackage :: + HasCallStack => + MonadIO m => + FilePath -> + ClientIdentity -> + Maybe Timeout -> + m (RawMLS KeyPackage, KeyPackageRef) +generateKeyPackage tmp qcid lifetime = do + let cmd0 = cliCmd tmp qcid + kp <- + liftIO $ + decodeMLSError <=< (flip spawn Nothing . shell . unwords) $ + cmd0 + <> ["key-package", "create"] + <> (("--lifetime " <>) . show . (#> Second) <$> maybeToList lifetime) + let ref = fromJust (kpRef' kp) + pure (kp, ref) + uploadKeyPackages :: HasCallStack => Brig -> @@ -59,20 +93,10 @@ uploadKeyPackages :: Int -> Http () uploadKeyPackages brig tmp KeyingInfo {..} u c n = do - let cmd0 = ["mls-test-cli", "--store", tmp (clientId <> ".db")] - clientId = - show (qUnqualified u) - <> ":" - <> T.unpack (client c) - <> "@" - <> T.unpack (domainText (qDomain u)) - void . liftIO . flip spawn Nothing . shell . unwords $ - cmd0 <> ["init", clientId] - kps <- - replicateM n . liftIO . flip spawn Nothing . shell . unwords $ - cmd0 - <> ["key-package", "create"] - <> (("--lifetime " <>) . show . (#> Second) <$> maybeToList kiLifetime) + let cmd0 = cliCmd tmp cid + cid = mkClientIdentity u c + initStore tmp cid + kps <- replicateM n (fst <$> generateKeyPackage tmp cid kiLifetime) when (kiSetKey == SetKey) $ do pk <- @@ -85,7 +109,7 @@ uploadKeyPackages brig tmp KeyingInfo {..} u c n = do . json defUpdateClient {updateClientMLSPublicKeys = Map.fromList [(Ed25519, pk)]} ) !!! const 200 === statusCode - let upload = object ["key_packages" .= toJSON (map Base64ByteString kps)] + let upload = object ["key_packages" .= toJSON (map (Base64ByteString . rmRaw) kps)] post ( brig . paths ["mls", "key-packages", "self", toByteString' c] @@ -102,3 +126,8 @@ getKeyPackageCount brig u c = . zUser (qUnqualified u) ) ByteString -> IO a +decodeMLSError s = case decodeMLS' s of + Left e -> assertFailure ("Could not parse MLS object: " <> Text.unpack e) + Right x -> pure x diff --git a/services/brig/test/integration/API/Metrics.hs b/services/brig/test/integration/API/Metrics.hs index b705d2e522..807942b60d 100644 --- a/services/brig/test/integration/API/Metrics.hs +++ b/services/brig/test/integration/API/Metrics.hs @@ -51,29 +51,30 @@ testPrometheusMetrics brig = do const (Just "TYPE http_request_duration_seconds histogram") =~= responseBody testMetricsEndpoint :: Brig -> Http () -testMetricsEndpoint brig = do - let p1 = "/self" +testMetricsEndpoint brig0 = do + let brig = apiVersion "v1" . brig0 + p1 = "/self" p2 uid = "/users/" <> uid <> "/clients" - p3 = "/onboarding/v3" + p3 = "/login" beforeSelf <- getCount "/self" "GET" beforeClients <- getCount "/users/:uid/clients" "GET" - beforeProperties <- getCount "/onboarding/v3" "POST" - uid <- userId <$> randomUser brig + beforeProperties <- getCount "/login" "POST" + (uid, Just email) <- (\u -> (userId u, userEmail u)) <$> randomUser brig uid' <- userId <$> randomUser brig _ <- get (brig . path p1 . zAuthAccess uid "conn" . expect2xx) _ <- get (brig . path (p2 $ toByteString' uid) . zAuthAccess uid "conn" . expect2xx) _ <- get (brig . path (p2 $ toByteString' uid') . zAuthAccess uid "conn" . expect2xx) - _ <- post (brig . path p3 . zAuthAccess uid "conn" . json 'x' . expect2xx) - _ <- post (brig . path p3 . zAuthAccess uid "conn" . json 'x' . expect2xx) + _ <- post (brig . path p3 . contentJson . queryItem "persist" "true" . json (defEmailLogin email) . expect2xx) + _ <- post (brig . path p3 . contentJson . queryItem "persist" "true" . json (defEmailLogin email) . expect2xx) countSelf <- getCount "/self" "GET" liftIO $ assertEqual "/self was called once" (beforeSelf + 1) countSelf countClients <- getCount "/users/:uid/clients" "GET" liftIO $ assertEqual "/users/:uid/clients was called twice" (beforeClients + 2) countClients - countProperties <- getCount "/onboarding/v3" "POST" - liftIO $ assertEqual "/onboarding/v3 was called twice" (beforeProperties + 2) countProperties + countProperties <- getCount "/login" "POST" + liftIO $ assertEqual "/login was called twice" (beforeProperties + 2) countProperties where getCount endpoint m = do - rsp <- responseBody <$> get (brig . path "i/metrics") + rsp <- responseBody <$> get (brig0 . path "i/metrics") -- is there some responseBodyAsText function used elsewhere? let asText = fromMaybe "" (fromByteString' (fromMaybe "" rsp)) pure $ fromRight 0 (parseOnly (parseCount endpoint m) asText) diff --git a/services/brig/test/integration/API/Provider.hs b/services/brig/test/integration/API/Provider.hs index 3d3e81803a..6b6674f748 100644 --- a/services/brig/test/integration/API/Provider.hs +++ b/services/brig/test/integration/API/Provider.hs @@ -733,7 +733,8 @@ 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 - getConversation galley uid cid !!! const 404 === statusCode + eventually $ do + getConversation galley uid cid !!! const 404 === statusCode -- Check the bot cannot see the conversation either getBotConv galley bid cid !!! const 404 === statusCode @@ -1849,7 +1850,7 @@ svcAssertBotCreated buf bid cid = liftIO $ do -- TODO: Verify the conversation name -- TODO: Verify the list of members pure b - _ -> throwM $ HUnitFailure Nothing "Event timeout (TestBotCreated)" + _ -> assertFailure "Event timeout (TestBotCreated)" svcAssertMessage :: MonadIO m => Chan TestBotEvent -> Qualified UserId -> OtrMessage -> Qualified ConvId -> m () svcAssertMessage buf from msg cnv = liftIO $ do diff --git a/services/brig/test/integration/API/Settings.hs b/services/brig/test/integration/API/Settings.hs index ff2059d1ee..0efdb338d8 100644 --- a/services/brig/test/integration/API/Settings.hs +++ b/services/brig/test/integration/API/Settings.hs @@ -125,7 +125,7 @@ testUsersEmailVisibleIffExpected opts brig galley viewingUserIs visibilitySettin ] let newOpts = opts & Opt.optionSettings . Opt.emailVisibility .~ visibilitySetting withSettingsOverrides newOpts $ do - get (brig . zUser viewerId . path "users" . queryItem "ids" uids) !!! do + get (apiVersion "v1" . brig . zUser viewerId . path "users" . queryItem "ids" uids) !!! do const 200 === statusCode const (Just expected) === result where @@ -155,7 +155,7 @@ testGetUserEmailShowsEmailsIffExpected opts brig galley viewingUserIs visibility let newOpts = opts & Opt.optionSettings . Opt.emailVisibility .~ visibilitySetting withSettingsOverrides newOpts $ do forM_ expectations $ \(uid, expectedEmail) -> - get (brig . zUser viewerId . paths ["users", toByteString' uid]) !!! do + get (apiVersion "v1" . brig . zUser viewerId . paths ["users", toByteString' uid]) !!! do const 200 === statusCode const expectedEmail === emailResult where diff --git a/services/brig/test/integration/API/Team.hs b/services/brig/test/integration/API/Team.hs index 1462a421a4..9cff893a44 100644 --- a/services/brig/test/integration/API/Team.hs +++ b/services/brig/test/integration/API/Team.hs @@ -24,6 +24,7 @@ where import qualified API.Search.Util as SearchUtil import API.Team.Util +import API.User.Util as Util import Bilge hiding (accept, head, timeout) import qualified Bilge import Bilge.Assert @@ -57,6 +58,7 @@ import Web.Cookie (parseSetCookie, setCookieName) import Wire.API.Asset import Wire.API.Connection import Wire.API.Team hiding (newTeam) +import qualified Wire.API.Team.Feature as Public import Wire.API.Team.Invitation import Wire.API.Team.Member hiding (invitation, userId) import qualified Wire.API.Team.Member as Member @@ -65,6 +67,7 @@ import Wire.API.Team.Role import Wire.API.Team.Size import Wire.API.User import Wire.API.User.Auth +import Wire.API.User.Client (ClientType (PermanentClientType)) newtype TeamSizeLimit = TeamSizeLimit Word32 @@ -108,7 +111,8 @@ tests conf m n b c g aws = do testGroup "sso" $ [ test m "post /i/users - 201 internal-SSO" $ testCreateUserInternalSSO b g, test m "delete /i/users/:uid - 202 internal-SSO (ensure no orphan teams)" $ testDeleteUserSSO b g, - test m "get /i/teams/:tid/is-team-owner/:uid" $ testSSOIsTeamOwner b g + test m "get /i/teams/:tid/is-team-owner/:uid" $ testSSOIsTeamOwner b g, + test m "2FA disabled for SSO user" $ test2FaDisabledForSsoUser b g ], testGroup "size" $ [test m "get /i/teams/:tid/size" $ testTeamSize b] ] @@ -214,9 +218,9 @@ testInvitationEmailLookupNginz brig nginz = do -- expect an invitation to be found querying with email after invite headInvitationByEmail nginz email 200 -headInvitationByEmail :: Brig -> Email -> Int -> Http () -headInvitationByEmail brig email expectedCode = - Bilge.head (brig . path "/teams/invitations/by-email" . contentJson . queryItem "email" (toByteString' email)) +headInvitationByEmail :: (Request -> Request) -> Email -> Int -> Http () +headInvitationByEmail service email expectedCode = + Bilge.head (service . path "/teams/invitations/by-email" . contentJson . queryItem "email" (toByteString' email)) !!! const expectedCode === statusCode testInvitationTooManyPending :: Brig -> TeamSizeLimit -> Http () @@ -379,7 +383,9 @@ createAndVerifyInvitation' replacementBrigApp acceptFn invite brig galley = do mem <- getTeamMember invitee tid galley liftIO $ assertEqual "Member not part of the team" invitee (mem ^. Member.userId) liftIO $ assertEqual "Member has no/wrong invitation metadata" invmeta (mem ^. Member.invitation) - conns <- listConnections invitee brig + conns <- + responseJsonError =<< listConnections brig invitee + Galley -> Http () +test2FaDisabledForSsoUser brig galley = do + teamid <- snd <$> createUserWithTeam brig + setTeamFeatureLockStatus @Public.SndFactorPasswordChallengeConfig galley teamid Public.LockStatusUnlocked + setTeamSndFactorPasswordChallenge galley teamid Public.FeatureStatusEnabled + let ssoid = UserSSOId mkSimpleSampleUref + createUserResp <- + postUser "dummy" True False (Just ssoid) (Just teamid) brig responseJsonMaybe createUserResp + let verificationCode = Nothing + addClient brig uid (defNewClientWithVerificationCode verificationCode PermanentClientType [head somePrekeys] (head someLastPrekeys)) + !!! const 201 === statusCode + -- TODO: -- add sso service. (we'll need a name for that now.) -- brig needs to notify the sso service about deletions! diff --git a/services/brig/test/integration/API/Team/Util.hs b/services/brig/test/integration/API/Team/Util.hs index 64e8120a6e..020447a2d8 100644 --- a/services/brig/test/integration/API/Team/Util.hs +++ b/services/brig/test/integration/API/Team/Util.hs @@ -38,7 +38,6 @@ import qualified Network.Wai.Utilities.Error as Error import Test.Tasty.HUnit import Util import Web.Cookie (parseSetCookie, setCookieName) -import Wire.API.Connection import Wire.API.Conversation import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role @@ -343,15 +342,6 @@ register' e t c brig = ) ) -listConnections :: HasCallStack => UserId -> Brig -> (MonadIO m, MonadHttp m, MonadThrow m) => m UserConnectionList -listConnections u brig = do - responseJsonError - =<< get - ( brig - . path "connections" - . zUser u - ) - getInvitation :: Brig -> InvitationCode -> (MonadIO m, MonadHttp m) => m (Maybe Invitation) getInvitation brig c = do r <- diff --git a/services/brig/test/integration/API/User/Account.hs b/services/brig/test/integration/API/User/Account.hs index e00b11c54f..24cbcf0968 100644 --- a/services/brig/test/integration/API/User/Account.hs +++ b/services/brig/test/integration/API/User/Account.hs @@ -24,7 +24,7 @@ module API.User.Account where import qualified API.Search.Util as Search -import API.Team.Util hiding (listConnections) +import API.Team.Util import API.User.Util import Bilge hiding (accept, timeout) import Bilge.Assert @@ -46,6 +46,7 @@ import qualified Data.ByteString as C8 import Data.ByteString.Char8 (pack) import Data.ByteString.Conversion import Data.Domain +import Data.Handle import Data.Id hiding (client) import Data.Json.Util (fromUTCTimeMillis) import Data.List1 (singleton) @@ -72,6 +73,7 @@ import qualified Network.HTTP.Types as Http import qualified Network.Wai as Wai import qualified Network.Wai.Utilities.Error as Error import qualified Network.Wai.Utilities.Error as Wai +import Test.QuickCheck (arbitrary, generate) import Test.Tasty hiding (Timeout) import Test.Tasty.Cannon hiding (Cannon) import qualified Test.Tasty.Cannon as WS @@ -87,6 +89,7 @@ import Wire.API.Federation.API.Brig (UserDeletedConnectionsNotification (..)) import qualified Wire.API.Federation.API.Brig as FedBrig import Wire.API.Federation.API.Common (EmptyResponse (EmptyResponse)) import Wire.API.Internal.Notification +import Wire.API.Team.Feature (ExposeInvitationURLsToTeamAdminConfig (..), FeatureStatus (..), FeatureTTL' (..), LockStatus (LockStatusLocked), withStatus) import Wire.API.Team.Invitation (Invitation (inInvitation)) import Wire.API.Team.Permission hiding (self) import Wire.API.User @@ -148,7 +151,6 @@ tests _ at opts p b c ch g aws = test' aws p "delete/with-legalhold" $ testDeleteUserWithLegalHold b c aws, test' aws p "delete/by-code" $ testDeleteUserByCode b, test' aws p "delete/anonymous" $ testDeleteAnonUser b, - test' aws p "delete /i/users/:uid - 202" $ testDeleteInternal b c aws, test' aws p "delete with profile pic" $ testDeleteWithProfilePic b ch, test' aws p "delete with connected remote users" $ testDeleteWithRemotes opts b, test' aws p "delete with connected remote users and failed remote notifcations" $ testDeleteWithRemotesAndFailedNotifications opts b c, @@ -160,6 +162,13 @@ tests _ at opts p b c ch g aws = testGroup "update user email by team owner" [ test' aws p "put /users/:uid/email" $ testUpdateUserEmailByTeamOwner b + ], + testGroup + "delete /i/users/:uid" + [ test' aws p "does nothing for completely deleted user" $ testDeleteUserWithCompletelyDeletedUser b c aws, + test' aws p "does nothing when the user doesn't exist" $ testDeleteUserWithNoUser b, + test' aws p "deletes a not deleted user" $ testDeleteUserWithNotDeletedUser b c aws, + test' aws p "delete again because of dangling property" $ testDeleteUserWithDanglingProperty b c aws ] ] @@ -580,11 +589,11 @@ testNonExistingUserUnqualified :: Brig -> Http () testNonExistingUserUnqualified brig = do findingOne <- liftIO $ Id <$> UUID.nextRandom foundOne <- liftIO $ Id <$> UUID.nextRandom - get (brig . paths ["users", pack $ show foundOne] . zUser findingOne) + get (apiVersion "v1" . brig . paths ["users", pack $ show foundOne] . zUser findingOne) !!! do const 404 === statusCode const (Just "not-found") === fmap Error.label . responseJsonMaybe - get (brig . paths ["users", pack $ show foundOne] . zUser foundOne) + get (apiVersion "v1" . brig . paths ["users", pack $ show foundOne] . zUser foundOne) !!! do const 404 === statusCode const (Just "not-found") === fmap Error.label . responseJsonMaybe @@ -596,11 +605,11 @@ testNonExistingUser brig = do uid2 <- liftIO $ Id <$> UUID.nextRandom let uid = qUnqualified qself domain = qDomain qself - get (brig . paths ["users", toByteString' domain, toByteString' uid1] . zUser uid) + get (apiVersion "v1" . brig . paths ["users", toByteString' domain, toByteString' uid1] . zUser uid) !!! do const 404 === statusCode const (Just "not-found") === fmap Error.label . responseJsonMaybe - get (brig . paths ["users", toByteString' domain, toByteString' uid2] . zUser uid) + get (apiVersion "v1" . brig . paths ["users", toByteString' domain, toByteString' uid2] . zUser uid) !!! do const 404 === statusCode const (Just "not-found") === fmap Error.label . responseJsonMaybe @@ -620,7 +629,7 @@ testUserInvalidDomain brig = do testExistingUserUnqualified :: Brig -> Http () testExistingUserUnqualified brig = do uid <- userId <$> randomUser brig - get (brig . paths ["users", pack $ show uid] . zUser uid) !!! do + get (apiVersion "v1" . brig . paths ["users", pack $ show uid] . zUser uid) !!! do const 200 === statusCode const (Just uid) === ( \r -> do @@ -634,7 +643,8 @@ testExistingUser brig = do let uid = qUnqualified quser domain = qDomain quser get - ( brig + ( apiVersion "v1" + . brig . zUser uid . paths [ "users", @@ -655,7 +665,8 @@ testUserExistsUnqualified brig = do qself <- userQualifiedId <$> randomUser brig quser <- userQualifiedId <$> randomUser brig head - ( brig + ( apiVersion "v1" + . brig . paths ["users", toByteString' (qUnqualified quser)] . zUser (qUnqualified qself) ) @@ -717,7 +728,8 @@ testMultipleUsersUnqualified brig = do (Just $ userDisplayName u3, Nothing) ] get - ( brig + ( apiVersion "v1" + . brig . zUser (userId u1) . contentJson . path "users" @@ -785,7 +797,7 @@ testCreateUserAnonExpiry b = do liftIO $ assertBool "Bob must be in deleted state" (fromMaybe False $ deleted resBob') where getProfile :: UserId -> UserId -> Http ResponseLBS - getProfile zusr uid = get (b . zUser zusr . paths ["users", toByteString' uid]) UserId -> UserId -> Http () awaitExpiry n zusr uid = do -- after expiration, a profile lookup should trigger garbage collection of ephemeral users @@ -809,7 +821,7 @@ testCreateUserAnonExpiry b = do field :: FromJSON a => Text -> Value -> Maybe a field f u = u ^? key f >>= maybeFromJSON -testUserUpdate :: Brig -> Cannon -> AWS.Env -> Http () +testUserUpdate :: HasCallStack => Brig -> Cannon -> AWS.Env -> Http () testUserUpdate brig cannon aws = do aliceUser <- randomUser brig liftIO $ Util.assertUserJournalQueue "user create alice" aws (userActivateJournaled aliceUser) @@ -1336,13 +1348,6 @@ testDeleteAnonUser brig = do deleteUser uid Nothing brig !!! const 200 === statusCode -testDeleteInternal :: Brig -> Cannon -> AWS.Env -> Http () -testDeleteInternal brig cannon aws = do - u <- randomUser brig - liftIO $ Util.assertUserJournalQueue "user activate testDeleteInternal1: " aws (userActivateJournaled u) - setHandleAndDeleteUser brig cannon u [] aws $ - \uid -> delete (brig . paths ["/i/users", toByteString' uid]) !!! const 202 === statusCode - testDeleteWithProfilePic :: Brig -> CargoHold -> Http () testDeleteWithProfilePic brig cargohold = do uid <- userId <$> createAnonUser "anon" brig @@ -1609,17 +1614,26 @@ testTooManyMembersForLegalhold opts brig = do responseJsonError =<< postInvitation brig tid owner invite Cannon -> AWS.Env -> Http () +testDeleteUserWithCompletelyDeletedUser brig cannon aws = do + u <- randomUser brig + liftIO $ Util.assertUserJournalQueue "user activate testDeleteUserWithCompletelyDeletedUser" aws (userActivateJournaled u) + setHandleAndDeleteUser brig cannon u [] aws $ + \uid -> deleteUserInternal uid brig !!! const 202 === statusCode + do + let uid = userId u + deleteUserInternal uid brig + !!! do + const 200 === statusCode + +testDeleteUserWithNoUser :: Brig -> Http () +testDeleteUserWithNoUser brig = do + nonExistingUid :: UserId <- liftIO $ generate arbitrary + deleteUserInternal nonExistingUid brig + !!! do + const 404 === statusCode + +testDeleteUserWithNotDeletedUser :: HasCallStack => Brig -> Cannon -> AWS.Env -> Http () +testDeleteUserWithNotDeletedUser brig cannon aws = do + u <- randomUser brig + liftIO $ Util.assertUserJournalQueue "user activate testDeleteUserWithNotDeletedUser" aws (userActivateJournaled u) + do + setHandleAndDeleteUser brig cannon u [] aws $ + ( \uid' -> + deleteUserInternal uid' brig + !!! do + const 202 === statusCode + ) + +testDeleteUserWithDanglingProperty :: Brig -> Cannon -> AWS.Env -> Http () +testDeleteUserWithDanglingProperty brig cannon aws = do + u <- randomUser brig + liftIO $ Util.assertUserJournalQueue "user activate testDeleteUserWithDanglingProperty" aws (userActivateJournaled u) + + let uid = userId u + -- First set a unique handle (to verify freeing of the handle) + hdl <- randomHandle + let update = RequestBodyLBS . encode $ HandleUpdate hdl + put (brig . path "/self/handle" . contentJson . zUser uid . zConn "c" . body update) + !!! const 200 === statusCode + + deleteUserInternal uid brig !!! const 202 === statusCode + liftIO $ Util.assertUserJournalQueue "user deletion testDeleteUserWithDanglingProperty" aws (userDeleteJournaled uid) + + setProperty brig (userId u) "foo" objectProp + !!! const 200 === statusCode + getProperty brig (userId u) "foo" !!! do + const 200 === statusCode + const (Just objectProp) === responseJsonMaybe + + execAndAssertUserDeletion brig cannon u (Handle hdl) [] aws $ \uid' -> do + deleteUserInternal uid' brig + !!! do + const 202 === statusCode + + getProperty brig (userId u) "foo" !!! do + const 404 === statusCode + where + objectProp = + object + [ "key.1" .= ("val1" :: Text), + "key.2" .= ("val2" :: Text) + ] + -- helpers setHandleAndDeleteUser :: Brig -> Cannon -> User -> [UserId] -> AWS.Env -> (UserId -> HttpT IO ()) -> Http () setHandleAndDeleteUser brig cannon u others aws execDelete = do let uid = userId u - quid = userQualifiedId u - email = fromMaybe (error "Must have an email set") (userEmail u) -- First set a unique handle (to verify freeing of the handle) hdl <- randomHandle let update = RequestBodyLBS . encode $ HandleUpdate hdl put (brig . path "/self/handle" . contentJson . zUser uid . zConn "c" . body update) !!! const 200 === statusCode + + execAndAssertUserDeletion brig cannon u (Handle hdl) others aws execDelete + +execAndAssertUserDeletion :: Brig -> Cannon -> User -> Handle -> [UserId] -> AWS.Env -> (UserId -> HttpT IO ()) -> Http () +execAndAssertUserDeletion brig cannon u hdl others aws execDelete = do + let uid = userId u + quid = userQualifiedId u + email = fromMaybe (error "Must have an email set") (userEmail u) + -- Delete the user WS.bracketRN cannon (uid : others) $ \wss -> do execDelete uid @@ -1665,9 +1752,9 @@ setHandleAndDeleteUser brig cannon u others aws execDelete = do Search.refreshIndex brig -- Does not appear in search; public profile shows the user as deleted forM_ others $ \usr -> do - get (brig . paths ["users", toByteString' uid] . zUser usr) !!! assertDeletedProfilePublic + get (apiVersion "v1" . brig . paths ["users", toByteString' uid] . zUser usr) !!! assertDeletedProfilePublic Search.assertCan'tFind brig usr quid (fromName (userDisplayName u)) - Search.assertCan'tFind brig usr quid hdl + Search.assertCan'tFind brig usr quid (fromHandle hdl) -- Email address is available again let Object o = object @@ -1677,7 +1764,7 @@ setHandleAndDeleteUser brig cannon u others aws execDelete = do ] -- This will generate a new event, we need to consume it here usr <- postUserInternal o brig - liftIO $ Util.assertUserJournalQueue "user activate testDeleteInternal: " aws (userActivateJournaled usr) + liftIO $ Util.assertUserJournalQueue "user activate execAndAssertUserDeletion" aws (userActivateJournaled usr) -- Handle is available again Bilge.head (brig . paths ["users", "handles", toByteString' hdl] . zUser uid) !!! const 404 === statusCode diff --git a/services/brig/test/integration/API/User/Auth.hs b/services/brig/test/integration/API/User/Auth.hs index 896ab54098..4e2ccc67cb 100644 --- a/services/brig/test/integration/API/User/Auth.hs +++ b/services/brig/test/integration/API/User/Auth.hs @@ -46,6 +46,7 @@ import Data.Handle (Handle (Handle)) import Data.Id import Data.Misc (PlainTextPassword (..)) import Data.Proxy +import Data.Qualified (Qualified (qUnqualified)) import Data.Range (unsafeRange) import qualified Data.Text as Text import Data.Text.Ascii (AsciiChars (validate)) @@ -62,6 +63,7 @@ import Test.Tasty.HUnit import qualified Test.Tasty.HUnit as HUnit import UnliftIO.Async hiding (wait) import Util +import Wire.API.Conversation (Conversation (..)) import qualified Wire.API.Team.Feature as Public import Wire.API.User import qualified Wire.API.User as Public @@ -148,8 +150,10 @@ tests conf m z db b g n = ], testGroup "update /access/self/email" - [ test m "valid token (idempotency case)" (testAccessSelfEmailAllowed n b), - test m "invalid or missing token" (testAccessSelfEmailDenied z n b) + [ test m "valid token (idempotency case) (with cookie)" (testAccessSelfEmailAllowed n b True), + test m "valid token (idempotency case) (without cookie)" (testAccessSelfEmailAllowed n b False), + test m "invalid or missing token (with cookie)" (testAccessSelfEmailDenied z n b True), + test m "invalid or missing token (without cookie)" (testAccessSelfEmailDenied z n b False) ], testGroup "cookies" @@ -197,7 +201,7 @@ testNginz b n = do liftIO $ assertEqual "Ensure nginz is started. Ensure nginz and brig share the same private/public zauth keys. Ensure ACL file is correct." 200 (statusCode _rs) -- ensure nginz allows refresh at /access _rs <- - post (n . path "/access" . cookie c . header "Authorization" ("Bearer " <> toByteString' t)) toByteString' t)) toByteString' t)) !!! const 200 === statusCode @@ -226,8 +230,12 @@ testNginzLegalHold b g n = do cUsr = decodeCookie rsUsr pure (c, t) + qconv <- + fmap cnvQualifiedId . responseJsonError + =<< createConversation g (userId alice) [] toByteString' t)) !!! do + post (unversioned . n . path "/access" . cookie c . header "Authorization" ("Bearer " <> toByteString' t)) !!! do const 200 === statusCode -- ensure legalhold tokens CANNOT fetch /clients get (n . path "/clients" . header "Authorization" ("Bearer " <> toByteString' t)) !!! const 403 === statusCode @@ -235,6 +243,10 @@ testNginzLegalHold b g n = do -- ensure legal hold tokens can fetch notifications get (n . path "/notifications" . header "Authorization" ("Bearer " <> toByteString' t)) !!! const 200 === statusCode + get (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 + -- | 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, -- effectively setting it to the backend host), clients may start sending two cookies for a @@ -264,16 +276,16 @@ testNginzMultipleCookies o b n = do badCookie2 <- (\c -> c {cookie_value = "SKsjKQbiqxuEugGMWVbq02fNEA7QFdNmTiSa1Y0YMgaEP5tWl3nYHWlIrM5F8Tt7Cfn2Of738C7oeiY8xzPHAC==.v=1.k=1.d=1.t=u.l=.u=13da31b4-c6bb-4561-8fed-07e728fa6cc5.r=f844b420"}) . decodeCookie <$> dologin -- Basic sanity checks - post (n . path "/access" . cookie goodCookie) !!! const 200 === statusCode - post (n . path "/access" . cookie badCookie1) !!! const 403 === statusCode - post (n . path "/access" . cookie badCookie2) !!! const 403 === statusCode + post (unversioned . n . path "/access" . cookie goodCookie) !!! const 200 === statusCode + post (unversioned . n . path "/access" . cookie badCookie1) !!! const 403 === statusCode + post (unversioned . n . path "/access" . cookie badCookie2) !!! const 403 === statusCode -- Sending both cookies should always work, regardless of the order (they are ordered by time) - post (n . path "/access" . cookie badCookie1 . cookie goodCookie . cookie badCookie2) !!! const 200 === statusCode - post (n . path "/access" . cookie goodCookie . cookie badCookie1 . cookie badCookie2) !!! const 200 === statusCode - post (n . path "/access" . cookie badCookie1 . cookie badCookie2 . cookie goodCookie) !!! const 200 === statusCode -- -- Sending a bad cookie and an unparseble one should work too - post (n . path "/access" . cookie unparseableCookie . cookie goodCookie) !!! const 200 === statusCode - post (n . path "/access" . cookie goodCookie . cookie unparseableCookie) !!! const 200 === statusCode + post (unversioned . n . path "/access" . cookie badCookie1 . cookie goodCookie . cookie badCookie2) !!! const 200 === statusCode + post (unversioned . n . path "/access" . cookie goodCookie . cookie badCookie1 . cookie badCookie2) !!! const 200 === statusCode + post (unversioned . n . path "/access" . cookie badCookie1 . cookie badCookie2 . cookie goodCookie) !!! const 200 === statusCode -- -- Sending a bad cookie and an unparseble one should work too + post (unversioned . n . path "/access" . cookie unparseableCookie . cookie goodCookie) !!! const 200 === statusCode + post (unversioned . n . path "/access" . cookie goodCookie . cookie unparseableCookie) !!! const 200 === statusCode -- We want to make sure we are using a cookie that was deleted from the DB but not expired - this way the client -- will still have it in the cookie jar because it did not get overriden @@ -281,10 +293,10 @@ testNginzMultipleCookies o b n = do now <- liftIO getCurrentTime liftIO $ assertBool "cookie should not be expired" (cookie_expiry_time deleted > now) liftIO $ assertBool "cookie should not be expired" (cookie_expiry_time valid > now) - post (n . path "/access" . cookie deleted) !!! const 403 === statusCode - post (n . path "/access" . cookie valid) !!! const 200 === statusCode - post (n . path "/access" . cookie deleted . cookie valid) !!! const 200 === statusCode - post (n . path "/access" . cookie valid . cookie deleted) !!! const 200 === statusCode + post (unversioned . n . path "/access" . cookie deleted) !!! const 403 === statusCode + post (unversioned . n . path "/access" . cookie valid) !!! const 200 === statusCode + post (unversioned . n . path "/access" . cookie deleted . cookie valid) !!! const 200 === statusCode + post (unversioned . n . path "/access" . cookie valid . cookie deleted) !!! const 200 === statusCode ------------------------------------------------------------------------------- -- Login @@ -653,11 +665,11 @@ testLegalHoldLogout brig galley = do uid <- prepareLegalHoldUser brig galley _rs <- legalHoldLogin brig (LegalHoldLogin uid (Just defPassword) Nothing) PersistentCookie ZAuth.Env -> Brig -> Http () testInvalidCookie z b = do -- Syntactically invalid - post (b . path "/access" . cookieRaw "zuid" "xxx") !!! do + post (unversioned . b . path "/access" . cookieRaw "zuid" "xxx") !!! do const 403 === statusCode const (Just "Invalid user token") =~= responseBody -- Expired @@ -717,7 +729,7 @@ testInvalidCookie z b = do let f = set (ZAuth.userTTL (Proxy @u)) 0 t <- toByteString' <$> runZAuth z (ZAuth.localSettings f (ZAuth.newUserToken @u user)) liftIO $ threadDelay 1000000 - post (b . path "/access" . cookieRaw "zuid" t) !!! do + post (unversioned . b . path "/access" . cookieRaw "zuid" t) !!! do const 403 === statusCode const (Just "expired") =~= responseBody @@ -726,9 +738,9 @@ testInvalidCookie z b = do testInvalidToken :: Brig -> Http () testInvalidToken b = do -- Syntactically invalid - post (b . path "/access" . queryItem "access_token" "xxx") + post (unversioned . b . path "/access" . queryItem "access_token" "xxx") !!! errResponse - post (b . path "/access" . header "Authorization" "Bearer xxx") + post (unversioned . b . path "/access" . header "Authorization" "Bearer xxx") !!! errResponse where errResponse = do @@ -738,12 +750,12 @@ testInvalidToken b = do testMissingCookie :: forall u a. ZAuth.TokenPair u a => ZAuth.Env -> Brig -> Http () testMissingCookie z b = do -- Missing cookie, i.e. token refresh mandates a cookie. - post (b . path "/access") + post (unversioned . b . path "/access") !!! errResponse t <- toByteString' <$> runZAuth z (randomAccessToken @u @a) - post (b . path "/access" . header "Authorization" ("Bearer " <> t)) + post (unversioned . b . path "/access" . header "Authorization" ("Bearer " <> t)) !!! errResponse - post (b . path "/access" . queryItem "access_token" t) + post (unversioned . b . path "/access" . queryItem "access_token" t) !!! errResponse where errResponse = do @@ -755,7 +767,7 @@ testUnknownCookie :: forall u. ZAuth.UserTokenLike u => ZAuth.Env -> Brig -> Htt testUnknownCookie z b = do -- Valid cookie but unknown to the server. t <- toByteString' <$> runZAuth z (randomUserToken @u) - post (b . path "/access" . cookieRaw "zuid" t) !!! do + post (unversioned . b . path "/access" . cookieRaw "zuid" t) !!! do const 403 === statusCode const (Just "invalid-credentials") =~= responseBody @@ -769,7 +781,7 @@ testTokenMismatchLegalhold z brig galley = do -- try refresh with a regular UserCookie but a LegalHoldAccessToken let c = decodeCookie _rs t <- toByteString' <$> runZAuth z (randomAccessToken @ZAuth.LegalHoldUser @ZAuth.LegalHoldAccess) - post (brig . path "/access" . cookie c . header "Authorization" ("Bearer " <> t)) !!! do + post (unversioned . brig . path "/access" . cookie c . header "Authorization" ("Bearer " <> t)) !!! do const 403 === statusCode const (Just "Token mismatch") =~= responseBody -- try refresh with a regular AccessToken but a LegalHoldUserCookie @@ -778,69 +790,70 @@ testTokenMismatchLegalhold z brig galley = do _rs <- legalHoldLogin brig (LegalHoldLogin alice (Just defPassword) Nothing) PersistentCookie let c' = decodeCookie _rs t' <- toByteString' <$> runZAuth z (randomAccessToken @ZAuth.User @ZAuth.Access) - post (brig . path "/access" . cookie c' . header "Authorization" ("Bearer " <> t')) !!! do + post (unversioned . brig . path "/access" . cookie c' . header "Authorization" ("Bearer " <> t')) !!! do const 403 === statusCode const (Just "Token mismatch") =~= responseBody -- | This only tests access; the logic is tested in 'testEmailUpdate' in `Account.hs`. -testAccessSelfEmailAllowed :: Nginz -> Brig -> Http () -testAccessSelfEmailAllowed nginz brig = do - -- this test duplicates some of 'initiateEmailUpdateLogin' intentionally. - forM_ [True, False] $ \withCookie -> do - usr <- randomUser brig - let Just email = userEmail usr - (mbCky, tok) <- do - rsp <- - login nginz (emailLogin email defPassword (Just "nexus1")) PersistentCookie - toByteString' tok) - - put (req . Bilge.json ()) - !!! const (if withCookie then 400 else 403) === statusCode - put (req . Bilge.json (EmailUpdate email)) - !!! const (if withCookie then 204 else 403) === statusCode - -testAccessSelfEmailDenied :: ZAuth.Env -> Nginz -> Brig -> Http () -testAccessSelfEmailDenied zenv nginz brig = do - -- this test duplicates some of 'initiateEmailUpdateLogin' intentionally. - forM_ [True, False] $ \withCookie -> do - mbCky <- - if withCookie - then do - usr <- randomUser brig - let Just email = userEmail usr - rsp <- - login nginz (emailLogin email defPassword (Just "nexus1")) PersistentCookie - toByteString' tok)) - !!! errResponse withCookie "invalid-credentials" "Invalid token" +-- this test duplicates some of 'initiateEmailUpdateLogin' intentionally. +testAccessSelfEmailAllowed :: Nginz -> Brig -> Bool -> Http () +testAccessSelfEmailAllowed nginz brig withCookie = do + usr <- randomUser brig + let Just email = userEmail usr + (mbCky, tok) <- do + rsp <- + login nginz (emailLogin email defPassword (Just "nexus1")) PersistentCookie + toByteString' tok) + + put (req . Bilge.json ()) + !!! const (if withCookie then 400 else 403) === statusCode + + put (req . Bilge.json (EmailUpdate email)) + !!! const (if withCookie then 204 else 403) === statusCode + +-- this test duplicates some of 'initiateEmailUpdateLogin' intentionally. +testAccessSelfEmailDenied :: ZAuth.Env -> Nginz -> Brig -> Bool -> Http () +testAccessSelfEmailDenied zenv nginz brig withCookie = do + mbCky <- + if withCookie + then do + usr <- randomUser brig + let Just email = userEmail usr + rsp <- + login nginz (emailLogin email defPassword (Just "nexus1")) PersistentCookie + toByteString' tok)) + !!! errResponse "invalid-credentials" "Invalid token" where - errResponse withCookie label msg = do + errResponse label msg = do const 403 === statusCode when withCookie $ do const (Just label) =~= responseBody @@ -867,7 +880,7 @@ getAndTestDBSupersededCookieAndItsValidSuccessor config b n = do liftIO $ threadDelay minAge -- Refresh tokens _rs <- - post (n . path "/access" . cookie c) do - post (brig . path "/access" . cookie cky) !!! do + post (unversioned . brig . path "/access" . cookie cky) !!! do const 403 === statusCode const Nothing === getHeader "Set-Cookie" "/login" -> do @@ -1098,11 +1111,11 @@ testLogout b = do Just email <- userEmail <$> randomUser b _rs <- login b (defEmailLogin email) SessionCookie let (t, c) = (decodeToken _rs, decodeCookie _rs) - post (b . path "/access" . cookie c) + post (unversioned . b . path "/access" . cookie c) !!! const 200 === statusCode - post (b . path "/access/logout" . cookie c . queryItem "access_token" (toByteString' t)) + post (unversioned . b . path "/access/logout" . cookie c . queryItem "access_token" (toByteString' t)) !!! const 200 === statusCode - post (b . path "/access" . cookie c) + post (unversioned . b . path "/access" . cookie c) !!! const 403 === statusCode testReauthentication :: Brig -> Http () diff --git a/services/brig/test/integration/API/User/Client.hs b/services/brig/test/integration/API/User/Client.hs index 239de959d7..50bf88810e 100644 --- a/services/brig/test/integration/API/User/Client.hs +++ b/services/brig/test/integration/API/User/Client.hs @@ -105,7 +105,7 @@ tests _cl _at opts p db b c g = test p "put /clients/:client - 200 (mls keys)" $ testMLSPublicKeyUpdate b, test p "get /clients/:client - 404" $ testMissingClient b, test p "get /clients/:client - 200" $ testMLSClient b, - test p "post /clients - 200 multiple temporary" $ testAddMultipleTemporary b g, + test p "post /clients - 200 multiple temporary" $ testAddMultipleTemporary b g c, test p "client/prekeys/race" $ testPreKeyRace b, test p "get/head nonce/clients" $ testNewNonce b ] @@ -378,7 +378,8 @@ testListClientsBulk opts brig = do ] ) post - ( brig + ( apiVersion "v1" + . brig . paths ["users", "list-clients"] . zUser uid3 . contentJson @@ -418,7 +419,8 @@ testListClientsBulkV2 opts brig = do ] ) post - ( brig + ( apiVersion "v1" + . brig . paths ["users", "list-clients", "v2"] . zUser uid3 . contentJson @@ -456,12 +458,12 @@ generateClients n brig = do testGetUserPrekeys :: Brig -> Http () testGetUserPrekeys brig = do [(uid, _c, lpk, cpk)] <- generateClients 1 brig - get (brig . paths ["users", toByteString' uid, "prekeys"] . zUser uid) !!! do + get (apiVersion "v1" . brig . paths ["users", toByteString' uid, "prekeys"] . zUser uid) !!! do const 200 === statusCode const (Just $ PrekeyBundle uid [cpk]) === responseJsonMaybe -- prekeys are deleted when retrieved, except the last one replicateM_ 2 $ - get (brig . paths ["users", toByteString' uid, "prekeys"] . zUser uid) !!! do + get (apiVersion "v1" . brig . paths ["users", toByteString' uid, "prekeys"] . zUser uid) !!! do const 200 === statusCode const (Just $ PrekeyBundle uid [lpk]) === responseJsonMaybe @@ -482,7 +484,7 @@ testGetUserPrekeysInvalidDomain brig = do testGetClientPrekey :: Brig -> Http () testGetClientPrekey brig = do [(uid, c, _lpk, cpk)] <- generateClients 1 brig - get (brig . paths ["users", toByteString' uid, "prekeys", toByteString' (clientId c)] . zUser uid) !!! do + get (apiVersion "v1" . brig . paths ["users", toByteString' uid, "prekeys", toByteString' (clientId c)] . zUser uid) !!! do const 200 === statusCode const (Just $ cpk) === responseJsonMaybe @@ -512,7 +514,8 @@ testMultiUserGetPrekeys brig = do uid <- userId <$> randomUser brig post - ( brig + ( apiVersion "v1" + . brig . paths ["users", "prekeys"] . contentJson . body (RequestBodyLBS $ encode userClients) @@ -708,7 +711,7 @@ testUpdateClient opts brig = do newClientModel = Just "featurephone" } c <- responseJsonError =<< addClient brig uid clt - get (brig . paths ["users", toByteString' uid, "prekeys", toByteString' (clientId c)] . zUser uid) !!! do + get (apiVersion "v1" . brig . paths ["users", toByteString' uid, "prekeys", toByteString' (clientId c)] . zUser uid) !!! do const 200 === statusCode const (Just $ ClientPrekey (clientId c) (somePrekeys !! 0)) === responseJsonMaybe getClient brig uid (clientId c) !!! do @@ -731,7 +734,7 @@ testUpdateClient opts brig = do ) !!! const 200 === statusCode - get (brig . paths ["users", toByteString' uid, "prekeys", toByteString' (clientId c)] . zUser uid) !!! do + get (apiVersion "v1" . brig . paths ["users", toByteString' uid, "prekeys", toByteString' (clientId c)] . zUser uid) !!! do const 200 === statusCode const (Just $ ClientPrekey (clientId c) newPrekey) === responseJsonMaybe @@ -741,7 +744,7 @@ testUpdateClient opts brig = do const (Just "label") === (clientLabel <=< responseJsonMaybe) -- via `/users/:uid/clients/:client`, only `id` and `class` are visible: - get (brig . paths ["users", toByteString' uid, "clients", toByteString' (clientId c)]) !!! do + get (apiVersion "v1" . brig . paths ["users", toByteString' uid, "clients", toByteString' (clientId c)]) !!! do const 200 === statusCode const (Just $ clientId c) === (fmap pubClientId . responseJsonMaybe) const (Just PhoneClient) === (pubClientClass <=< responseJsonMaybe) @@ -761,7 +764,8 @@ testUpdateClient opts brig = do -- empty update should be a no-op put - ( brig + ( apiVersion "v1" + . brig . paths ["clients", toByteString' (clientId c)] . zUser uid . contentJson @@ -780,7 +784,8 @@ testUpdateClient opts brig = do checkUpdate capsIn respStatusOk capsOut = do let update'' = defUpdateClient {updateClientCapabilities = Set.fromList <$> capsIn} put - ( brig + ( apiVersion "v1" + . brig . paths ["clients", toByteString' (clientId c)] . zUser uid . contentJson @@ -813,7 +818,7 @@ testUpdateClient opts brig = do flushClientPrekey = do responseJsonMaybe <$> ( get - (brig . paths ["users", toByteString' uid, "prekeys", toByteString' (clientId c)] . zUser uid) + (apiVersion "v1" . brig . paths ["users", toByteString' uid, "prekeys", toByteString' (clientId c)] . zUser uid) Galley -> Http () -testAddMultipleTemporary brig galley = do +testAddMultipleTemporary :: Brig -> Galley -> Cannon -> Http () +testAddMultipleTemporary brig galley cannon = do uid <- userId <$> randomUser brig let clt1 = (defNewClient TemporaryClientType [somePrekeys !! 0] (someLastPrekeys !! 0)) { newClientClass = Just PhoneClient, newClientModel = Just "featurephone1" } - _ <- addClient brig uid clt1 + client <- responseJsonError =<< addClient brig uid clt1 brigClients1 <- numOfBrigClients uid galleyClients1 <- numOfGalleyClients uid liftIO $ assertEqual "Too many clients found" (Just 1) brigClients1 @@ -910,7 +915,14 @@ testAddMultipleTemporary brig galley = do { newClientClass = Just PhoneClient, newClientModel = Just "featurephone2" } - _ <- addClient brig uid clt2 + WS.bracketR cannon uid $ \ws -> do + _ <- addClient brig uid clt2 + void . liftIO . WS.assertMatch (5 # Second) ws $ \n -> do + let j = Object $ List1.head (ntfPayload n) + let etype = j ^? key "type" . _String + let eclient = j ^? key "client" . key "id" . _String + etype @?= Just "user.client-remove" + fmap ClientId eclient @?= Just (clientId client) brigClients2 <- numOfBrigClients uid galleyClients2 <- numOfGalleyClients uid liftIO $ assertEqual "Too many clients found" (Just 1) brigClients2 diff --git a/services/brig/test/integration/API/User/Connection.hs b/services/brig/test/integration/API/User/Connection.hs index 1bd463d0ee..2911dd3aea 100644 --- a/services/brig/test/integration/API/User/Connection.hs +++ b/services/brig/test/integration/API/User/Connection.hs @@ -619,7 +619,7 @@ testLocalConnectionsPaging b = do let count' = count + step let range = queryRange (toByteString' <$> start) (Just step) r <- - get (b . path "/connections" . zUser u . range) + get (apiVersion "v1" . b . path "/connections" . zUser u . range) conns) diff --git a/services/brig/test/integration/API/User/Handles.hs b/services/brig/test/integration/API/User/Handles.hs index 5eb3a44c29..f15c65bb0c 100644 --- a/services/brig/test/integration/API/User/Handles.hs +++ b/services/brig/test/integration/API/User/Handles.hs @@ -176,7 +176,7 @@ testHandleQuery opts brig = do Bilge.head (brig . paths ["users", "handles", toByteString' hdl] . zUser uid) !!! const 200 === statusCode -- Query user profiles by handles - get (brig . path "/users" . queryItem "handles" (toByteString' hdl) . zUser uid) !!! do + get (apiVersion "v1" . brig . path "/users" . queryItem "handles" (toByteString' hdl) . zUser uid) !!! do const 200 === statusCode const (Just (Handle hdl)) === (profileHandle <=< listToMaybe <=< responseJsonMaybe) -- Bulk availability check @@ -241,7 +241,8 @@ testGetUserByUnqualifiedHandle brig = do _ <- putHandle brig (userId user) handle requestingUser <- randomId get - ( brig + ( apiVersion "v1" + . brig . paths ["users", "handles", toByteString' handle] . zUser requestingUser ) @@ -254,7 +255,8 @@ testGetUserByUnqualifiedHandleFailure brig = do handle <- randomHandle requestingUser <- randomId get - ( brig + ( apiVersion "v1" + . brig . paths ["users", "handles", toByteString' handle] . zUser requestingUser ) @@ -272,7 +274,8 @@ testGetUserByQualifiedHandle brig = do profileForUnconnectedUser <- responseJsonError =<< get - ( brig + ( apiVersion "v1" + . brig . paths ["users", "by-handle", toByteString' domain, toByteString' handle] . zUser (userId unconnectedUser) . expect2xx @@ -296,7 +299,8 @@ testGetUserByQualifiedHandleFailure brig = do handle <- randomHandle qself <- userQualifiedId <$> randomUser brig get - ( brig + ( apiVersion "v1" + . brig . paths [ "users", "by-handle", @@ -315,7 +319,8 @@ testGetUserByQualifiedHandleNoFederation opt brig = do someUser <- randomUser brig withSettingsOverrides newOpts $ get - ( brig + ( apiVersion "v1" + . brig . paths ["users", "by-handle", "non-existant.example.com", "oh-a-handle"] . zUser (userId someUser) ) @@ -328,10 +333,10 @@ assertCanFind :: (Monad m, MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) = assertCanFind brig from target = do liftIO $ assertBool "assertCanFind: Target must have a handle set" (isJust $ userHandle target) let targetHandle = fromMaybe (error "Impossible") (userHandle target) - get (brig . path "/users" . queryItem "handles" (toByteString' targetHandle) . zUser (userId from)) !!! do + get (apiVersion "v1" . brig . path "/users" . queryItem "handles" (toByteString' targetHandle) . zUser (userId from)) !!! do const 200 === statusCode const (userHandle target) === (>>= (listToMaybe >=> profileHandle)) . responseJsonMaybe - get (brig . paths ["users", "handles", toByteString' targetHandle] . zUser (userId from)) !!! do + get (apiVersion "v1" . brig . paths ["users", "handles", toByteString' targetHandle] . zUser (userId from)) !!! do const 200 === statusCode const (Just (UserHandleInfo $ userQualifiedId target)) === responseJsonMaybe @@ -339,7 +344,7 @@ assertCannotFind :: (Monad m, MonadCatch m, MonadIO m, MonadHttp m, HasCallStack assertCannotFind brig from target = do liftIO $ assertBool "assertCannotFind: Target must have a handle set" (isJust $ userHandle target) let targetHandle = fromMaybe (error "Impossible") (userHandle target) - get (brig . path "/users" . queryItem "handles" (toByteString' targetHandle) . zUser (userId from)) !!! do + get (apiVersion "v1" . brig . path "/users" . queryItem "handles" (toByteString' targetHandle) . zUser (userId from)) !!! do const 404 === statusCode - get (brig . paths ["users", "handles", toByteString' targetHandle] . zUser (userId from)) !!! do + get (apiVersion "v1" . brig . paths ["users", "handles", toByteString' targetHandle] . zUser (userId from)) !!! do const 404 === statusCode diff --git a/services/brig/test/integration/API/User/Util.hs b/services/brig/test/integration/API/User/Util.hs index 8c23cb39a0..7551ef25c3 100644 --- a/services/brig/test/integration/API/User/Util.hs +++ b/services/brig/test/integration/API/User/Util.hs @@ -22,9 +22,9 @@ module API.User.Util where import Bilge hiding (accept, timeout) import Bilge.Assert import qualified Brig.Code as Code +import Brig.Effects.CodeStore +import Brig.Effects.CodeStore.Cassandra import Brig.Options (Opts) -import Brig.Sem.CodeStore -import Brig.Sem.CodeStore.Cassandra import Brig.Types.Team.LegalHold (LegalHoldClientRequest (..)) import qualified Brig.ZAuth import qualified Cassandra as DB @@ -186,7 +186,8 @@ initiateEmailUpdateLogin brig email loginCreds uid = do initiateEmailUpdateCreds :: Brig -> Email -> (Bilge.Cookie, Brig.ZAuth.AccessToken) -> UserId -> (MonadIO m, MonadCatch m, MonadHttp m) => m ResponseLBS initiateEmailUpdateCreds brig email (cky, tok) uid = do put $ - brig + unversioned + . brig . path "/access/self/email" . cookie cky . header "Authorization" ("Bearer " <> toByteString' tok) @@ -261,7 +262,8 @@ getClientCapabilities brig u c = getUserClientsUnqualified :: Brig -> UserId -> (MonadIO m, MonadHttp m) => m ResponseLBS getUserClientsUnqualified brig uid = get $ - brig + apiVersion "v1" + . brig . paths ["users", toByteString' uid, "clients"] . zUser uid @@ -286,10 +288,11 @@ deleteClient brig u c pw = RequestBodyLBS . encode . object . maybeToList $ fmap ("password" .=) pw -listConnections :: Brig -> UserId -> (MonadIO m, MonadHttp m) => m ResponseLBS +listConnections :: HasCallStack => Brig -> UserId -> (MonadIO m, MonadHttp m) => m ResponseLBS listConnections brig u = get $ - brig + apiVersion "v1" + . brig . path "connections" . zUser u @@ -434,7 +437,7 @@ sendConnectionUpdateAction brig opts uid1 quid2 reaction expectedRel = do assertEmailVisibility :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> User -> User -> Bool -> m () assertEmailVisibility brig a b visible = - get (brig . paths ["users", pack . show $ userId b] . zUser (userId a)) !!! do + get (apiVersion "v1" . brig . paths ["users", pack . show $ userId b] . zUser (userId a)) !!! do const 200 === statusCode if visible then const (Just (userEmail b)) === fmap userEmail . responseJsonMaybe @@ -452,7 +455,7 @@ uploadAsset c usr sts dat = do mpb = buildMultipartBody sts ct (LB.fromStrict dat) post ( c - . path "/assets/v3" + . path "/assets" . zUser usr . zConn "conn" . content "multipart/mixed" @@ -470,7 +473,7 @@ downloadAsset :: downloadAsset c usr ast = get ( c - . paths ["/assets/v4", toByteString' (qDomain ast), toByteString' (qUnqualified ast)] + . paths ["/assets", toByteString' (qDomain ast), toByteString' (qUnqualified ast)] . zUser usr . zConn "conn" ) diff --git a/services/brig/test/integration/API/Version.hs b/services/brig/test/integration/API/Version.hs index 0a0702509c..455925be8c 100644 --- a/services/brig/test/integration/API/Version.hs +++ b/services/brig/test/integration/API/Version.hs @@ -50,7 +50,7 @@ testVersion brig = do testVersionV1 :: Brig -> Http () testVersionV1 brig = do vinfo <- - responseJsonError =<< get (brig . path "/v1/api-version") + responseJsonError =<< get (apiVersion "v1" . brig . path "api-version") Http () testUnsupportedVersion brig = do e <- - responseJsonError =<< get (brig . path "/v500/api-version") + responseJsonError =<< get (apiVersion "v500" . brig . path "api-version") Opts.Opts -> [String] -> IO () runTests iConf brigOpts otherArgs = do - let b = mkRequest $ brig iConf - c = mkRequest $ cannon iConf - gd = mkRequest $ gundeck iConf - ch = mkRequest $ cargohold iConf - g = mkRequest $ galley iConf - n = mkRequest $ nginz iConf - s = mkRequest $ spar iConf + let b = mkVersionedRequest $ brig iConf + c = mkVersionedRequest $ cannon iConf + gd = mkVersionedRequest $ gundeck iConf + ch = mkVersionedRequest $ cargohold iConf + g = mkVersionedRequest $ galley iConf + n = mkVersionedRequest $ nginz iConf + s = mkVersionedRequest $ spar iConf f = federatorInternal iConf - brigTwo = mkRequest $ remoteBrig (backendTwo iConf) - cannonTwo = mkRequest $ remoteCannon (backendTwo iConf) - galleyTwo = mkRequest $ remoteGalley (backendTwo iConf) - ch2 = mkRequest $ remoteCargohold (backendTwo iConf) + brigTwo = mkVersionedRequest $ remoteBrig (backendTwo iConf) + cannonTwo = mkVersionedRequest $ remoteCannon (backendTwo iConf) + galleyTwo = mkVersionedRequest $ remoteGalley (backendTwo iConf) + ch2 = mkVersionedRequest $ remoteCargohold (backendTwo iConf) let Opts.TurnServersFiles turnFile turnFileV2 = case Opts.serversSource $ Opts.turn brigOpts of Opts.TurnSourceFiles files -> files @@ -178,6 +182,17 @@ runTests iConf brigOpts otherArgs = do where mkRequest (Endpoint h p) = host (encodeUtf8 h) . port p + mkVersionedRequest endpoint = addPrefix . mkRequest endpoint + + addPrefix :: Request -> Request + addPrefix r = r {HTTP.path = "v" <> toHeader latestVersion <> "/" <> removeSlash (HTTP.path r)} + where + removeSlash s = case B8.uncons s of + Just ('/', s') -> s' + _ -> s + latestVersion :: Version + latestVersion = maxBound + parseEmailAWSOpts :: IO (Maybe Opts.EmailAWSOpts) parseEmailAWSOpts = case Opts.email . Opts.emailSMS $ brigOpts of (Opts.EmailAWS aws) -> pure (Just aws) diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index 42db733b75..bcb0e9031a 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -40,6 +40,7 @@ import Control.Exception (throw) import Control.Lens ((^.), (^?), (^?!)) import Control.Monad.Catch (MonadCatch, MonadMask) import qualified Control.Monad.Catch as Catch +import qualified Control.Monad.State as State import Control.Monad.State.Class (MonadState) import qualified Control.Monad.State.Class as MonadState import Control.Monad.Trans.Except (ExceptT (ExceptT), runExceptT) @@ -50,7 +51,7 @@ import qualified Data.Aeson.Types as Aeson import qualified Data.ByteString as BS import Data.ByteString.Builder (toLazyByteString) import Data.ByteString.Char8 (pack) -import qualified Data.ByteString.Char8 as C8 +import qualified Data.ByteString.Char8 as B8 import Data.ByteString.Conversion import Data.Domain (Domain (..), domainText, mkDomain) import Data.Handle (Handle (..)) @@ -59,7 +60,7 @@ import Data.List1 (List1) import qualified Data.List1 as List1 import Data.Misc (PlainTextPassword (..)) import Data.Proxy -import Data.Qualified +import Data.Qualified hiding (isLocal) import Data.Range import qualified Data.Sequence as Seq import Data.String.Conversions (cs) @@ -135,6 +136,38 @@ type Spar = Request -> Request data FedClient (comp :: Component) = FedClient HTTP.Manager Endpoint +-- | Note: Apply this function last when composing (Request -> Request) functions +apiVersion :: ByteString -> Request -> Request +apiVersion newVersion r = r {HTTP.path = setVersion newVersion (HTTP.path r)} + where + setVersion :: ByteString -> ByteString -> ByteString + setVersion v p = + let p' = removeSlash' p + in v <> "/" <> fromMaybe p' (removeVersionPrefix p') + +removeSlash' :: ByteString -> ByteString +removeSlash' s = case B8.uncons s of + Just ('/', s') -> s' + _ -> s + +removeVersionPrefix :: ByteString -> Maybe ByteString +removeVersionPrefix bs = do + let (x, s) = B8.splitAt 1 bs + guard (x == B8.pack "v") + (_, s') <- B8.readInteger s + pure (B8.tail s') + +-- | Note: Apply this function last when composing (Request -> Request) functions +unversioned :: Request -> Request +unversioned r = + r + { HTTP.path = + maybe + (HTTP.path r) + (B8.pack "/" <>) + (removeVersionPrefix . removeSlash' $ HTTP.path r) + } + runFedClient :: forall (name :: Symbol) comp api. ( HasFedEndpoint comp api name, @@ -317,7 +350,7 @@ getPhoneLoginCode brig p = do let lbs = fromMaybe "" $ responseBody r pure (LoginCode <$> (lbs ^? key "code" . _String)) -assertUpdateNotification :: WS.WebSocket -> UserId -> UserUpdate -> IO Notification +assertUpdateNotification :: WS.WebSocket -> UserId -> UserUpdate -> IO () assertUpdateNotification ws uid upd = WS.assertMatch (5 # Second) ws $ \n -> do let j = Object $ List1.head (ntfPayload n) j ^? key "type" . _String @?= Just "user.update" @@ -333,7 +366,8 @@ assertUpdateNotification ws uid upd = WS.assertMatch (5 # Second) ws $ \n -> do getConnection :: Brig -> UserId -> UserId -> Http ResponseLBS getConnection brig from to = get $ - brig + apiVersion "v1" + . brig . paths ["/connections", toByteString' to] . zUser from . zConn "conn" @@ -439,7 +473,8 @@ getSelfProfile brig usr = do getUser :: Brig -> UserId -> UserId -> Http ResponseLBS getUser brig zusr usr = get $ - brig + apiVersion "v1" + . brig . paths ["users", toByteString' usr] . zUser zusr @@ -450,7 +485,8 @@ login :: Brig -> Login -> CookieType -> (MonadIO m, MonadHttp m) => m ResponseLB login b l t = let js = RequestBodyLBS (encode l) in post $ - b + unversioned + . b . path "/login" . contentJson . (if t == PersistentCookie then queryItem "persist" "true" else id) @@ -510,7 +546,8 @@ sendLoginCode b p typ force = postConnection :: Brig -> UserId -> UserId -> (MonadIO m, MonadHttp m) => m ResponseLBS postConnection brig from to = post $ - brig + apiVersion "v1" + . brig . path "/connections" . contentJson . body payload @@ -533,7 +570,8 @@ postConnectionQualified brig from (Qualified toUser toDomain) = putConnection :: Brig -> UserId -> UserId -> Relation -> (MonadIO m, MonadHttp m) => m ResponseLBS putConnection brig from to r = put $ - brig + apiVersion "v1" + . brig . paths ["/connections", toByteString' to] . contentJson . body payload @@ -602,7 +640,8 @@ getUserInfoFromHandle brig domain handle = do u <- randomId responseJsonError =<< get - ( brig + ( apiVersion "v1" + . brig . paths ["users", "by-handle", toByteString' (domainText domain), toByteString' handle] . zUser u . expect2xx @@ -649,7 +688,8 @@ defNewClientWithVerificationCode mbCode ty pks lpk = getPreKey :: Brig -> UserId -> UserId -> ClientId -> Http ResponseLBS getPreKey brig zusr u c = get $ - brig + apiVersion "v1" + . brig . paths ["users", toByteString' u, "prekeys", toByteString' c] . zUser zusr @@ -744,7 +784,8 @@ listConvs :: m ResponseLBS listConvs galley zusr convs = do post $ - galley + apiVersion "v1" + . galley . path "/conversations/list/v2" . zUser zusr . zConn "conn" @@ -803,7 +844,7 @@ zAuthAccess :: UserId -> ByteString -> Request -> Request zAuthAccess u c = header "Z-Type" "access" . zUser u . zConn c zUser :: UserId -> Request -> Request -zUser = header "Z-User" . C8.pack . show +zUser = header "Z-User" . B8.pack . show zConn :: ByteString -> Request -> Request zConn = header "Z-Connection" @@ -893,7 +934,7 @@ somePrekeys = Prekey (PrekeyId 23) "pQABARcCoQBYIASE94LjK6Raipk/lN/YewouqO+kcQGpxIqP+iW2hyHiA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=", Prekey (PrekeyId 24) "pQABARgYAqEAWCBZ222LpS6/99Btlw+83PihrA655skwsNevt//8oz5axQOhAKEAWCCy39UyMEgetquvTo7P19bcyfnWBzQMOEG1v+0wub0magT2", Prekey (PrekeyId 25) "pQABARgZAqEAWCDGEwo61w4O8T8lyw0HdoOjGWBKQUNqo6+jSfrPR9alrAOhAKEAWCCy39UyMEgetquvTo7P19bcyfnWBzQMOEG1v+0wub0magT2", - Prekey (PrekeyId 26) "pQABARgaAqEAWCBMSQoQ6B35plC80i1O3AWlJSftCEbCbju97Iykg5+NWQOhAKEAWCCy39UyMEgetquvTo7P19bcyfnWBzQMOEG1v+0wub0magT2" + Prekey (PrekeyId 26) "pQABARgaAqEAWCBMSQoQ6B35plB80i1O3AWlJSftCEbCbju97Iykg5+NWQOhAKEAWCCy39UyMEgetquvTo7P19bcyfnWBzQMOEG1v+0wub0magT2" ] -- | The client ID of the first of 'someLastPrekeys' @@ -1048,6 +1089,10 @@ aFewTimes (\_ -> pure . not . good) (const action) +-- see also: `aFewTimes`. we should really clean this up. +eventually :: (MonadIO m, MonadMask m) => m a -> m a +eventually = recovering (limitRetries 3 <> exponentialBackoff 100000) [] . const + assertOne :: (HasCallStack, MonadIO m, Show a) => [a] -> m a assertOne [a] = pure a assertOne xs = liftIO . assertFailure $ "Expected exactly one element, found " <> show xs @@ -1234,7 +1279,7 @@ fromServantRequest domain r = <> headers <> [(originDomainHeaderName, T.encodeUtf8 (domainText domain))], Wai.isSecure = True, - Wai.pathInfo = filter (not . T.null) (map Data.String.Conversions.cs (C8.split '/' pathBS)), + Wai.pathInfo = filter (not . T.null) (map Data.String.Conversions.cs (B8.split '/' pathBS)), Wai.queryString = toList (Servant.requestQueryString r) } in WaiTest.SRequest req (cs bodyBS) diff --git a/services/cannon/src/Cannon/Run.hs b/services/cannon/src/Cannon/Run.hs index 635e414a9f..c5a30103ab 100644 --- a/services/cannon/src/Cannon/Run.hs +++ b/services/cannon/src/Cannon/Run.hs @@ -91,6 +91,10 @@ run o = do void $ installHandler sigTERM (signalHandler (env e) tid) Nothing void $ installHandler sigINT (signalHandler (env e) tid) Nothing runSettings s app `finally` do + -- FUTUREWORK(@akshaymankar, @fisx): we may want to call `runSettingsWithShutdown` here, + -- but it's a sensitive change, and it looks like this is closing all the websockets at + -- the same time and then calling the drain script. I suspect this might be due to some + -- cleanup in wai. this needs to be tested very carefully when touched. Async.cancel refreshMetricsThread L.close (applog e) where diff --git a/services/cargohold/cargohold.cabal b/services/cargohold/cargohold.cabal index f82b665ae3..74ebc915c5 100644 --- a/services/cargohold/cargohold.cabal +++ b/services/cargohold/cargohold.cabal @@ -290,6 +290,7 @@ executable cargohold-integration , extended , federator , HsOpenSSL >=0.11 + , http-api-data , http-client >=0.4 , http-client-tls >=0.2 , http-media diff --git a/services/cargohold/src/CargoHold/Run.hs b/services/cargohold/src/CargoHold/Run.hs index 7f0b3d73d1..09677b898e 100644 --- a/services/cargohold/src/CargoHold/Run.hs +++ b/services/cargohold/src/CargoHold/Run.hs @@ -69,7 +69,7 @@ run o = lowerCodensity $ do (o ^. optCargohold . epPort) (e ^. appLogger) (e ^. metrics) - runSettingsWithShutdown s app 5 + runSettingsWithShutdown s app Nothing mkApp :: Opts -> Codensity IO (Application, Env) mkApp o = Codensity $ \k -> diff --git a/services/cargohold/test/integration/API/Util.hs b/services/cargohold/test/integration/API/Util.hs index c0609d8660..c98851fdd4 100644 --- a/services/cargohold/test/integration/API/Util.hs +++ b/services/cargohold/test/integration/API/Util.hs @@ -64,9 +64,11 @@ uploadRaw :: Lazy.ByteString -> TestM (Response (Maybe Lazy.ByteString)) uploadRaw c usr bs = do - cargohold <- viewCargohold + cargohold <- viewUnversionedCargohold post $ - c . cargohold + apiVersion "v1" + . c + . cargohold . method POST . zUser usr . zConn "conn" @@ -90,8 +92,8 @@ zConn = header "Z-Connection" deleteAssetV3 :: UserId -> Qualified AssetKey -> TestM (Response (Maybe Lazy.ByteString)) deleteAssetV3 u k = do - c <- viewCargohold - delete $ c . zUser u . paths ["assets", "v3", toByteString' (qUnqualified k)] + c <- viewUnversionedCargohold + delete $ apiVersion "v1" . c . zUser u . paths ["assets", "v3", toByteString' (qUnqualified k)] deleteAsset :: UserId -> Qualified AssetKey -> TestM (Response (Maybe Lazy.ByteString)) deleteAsset u k = do @@ -100,7 +102,6 @@ deleteAsset u k = do c . zUser u . paths [ "assets", - "v4", toByteString' (qDomain k), toByteString' (qUnqualified k) ] @@ -109,10 +110,14 @@ class IsAssetLocation key where locationPath :: key -> Request -> Request instance IsAssetLocation AssetKey where - locationPath k = paths ["assets", "v3", toByteString' k] + locationPath k = + apiVersion "v1" + . paths ["assets", "v3", toByteString' k] instance IsAssetLocation (Qualified AssetKey) where - locationPath k = paths ["assets", "v4", toByteString' (qDomain k), toByteString' (qUnqualified k)] + locationPath k = + apiVersion "v2" + . paths ["assets", toByteString' (qDomain k), toByteString' (qUnqualified k)] instance IsAssetLocation ByteString where locationPath = path @@ -137,7 +142,7 @@ downloadAssetWith :: tok -> TestM (Response (Maybe LByteString)) downloadAssetWith r uid loc tok = do - c <- viewCargohold + c <- viewUnversionedCargohold get $ c . r . zUser uid @@ -158,14 +163,14 @@ postToken uid key = do c <- viewCargohold post $ c . zUser uid - . paths ["assets", "v3", toByteString' key, "token"] + . paths ["assets", toByteString' key, "token"] deleteToken :: UserId -> AssetKey -> TestM (Response (Maybe LByteString)) deleteToken uid key = do c <- viewCargohold delete $ c . zUser uid - . paths ["assets", "v3", toByteString' key, "token"] + . paths ["assets", toByteString' key, "token"] viewFederationDomain :: TestM Domain viewFederationDomain = view (tsOpts . optSettings . setFederationDomain) diff --git a/services/cargohold/test/integration/TestSetup.hs b/services/cargohold/test/integration/TestSetup.hs index cd92d677f9..af4eb7d667 100644 --- a/services/cargohold/test/integration/TestSetup.hs +++ b/services/cargohold/test/integration/TestSetup.hs @@ -26,11 +26,14 @@ module TestSetup Cargohold, TestM, runTestM, + viewUnversionedCargohold, viewCargohold, createTestSetup, runFederationClient, withFederationClient, withFederationError, + apiVersion, + unversioned, ) where @@ -42,12 +45,14 @@ import Control.Monad.Codensity import Control.Monad.Except import Control.Monad.Morph import qualified Data.Aeson as Aeson +import qualified Data.ByteString.Char8 as B8 import Data.ByteString.Conversion import qualified Data.Text as T import Data.Text.Encoding import Data.Yaml import Imports import Network.HTTP.Client hiding (responseBody) +import qualified Network.HTTP.Client as HTTP import Network.HTTP.Client.TLS import qualified Network.Wai.Utilities.Error as Wai import Servant.Client.Streaming @@ -56,7 +61,9 @@ import Test.Tasty.HUnit import Util.Options import Util.Options.Common import Util.Test +import Web.HttpApiData import Wire.API.Federation.Domain +import Wire.API.Routes.Version type Cargohold = Request -> Request @@ -73,8 +80,49 @@ data TestSetup = TestSetup makeLenses ''TestSetup +-- | Note: Apply this function last when composing (Request -> Request) functions +apiVersion :: ByteString -> Request -> Request +apiVersion newVersion r = r {HTTP.path = setVersion newVersion (HTTP.path r)} + where + setVersion :: ByteString -> ByteString -> ByteString + setVersion v p = + let p' = removeSlash' p + in v <> "/" <> fromMaybe p' (removeVersionPrefix p') + +removeSlash' :: ByteString -> ByteString +removeSlash' s = case B8.uncons s of + Just ('/', s') -> s' + _ -> s + +removeVersionPrefix :: ByteString -> Maybe ByteString +removeVersionPrefix bs = do + let (x, s) = B8.splitAt 1 bs + guard (x == B8.pack "v") + (_, s') <- B8.readInteger s + pure (B8.tail s') + +-- | Note: Apply this function last when composing (Request -> Request) functions +unversioned :: Request -> Request +unversioned r = + r + { HTTP.path = + maybe + (HTTP.path r) + (B8.pack "/" <>) + (removeVersionPrefix . removeSlash' $ HTTP.path r) + } + viewCargohold :: TestM Cargohold -viewCargohold = mkRequest <$> view tsEndpoint +viewCargohold = + fmap + (apiVersion ("v" <> toHeader latestVersion) .) + viewUnversionedCargohold + where + latestVersion :: Version + latestVersion = maxBound + +viewUnversionedCargohold :: TestM Cargohold +viewUnversionedCargohold = mkRequest <$> view tsEndpoint runTestM :: TestSetup -> TestM a -> IO a runTestM ts action = runHttpT (view tsManager ts) (runReaderT action ts) diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index f10581474e..6e3529e497 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -31,9 +31,14 @@ library Galley.API.Mapping Galley.API.Message Galley.API.MLS + Galley.API.MLS.GroupInfo Galley.API.MLS.KeyPackage Galley.API.MLS.Keys Galley.API.MLS.Message + Galley.API.MLS.Propagate + Galley.API.MLS.Removal + Galley.API.MLS.Types + Galley.API.MLS.Util Galley.API.MLS.Welcome Galley.API.One2One Galley.API.Public @@ -203,6 +208,7 @@ library , extra >=1.3 , galley-types >=0.65.0 , gundeck-types >=1.35.2 + , hex , HsOpenSSL >=0.11 , HsOpenSSL-x509-system >=0.1 , http-client >=0.4 @@ -442,6 +448,7 @@ executable galley-integration , currency-codes , data-default , data-timeout + , directory , errors , exceptions , extended @@ -451,6 +458,7 @@ executable galley-integration , galley , galley-types , gundeck-types + , hex , HsOpenSSL , HsOpenSSL-x509-system , hspec @@ -500,6 +508,7 @@ executable galley-integration , transformers , types-common , types-common-journal + , unix , unliftio , unordered-containers , uri-bytestring @@ -660,6 +669,11 @@ executable galley-schema V68_MLSCommitLock V69_MLSProposal V70_MLSCipherSuite + V71_MemberClientKeypackage + V72_DropManagedConversations + V73_MemberClientTable + V74_ExposeInvitationsToTeamAdmin + V75_MLSGroupInfo hs-source-dirs: schema/src default-extensions: diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index be7d1fe7d6..a7759cad19 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -29,6 +29,7 @@ settings: httpPoolSize: 128 maxTeamSize: 32 maxFanoutSize: 18 + exposeInvitationURLsTeamAllowlist: [] maxConvSize: 16 intraListing: false conversationCodeURI: https://account.wire.com/conversation-join/ diff --git a/services/galley/schema/src/Main.hs b/services/galley/schema/src/Main.hs index 1d26cc7a89..1c987650cc 100644 --- a/services/galley/schema/src/Main.hs +++ b/services/galley/schema/src/Main.hs @@ -73,6 +73,11 @@ import qualified V67_MLSFeature import qualified V68_MLSCommitLock import qualified V69_MLSProposal import qualified V70_MLSCipherSuite +import qualified V71_MemberClientKeypackage +import qualified V72_DropManagedConversations +import qualified V73_MemberClientTable +import qualified V74_ExposeInvitationsToTeamAdmin +import qualified V75_MLSGroupInfo main :: IO () main = do @@ -131,7 +136,12 @@ main = do V67_MLSFeature.migration, V68_MLSCommitLock.migration, V69_MLSProposal.migration, - V70_MLSCipherSuite.migration + V70_MLSCipherSuite.migration, + V71_MemberClientKeypackage.migration, + V72_DropManagedConversations.migration, + V73_MemberClientTable.migration, + V74_ExposeInvitationsToTeamAdmin.migration, + V75_MLSGroupInfo.migration -- When adding migrations here, don't forget to update -- 'schemaVersion' in Galley.Cassandra -- (see also docs/developer/cassandra-interaction.md) diff --git a/services/galley/schema/src/V71_MemberClientKeypackage.hs b/services/galley/schema/src/V71_MemberClientKeypackage.hs new file mode 100644 index 0000000000..1695957905 --- /dev/null +++ b/services/galley/schema/src/V71_MemberClientKeypackage.hs @@ -0,0 +1,50 @@ +-- 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 V71_MemberClientKeypackage where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + Migration 71 "Replace mls_clients with mls_clients_keypackages in member table" $ do + schema' + [r| + ALTER TABLE member ADD ( + mls_clients_keypackages set>> + ); + |] + schema' + [r| + ALTER TABLE member DROP ( + mls_clients + ); + |] + schema' + [r| + ALTER TABLE member_remote_user ADD ( + mls_clients_keypackages set>> + ); + |] + schema' + [r| + ALTER TABLE member_remote_user DROP ( + mls_clients + ); + |] diff --git a/services/galley/schema/src/V72_DropManagedConversations.hs b/services/galley/schema/src/V72_DropManagedConversations.hs new file mode 100644 index 0000000000..acb633fe5e --- /dev/null +++ b/services/galley/schema/src/V72_DropManagedConversations.hs @@ -0,0 +1,30 @@ +-- 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 V72_DropManagedConversations where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = Migration 72 "Drop the managed column from team_conv" $ do + schema' + [r| ALTER TABLE team_conv DROP ( + managed + ); + |] diff --git a/services/galley/schema/src/V73_MemberClientTable.hs b/services/galley/schema/src/V73_MemberClientTable.hs new file mode 100644 index 0000000000..15f642018b --- /dev/null +++ b/services/galley/schema/src/V73_MemberClientTable.hs @@ -0,0 +1,49 @@ +-- 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 V73_MemberClientTable where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + Migration 73 "Move mls_clients_keypackages to its own table" $ do + schema' + [r| + CREATE TABLE member_client ( + conv uuid, + user_domain text, + user uuid, + client text, + key_package_ref blob, + PRIMARY KEY (conv, user_domain, user, client) + ); + |] + schema' + [r| + ALTER TABLE member DROP ( + mls_clients_keypackages + ); + |] + schema' + [r| + ALTER TABLE member_remote_user DROP ( + mls_clients_keypackages + ); + |] diff --git a/services/galley/schema/src/V74_ExposeInvitationsToTeamAdmin.hs b/services/galley/schema/src/V74_ExposeInvitationsToTeamAdmin.hs new file mode 100644 index 0000000000..f3df5766d9 --- /dev/null +++ b/services/galley/schema/src/V74_ExposeInvitationsToTeamAdmin.hs @@ -0,0 +1,33 @@ +-- 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 V74_ExposeInvitationsToTeamAdmin + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = Migration 74 "Add feature config for team feature exposing invitation URLs to team admins" $ do + schema' + [r| ALTER TABLE team_features ADD ( + expose_invitation_urls_to_team_admin int + ) + |] diff --git a/services/galley/schema/src/V75_MLSGroupInfo.hs b/services/galley/schema/src/V75_MLSGroupInfo.hs new file mode 100644 index 0000000000..4615c73954 --- /dev/null +++ b/services/galley/schema/src/V75_MLSGroupInfo.hs @@ -0,0 +1,34 @@ +-- 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 V75_MLSGroupInfo + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + Migration 75 "Add the MLS public group state column to the conversation table" $ + schema' + [r| ALTER TABLE conversation ADD ( + public_group_state blob + ) + |] diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index d3201e1c5e..dcae8353e8 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -51,7 +51,9 @@ import qualified Data.Set as Set import Data.Singletons import Data.Time.Clock import Galley.API.Error +import Galley.API.MLS.Removal import Galley.API.Util +import Galley.App import Galley.Data.Conversation import qualified Galley.Data.Conversation as Data import Galley.Data.Services @@ -64,6 +66,7 @@ import qualified Galley.Effects.ConversationStore as E import qualified Galley.Effects.FederatorAccess as E import qualified Galley.Effects.FireAndForget as E import qualified Galley.Effects.MemberStore as E +import Galley.Effects.ProposalStore import qualified Galley.Effects.TeamStore as E import Galley.Options import Galley.Types.Conversations.Members @@ -73,6 +76,7 @@ import Imports import Polysemy import Polysemy.Error import Polysemy.Input +import Polysemy.TinyLog import qualified Polysemy.TinyLog as P import qualified System.Logger as Log import Wire.API.Conversation hiding (Conversation, Member) @@ -96,6 +100,7 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con Members '[ BrigAccess, Error FederationError, + Error InternalError, ErrorS 'NotATeamMember, ErrorS 'NotConnected, ErrorS ('ActionDenied 'LeaveConversation), @@ -108,17 +113,33 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con ExternalAccess, FederatorAccess, GundeckAccess, + Input Env, Input Opts, Input UTCTime, LegalHoldStore, MemberStore, + ProposalStore, TeamStore, + TinyLog, ConversationStore, Error NoChanges ] r HasConversationActionEffects 'ConversationLeaveTag r = - (Members '[MemberStore, Error NoChanges] r) + ( Members + '[ MemberStore, + Error InternalError, + Error NoChanges, + ExternalAccess, + FederatorAccess, + GundeckAccess, + Input UTCTime, + Input Env, + ProposalStore, + TinyLog + ] + r + ) HasConversationActionEffects 'ConversationRemoveMembersTag r = (Members '[MemberStore, Error NoChanges] r) HasConversationActionEffects 'ConversationMemberUpdateTag r = @@ -132,6 +153,7 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con '[ BotAccess, BrigAccess, CodeStore, + Error InternalError, Error InvalidInput, Error NoChanges, ErrorS 'InvalidTargetAccess, @@ -140,8 +162,11 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con FederatorAccess, FireAndForget, GundeckAccess, + Input Env, MemberStore, + ProposalStore, TeamStore, + TinyLog, Input UTCTime, ConversationStore ] @@ -264,10 +289,28 @@ performAction tag origUser lconv action = do SConversationJoinTag -> do performConversationJoin origUser lconv action SConversationLeaveTag -> do - let presentVictims = filter (isConvMemberL lconv) (toList action) - when (null presentVictims) noChanges - E.deleteMembers (tUnqualified lcnv) (toUserList lconv presentVictims) - pure (mempty, action) -- FUTUREWORK: should we return the filtered action here? + let victims = [origUser] + E.deleteMembers (tUnqualified lcnv) (toUserList lconv victims) + -- update in-memory view of the conversation + let lconv' = + lconv <&> \c -> + foldQualified + lconv + ( \lu -> + c + { convLocalMembers = + filter (\lm -> lmId lm /= tUnqualified lu) (convLocalMembers c) + } + ) + ( \ru -> + c + { convRemoteMembers = + filter (\rm -> rmId rm /= ru) (convRemoteMembers c) + } + ) + origUser + traverse_ (removeUser lconv') victims + pure (mempty, action) SConversationRemoveMembersTag -> do let presentVictims = filter (isConvMemberL lconv) (toList action) when (null presentVictims) noChanges @@ -368,6 +411,7 @@ performConversationJoin qusr lconv (ConversationJoin invited role) = do checkLHPolicyConflictsLocal :: Members '[ ConversationStore, + Error InternalError, ErrorS ('ActionDenied 'LeaveConversation), ErrorS 'InvalidOperation, ErrorS 'ConvNotFound, @@ -375,11 +419,14 @@ performConversationJoin qusr lconv (ConversationJoin invited role) = do ExternalAccess, FederatorAccess, GundeckAccess, + Input Env, Input Opts, Input UTCTime, LegalHoldStore, MemberStore, - TeamStore + ProposalStore, + TeamStore, + TinyLog ] r => [UserId] -> @@ -410,14 +457,11 @@ performConversationJoin qusr lconv (ConversationJoin invited role) = do then do for_ convUsersLHStatus $ \(mem, status) -> when (consentGiven status == ConsentNotGiven) $ do - let lvictim = qualifyAs lconv (lmId mem) - void . runError @NoChanges $ - updateLocalConversation - @'ConversationLeaveTag - (fmap convId lconv) - (qUntagged lvictim) - Nothing - $ pure (qUntagged lvictim) + kickMember + qusr + lconv + (convBotsAndMembers (tUnqualified lconv)) + (qUntagged (qualifyAs lconv (lmId mem))) else throwS @'MissingLegalholdConsent checkLHPolicyConflictsRemote :: @@ -460,9 +504,9 @@ performConversationAccessData qusr lconv action = do let bmToNotify = current {bmBots = bmBots desired} -- Remove users and notify everyone - void . for_ (nonEmpty (bmQualifiedMembers lcnv toRemove)) $ \usersToRemove -> do - void . runError @NoChanges $ performAction SConversationLeaveTag qusr lconv usersToRemove - notifyConversationAction (sing @'ConversationLeaveTag) qusr Nothing lconv bmToNotify usersToRemove + for_ (bmQualifiedMembers lcnv toRemove) $ + kickMember qusr lconv bmToNotify + pure (mempty, action) where lcnv = fmap convId lconv @@ -519,6 +563,7 @@ updateLocalConversation :: ExternalAccess, FederatorAccess, GundeckAccess, + Input Env, Input UTCTime ] r, @@ -584,6 +629,7 @@ updateLocalConversationUnchecked lconv qusr con action = do notifyConversationAction (sing @tag) qusr + False con lconv (convBotsAndMembers conv <> extraTargets) @@ -638,12 +684,13 @@ notifyConversationAction :: Members '[FederatorAccess, ExternalAccess, GundeckAccess, Input UTCTime] r => Sing tag -> Qualified UserId -> + Bool -> Maybe ConnId -> Local Conversation -> BotsAndMembers -> ConversationAction (tag :: ConversationActionTag) -> Sem r LocalConversationUpdate -notifyConversationAction tag quid con lconv targets action = do +notifyConversationAction tag quid notifyOrigDomain con lconv targets action = do now <- input let lcnv = fmap convId lconv conv = tUnqualified lconv @@ -675,12 +722,12 @@ notifyConversationAction tag quid con lconv targets action = do . E.runFederatedConcurrently (toList (bmRemotes targets)) $ \ruids -> do let update = mkUpdate (tUnqualified ruids) - -- filter out user from quid's domain, because quid's backend will update - -- local state and notify its users itself using the ConversationUpdate - -- returned by this function - if tDomain ruids == qDomain quid - then pure (Just update) - else fedClient @'Galley @"on-conversation-updated" update $> Nothing + -- if notifyOrigDomain is false, filter out user from quid's domain, + -- because quid's backend will update local state and notify its users + -- itself using the ConversationUpdate returned by this function + if notifyOrigDomain || tDomain ruids /= qDomain quid + then fedClient @'Galley @"on-conversation-updated" update $> Nothing + else pure (Just update) -- notify local participants and bots pushConversationEvent con e (qualifyAs lcnv (bmLocals targets)) (bmBots targets) @@ -735,3 +782,40 @@ notifyRemoteConversationAction loc rconvUpdate con = do let bots = [] pushConversationEvent con event localPresentUsers bots $> event + +-- | Kick a user from a conversation and send notifications. +-- +-- This function removes the given victim from the conversation by making them +-- leave, but then sends notifications as if the user was removed by someone +-- else. +kickMember :: + ( Member (Error InternalError) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member ProposalStore r, + Member (Input UTCTime) r, + Member (Input Env) r, + Member MemberStore r, + Member TinyLog r + ) => + Qualified UserId -> + Local Conversation -> + BotsAndMembers -> + Qualified UserId -> + Sem r () +kickMember qusr lconv targets victim = void . runError @NoChanges $ do + (extraTargets, _) <- + performAction + SConversationLeaveTag + victim + lconv + () + notifyConversationAction + (sing @'ConversationRemoveMembersTag) + qusr + True + Nothing + lconv + (targets <> extraTargets) + (pure victim) diff --git a/services/galley/src/Galley/API/Clients.hs b/services/galley/src/Galley/API/Clients.hs index c4d4859a38..95bf989479 100644 --- a/services/galley/src/Galley/API/Clients.hs +++ b/services/galley/src/Galley/API/Clients.hs @@ -22,16 +22,41 @@ module Galley.API.Clients ) where +import Data.Either.Combinators (whenLeft) +import Data.Hex import Data.Id +import Data.Proxy +import Data.Qualified +import Data.Range +import Data.String.Conversions +import qualified Data.Text as T +import Data.Time +import Galley.API.Error +import Galley.API.MLS.Removal +import qualified Galley.API.Query as Query +import Galley.API.Util import Galley.Effects import qualified Galley.Effects.BrigAccess as E import qualified Galley.Effects.ClientStore as E +import Galley.Effects.ConversationStore (getConversation) +import Galley.Effects.FederatorAccess +import Galley.Effects.ProposalStore (ProposalStore) +import Galley.Env import Galley.Types.Clients (clientIds, fromUserClients) import Imports import Network.Wai -import Network.Wai.Predicate hiding (setStatus) -import Network.Wai.Utilities +import Network.Wai.Predicate hiding (Error, setStatus) +import Network.Wai.Utilities hiding (Error) import Polysemy +import Polysemy.Error +import Polysemy.Input +import qualified Polysemy.TinyLog as P +import qualified System.Logger as Log +import Wire.API.Conversation hiding (Member) +import Wire.API.Federation.API +import Wire.API.Federation.API.Galley (ClientRemovedRequest (ClientRemovedRequest)) +import Wire.API.Routes.MultiTablePaging +import Wire.Sem.Paging.Cassandra (CassandraPaging) getClientsH :: Members '[BrigAccess, ClientStore] r => @@ -61,9 +86,68 @@ addClientH (usr ::: clt) = do pure empty rmClientH :: - Member ClientStore r => + forall p1 r. + ( p1 ~ CassandraPaging, + Members + '[ ClientStore, + ConversationStore, + ExternalAccess, + FederatorAccess, + GundeckAccess, + Input Env, + Input (Local ()), + Input UTCTime, + ListItems p1 ConvId, + ListItems p1 (Remote ConvId), + MemberStore, + Error InternalError, + ProposalStore, + P.TinyLog + ] + r + ) => UserId ::: ClientId -> Sem r Response -rmClientH (usr ::: clt) = do - E.deleteClient usr clt +rmClientH (usr ::: cid) = do + lusr <- qualifyLocal usr + let nRange1000 = toRange (Proxy @1000) :: Range 1 1000 Int32 + firstConvIds <- Query.conversationIdsPageFrom lusr (GetPaginatedConversationIds Nothing nRange1000) + goConvs nRange1000 firstConvIds lusr + + E.deleteClient usr cid pure empty + where + rpc = fedClient @'Galley @"on-client-removed" + goConvs :: Range 1 1000 Int32 -> ConvIdsPage -> Local UserId -> Sem r () + goConvs range page lusr = do + let (localConvs, remoteConvs) = partitionQualified lusr (mtpResults page) + for_ localConvs $ \convId -> do + mConv <- getConversation convId + for_ mConv $ \conv -> do + lconv <- qualifyLocal conv + removeClient lconv (qUntagged lusr) cid + traverse_ removeRemoteMLSClients (rangedChunks remoteConvs) + when (mtpHasMore page) $ do + let nextState = mtpPagingState page + nextQuery = GetPaginatedConversationIds (Just nextState) range + newCids <- Query.conversationIdsPageFrom lusr nextQuery + goConvs range newCids lusr + + removeRemoteMLSClients :: Range 1 1000 [Remote ConvId] -> Sem r () + removeRemoteMLSClients convIds = do + for_ (bucketRemote (fromRange convIds)) $ \remoteConvs -> do + runFederatedEither remoteConvs (rpc (ClientRemovedRequest usr cid (tUnqualified remoteConvs))) + >>= logAndIgnoreError "Error in onConversationUpdated call" usr + + logAndIgnoreError message usr' res = + whenLeft res $ \federationError -> + P.err + ( Log.msg + ( "Federation error while notifying remote backends of a client deletion (Galley). " + <> message + <> " " + <> show federationError + ) + . Log.field "user" (show usr') + . Log.field "client" (hex . T.unpack . client $ cid) + ) diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index 17e2ee5edf..85fd3e3c00 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -40,6 +40,7 @@ import qualified Data.Set as Set import Data.Time import qualified Data.UUID.Tagged as U import Galley.API.Error +import Galley.API.MLS.KeyPackage (nullKeyPackageRef) import Galley.API.Mapping import Galley.API.One2One import Galley.API.Util @@ -117,7 +118,7 @@ createGroupConversation lusr conn newConv = do case (newConvProtocol newConv, newConvCreatorClient newConv) of (ProtocolProteusTag, _) -> pure () (ProtocolMLSTag, Just c) -> - E.addMLSClients lcnv (qUntagged lusr) (Set.singleton c) + E.addMLSClients lcnv (qUntagged lusr) (Set.singleton (c, nullKeyPackageRef)) (ProtocolMLSTag, Nothing) -> throw (InvalidPayload "Missing creator_client field when creating an MLS conversation") diff --git a/services/galley/src/Galley/API/Error.hs b/services/galley/src/Galley/API/Error.hs index 6d5714720f..e43688e0c1 100644 --- a/services/galley/src/Galley/API/Error.hs +++ b/services/galley/src/Galley/API/Error.hs @@ -23,6 +23,7 @@ module Galley.API.Error InvalidInput (..), InternalError (..), internalErrorWithDescription, + internalErrorDescription, legalHoldServiceUnavailable, -- * Errors thrown by wai-routing handlers @@ -34,6 +35,7 @@ import Data.Id import Data.Text.Lazy as LT (pack) import Imports import Network.HTTP.Types.Status +import Network.Wai.Utilities (Error (message)) import qualified Network.Wai.Utilities.Error as Wai import Wire.API.Error @@ -44,6 +46,9 @@ data InternalError | CannotCreateManagedConv | InternalErrorWithDescription LText +internalErrorDescription :: InternalError -> LText +internalErrorDescription = message . toWai + instance APIError InternalError where toWai (BadConvState convId) = badConvState convId toWai BadMemberState = Wai.mkError status500 "bad-state" "Bad internal member state." diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 4a93480e8c..46e56f2fe6 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -38,8 +38,10 @@ import qualified Data.Text.Lazy as LT import Data.Time.Clock import Galley.API.Action import Galley.API.Error +import Galley.API.MLS.GroupInfo import Galley.API.MLS.KeyPackage import Galley.API.MLS.Message +import Galley.API.MLS.Removal import Galley.API.MLS.Welcome import qualified Galley.API.Mapping as Mapping import Galley.API.Message @@ -49,6 +51,7 @@ import Galley.App import qualified Galley.Data.Conversation as Data import Galley.Effects import qualified Galley.Effects.BrigAccess as E +import Galley.Effects.ConversationStore (getConversation) import qualified Galley.Effects.ConversationStore as E import qualified Galley.Effects.FireAndForget as E import qualified Galley.Effects.MemberStore as E @@ -62,6 +65,7 @@ import Polysemy.Error import Polysemy.Input import Polysemy.Internal.Kind (Append) import Polysemy.Resource +import Polysemy.TinyLog import qualified Polysemy.TinyLog as P import Servant (ServerT) import Servant.API @@ -77,11 +81,13 @@ import Wire.API.Error.Galley import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Common (EmptyResponse (..)) -import Wire.API.Federation.API.Galley (ConversationUpdateResponse) +import Wire.API.Federation.API.Galley import qualified Wire.API.Federation.API.Galley as F import Wire.API.Federation.Error +import Wire.API.MLS.CommitBundle import Wire.API.MLS.Credential import Wire.API.MLS.Message +import Wire.API.MLS.PublicGroupState import Wire.API.MLS.Serialisation import Wire.API.MLS.Welcome import Wire.API.Message @@ -106,6 +112,37 @@ federationSitemap = :<|> Named @"mls-welcome" mlsSendWelcome :<|> Named @"on-mls-message-sent" onMLSMessageSent :<|> Named @"send-mls-message" sendMLSMessage + :<|> Named @"send-mls-commit-bundle" sendMLSCommitBundle + :<|> Named @"query-group-info" queryGroupInfo + :<|> Named @"on-client-removed" onClientRemoved + +onClientRemoved :: + ( Members + '[ ConversationStore, + Error InternalError, + ExternalAccess, + FederatorAccess, + GundeckAccess, + Input Env, + Input (Local ()), + Input UTCTime, + MemberStore, + ProposalStore, + TinyLog + ] + r + ) => + Domain -> + ClientRemovedRequest -> + Sem r EmptyResponse +onClientRemoved domain req = do + let qusr = Qualified (F.crrUser req) domain + for_ (F.crrConvs req) $ \convId -> do + mConv <- getConversation convId + for mConv $ \conv -> do + lconv <- qualifyLocal conv + removeClient lconv qusr (F.crrClient req) + pure EmptyResponse onConversationCreated :: Members @@ -226,8 +263,8 @@ onConversationUpdated requestingDomain cu = do [] -> pure (Nothing, []) -- If no users get added, its like no action was performed. (u : us) -> pure (Just (SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (u :| us) role)), addedLocalUsers) SConversationLeaveTag -> do - let localUsers = getLocalUsers (tDomain loc) action - E.deleteMembersInRemoteConversation rconvId localUsers + let users = foldQualified loc (pure . tUnqualified) (const []) (F.cuOrigUserId cu) + E.deleteMembersInRemoteConversation rconvId users pure (Just sca, []) SConversationRemoveMembersTag -> do let localUsers = getLocalUsers (tDomain loc) action @@ -290,13 +327,17 @@ addLocalUsersToRemoteConv remoteConvId qAdder localUsers = do leaveConversation :: Members '[ ConversationStore, + Error InternalError, Error InvalidInput, ExternalAccess, FederatorAccess, GundeckAccess, + Input Env, Input (Local ()), Input UTCTime, - MemberStore + MemberStore, + ProposalStore, + TinyLog ] r => Domain -> @@ -321,24 +362,23 @@ leaveConversation requestingDomain lc = do lcnv (qUntagged leaver) Nothing - (pure (qUntagged leaver)) + () pure (update, conv) case res of Left e -> pure $ F.LeaveConversationResponse (Left e) Right (_update, conv) -> do - let action = pure (qUntagged leaver) - let remotes = filter ((== tDomain leaver) . tDomain) (rmId <$> Data.convRemoteMembers conv) let botsAndMembers = BotsAndMembers mempty (Set.fromList remotes) mempty _ <- notifyConversationAction SConversationLeaveTag (qUntagged leaver) + False Nothing (qualifyAs lcnv conv) botsAndMembers - action + () pure $ F.LeaveConversationResponse (Right ()) @@ -422,9 +462,13 @@ onUserDeleted :: FireAndForget, ExternalAccess, GundeckAccess, + Error InternalError, Input (Local ()), Input UTCTime, - MemberStore + Input Env, + MemberStore, + ProposalStore, + TinyLog ] r => Domain -> @@ -452,16 +496,17 @@ onUserDeleted origDomain udcn = do -- The self conv cannot be on a remote backend. Public.SelfConv -> pure () Public.RegularConv -> do - let action = pure untaggedDeletedUser - botsAndMembers = convBotsAndMembers conv + let botsAndMembers = convBotsAndMembers conv + removeUser (qualifyAs lc conv) (qUntagged deletedUser) void $ notifyConversationAction (sing @'ConversationLeaveTag) untaggedDeletedUser + False Nothing (qualifyAs lc conv) botsAndMembers - action + () pure EmptyResponse updateConversation :: @@ -477,11 +522,14 @@ updateConversation :: FederatorAccess, Error InternalError, GundeckAccess, + Input Env, Input Opts, Input UTCTime, LegalHoldStore, MemberStore, + ProposalStore, TeamStore, + TinyLog, ConversationStore, Input (Local ()) ] @@ -547,6 +595,49 @@ updateConversation origDomain updateRequest = do toResponse (Right (Left NoChanges)) = F.ConversationUpdateResponseNoChanges toResponse (Right (Right update)) = F.ConversationUpdateResponseUpdate update +sendMLSCommitBundle :: + ( Members + [ BrigAccess, + ConversationStore, + ExternalAccess, + Error FederationError, + Error InternalError, + FederatorAccess, + GundeckAccess, + Input (Local ()), + Input Env, + Input Opts, + Input UTCTime, + LegalHoldStore, + MemberStore, + Resource, + TeamStore, + P.TinyLog, + ProposalStore + ] + r + ) => + Domain -> + F.MessageSendRequest -> + Sem r F.MLSMessageResponse +sendMLSCommitBundle remoteDomain msr = + fmap (either (F.MLSMessageResponseProtocolError . unTagged) id) + . runError @MLSProtocolError + . fmap (either F.MLSMessageResponseError id) + . runError + . fmap (either (F.MLSMessageResponseProposalFailure . pfInner) id) + . runError + $ do + loc <- qualifyLocal () + let sender = toRemoteUnsafe remoteDomain (F.msrSender msr) + bundle <- either (throw . mlsProtocolError) pure $ decodeMLS' (fromBase64ByteString (F.msrRawMessage msr)) + mapToGalleyError @MLSBundleStaticErrors $ do + let msg = rmValue (cbCommitMsg (rmValue bundle)) + qcnv <- E.getConversationIdByGroupId (msgGroupId msg) >>= noteS @'ConvNotFound + when (qUnqualified qcnv /= F.msrConvId msr) $ throwS @'MLSGroupConversationMismatch + F.MLSMessageResponseUpdates . map lcuUpdate + <$> postMLSCommitBundle loc (qUntagged sender) qcnv Nothing bundle + sendMLSMessage :: ( Members [ BrigAccess, @@ -557,6 +648,7 @@ sendMLSMessage :: FederatorAccess, GundeckAccess, Input (Local ()), + Input Env, Input Opts, Input UTCTime, LegalHoldStore, @@ -682,3 +774,27 @@ onMLSMessageSent domain rmm = do runMessagePush loc (Just (qUntagged rcnv)) $ foldMap mkPush recipients pure EmptyResponse + +queryGroupInfo :: + ( Members + '[ ConversationStore, + Input (Local ()) + ] + r, + Member MemberStore r + ) => + Domain -> + F.GetGroupInfoRequest -> + Sem r F.GetGroupInfoResponse +queryGroupInfo origDomain req = + fmap (either F.GetGroupInfoResponseError F.GetGroupInfoResponseState) + . runError @GalleyError + . mapToGalleyError @MLSGroupInfoStaticErrors + $ do + lconvId <- qualifyLocal . ggireqConv $ req + let sender = toRemoteUnsafe origDomain . ggireqSender $ req + state <- getGroupInfoFromLocalConv (qUntagged sender) lconvId + pure + . Base64ByteString + . unOpaquePublicGroupState + $ state diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 04497a5682..e7d9876ba4 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -37,8 +37,10 @@ import GHC.TypeLits (AppendSymbol) import qualified Galley.API.Clients as Clients import qualified Galley.API.Create as Create import qualified Galley.API.CustomBackend as CustomBackend +import Galley.API.Error import Galley.API.LegalHold (unsetTeamLegalholdWhitelistedH) import Galley.API.LegalHold.Conflicts +import Galley.API.MLS.Removal import Galley.API.One2One import Galley.API.Public import Galley.API.Public.Servant @@ -58,6 +60,7 @@ import Galley.Effects.FederatorAccess import Galley.Effects.GundeckAccess import Galley.Effects.LegalHoldStore as LegalHoldStore import Galley.Effects.MemberStore +import Galley.Effects.ProposalStore import Galley.Effects.TeamStore import qualified Galley.Intra.Push as Intra import Galley.Monad @@ -189,6 +192,10 @@ type IFeatureAPI = :<|> IFeatureStatusGet MLSConfig :<|> IFeatureStatusPut '() MLSConfig :<|> IFeatureStatusPatch '() MLSConfig + -- ExposeInvitationURLsToTeamAdminConfig + :<|> IFeatureStatusGet ExposeInvitationURLsToTeamAdminConfig + :<|> IFeatureStatusPut '() ExposeInvitationURLsToTeamAdminConfig + :<|> IFeatureStatusPatch '() ExposeInvitationURLsToTeamAdminConfig -- SearchVisibilityInboundConfig :<|> IFeatureStatusGet SearchVisibilityInboundConfig :<|> IFeatureStatusPut '() SearchVisibilityInboundConfig @@ -527,6 +534,9 @@ featureAPI = <@> mkNamedAPI @'("iget", MLSConfig) (getFeatureStatus @Cassandra DontDoAuth) <@> mkNamedAPI @'("iput", MLSConfig) (setFeatureStatusInternal @Cassandra) <@> mkNamedAPI @'("ipatch", MLSConfig) (patchFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("iget", ExposeInvitationURLsToTeamAdminConfig) (getFeatureStatus @Cassandra DontDoAuth) + <@> mkNamedAPI @'("iput", ExposeInvitationURLsToTeamAdminConfig) (setFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("ipatch", ExposeInvitationURLsToTeamAdminConfig) (patchFeatureStatusInternal @Cassandra) <@> mkNamedAPI @'("iget", SearchVisibilityInboundConfig) (getFeatureStatus @Cassandra DontDoAuth) <@> mkNamedAPI @'("iput", SearchVisibilityInboundConfig) (setFeatureStatusInternal @Cassandra) <@> mkNamedAPI @'("ipatch", SearchVisibilityInboundConfig) (patchFeatureStatusInternal @Cassandra) @@ -629,10 +639,13 @@ rmUser :: FederatorAccess, GundeckAccess, Input UTCTime, + Input Env, ListItems p1 ConvId, ListItems p1 (Remote ConvId), ListItems p2 TeamId, + Input (Local ()), MemberStore, + ProposalStore, TeamStore, P.TinyLog ] @@ -678,6 +691,9 @@ rmUser lusr conn = do ConnectConv -> deleteMembers (Data.convId c) (UserList [tUnqualified lusr] []) $> Nothing RegularConv | tUnqualified lusr `isMember` Data.convLocalMembers c -> do + runError (removeUser (qualifyAs lusr c) (qUntagged lusr)) >>= \case + Left e -> P.err $ Log.msg ("failed to send remove proposal: " <> internalErrorDescription e) + Right _ -> pure () deleteMembers (Data.convId c) (UserList [tUnqualified lusr] []) let e = Event @@ -708,7 +724,7 @@ rmUser lusr conn = do cuOrigUserId = qUser, cuConvId = cid, cuAlreadyPresentUsers = tUnqualified remotes, - cuAction = SomeConversationAction (sing @'ConversationLeaveTag) (pure qUser) + cuAction = SomeConversationAction (sing @'ConversationLeaveTag) () } let rpc = fedClient @'Galley @"on-conversation-updated" convUpdate runFederatedEither remotes rpc diff --git a/services/galley/src/Galley/API/LegalHold.hs b/services/galley/src/Galley/API/LegalHold.hs index 302ef62ad5..71288e7af5 100644 --- a/services/galley/src/Galley/API/LegalHold.hs +++ b/services/galley/src/Galley/API/LegalHold.hs @@ -48,11 +48,13 @@ import Galley.API.Error import Galley.API.Query (iterateConversations) import Galley.API.Update (removeMemberFromLocalConv) import Galley.API.Util +import Galley.App import qualified Galley.Data.Conversation as Data import Galley.Effects import Galley.Effects.BrigAccess import Galley.Effects.FireAndForget import qualified Galley.Effects.LegalHoldStore as LegalHoldData +import Galley.Effects.ProposalStore import qualified Galley.Effects.TeamFeatureStore as TeamFeatures import Galley.Effects.TeamMemberStore import Galley.Effects.TeamStore @@ -187,30 +189,32 @@ removeSettingsInternalPaging :: BrigAccess, CodeStore, ConversationStore, - Error InternalError, Error AuthenticationError, - ErrorS OperationDenied, - ErrorS 'NotATeamMember, + Error InternalError, ErrorS ('ActionDenied 'RemoveConversationMember), ErrorS 'InvalidOperation, - ErrorS 'LegalHoldNotEnabled, + ErrorS 'LegalHoldCouldNotBlockConnections, ErrorS 'LegalHoldDisableUnimplemented, + ErrorS 'LegalHoldNotEnabled, ErrorS 'LegalHoldServiceNotRegistered, + ErrorS 'NotATeamMember, + ErrorS OperationDenied, ErrorS 'UserLegalHoldIllegalOperation, - ErrorS 'LegalHoldCouldNotBlockConnections, ExternalAccess, FederatorAccess, FireAndForget, GundeckAccess, - Input UTCTime, + Input Env, Input (Local ()), + Input UTCTime, LegalHoldStore, ListItems LegacyPaging ConvId, MemberStore, + ProposalStore, + P.TinyLog, TeamFeatureStore db, TeamMemberStore InternalPaging, TeamStore, - P.TinyLog, WaiRoutes ] r => @@ -230,30 +234,32 @@ removeSettings :: BrigAccess, CodeStore, ConversationStore, - Error InternalError, Error AuthenticationError, - ErrorS 'NotATeamMember, - ErrorS OperationDenied, + Error InternalError, ErrorS ('ActionDenied 'RemoveConversationMember), ErrorS 'InvalidOperation, - ErrorS 'LegalHoldNotEnabled, + ErrorS 'LegalHoldCouldNotBlockConnections, ErrorS 'LegalHoldDisableUnimplemented, + ErrorS 'LegalHoldNotEnabled, ErrorS 'LegalHoldServiceNotRegistered, + ErrorS 'NotATeamMember, + ErrorS OperationDenied, ErrorS 'UserLegalHoldIllegalOperation, - ErrorS 'LegalHoldCouldNotBlockConnections, ExternalAccess, FederatorAccess, FireAndForget, GundeckAccess, - Input UTCTime, + Input Env, Input (Local ()), + Input UTCTime, LegalHoldStore, ListItems LegacyPaging ConvId, MemberStore, + ProposalStore, + P.TinyLog, TeamFeatureStore db, TeamMemberStore p, - TeamStore, - P.TinyLog + TeamStore ] r ) => @@ -305,11 +311,13 @@ removeSettings' :: GundeckAccess, Input UTCTime, Input (Local ()), + Input Env, LegalHoldStore, ListItems LegacyPaging ConvId, MemberStore, TeamMemberStore p, TeamStore, + ProposalStore, P.TinyLog ] r @@ -386,18 +394,20 @@ grantConsent :: Error InternalError, ErrorS ('ActionDenied 'RemoveConversationMember), ErrorS 'InvalidOperation, + ErrorS 'LegalHoldCouldNotBlockConnections, ErrorS 'TeamMemberNotFound, ErrorS 'UserLegalHoldIllegalOperation, - ErrorS 'LegalHoldCouldNotBlockConnections, ExternalAccess, FederatorAccess, GundeckAccess, + Input Env, Input UTCTime, LegalHoldStore, ListItems LegacyPaging ConvId, MemberStore, - TeamStore, - P.TinyLog + ProposalStore, + P.TinyLog, + TeamStore ] r => Local UserId -> @@ -422,27 +432,29 @@ requestDevice :: ConversationStore, Error InternalError, ErrorS ('ActionDenied 'RemoveConversationMember), + ErrorS 'LegalHoldCouldNotBlockConnections, + ErrorS 'LegalHoldNotEnabled, + ErrorS 'LegalHoldServiceBadResponse, + ErrorS 'LegalHoldServiceNotRegistered, ErrorS 'NotATeamMember, + ErrorS 'NoUserLegalHoldConsent, ErrorS OperationDenied, ErrorS 'TeamMemberNotFound, - ErrorS 'LegalHoldNotEnabled, ErrorS 'UserLegalHoldAlreadyEnabled, - ErrorS 'NoUserLegalHoldConsent, - ErrorS 'LegalHoldServiceBadResponse, - ErrorS 'LegalHoldServiceNotRegistered, - ErrorS 'LegalHoldCouldNotBlockConnections, ErrorS 'UserLegalHoldIllegalOperation, - Input (Local ()), ExternalAccess, FederatorAccess, GundeckAccess, + Input (Local ()), + Input Env, Input UTCTime, LegalHoldStore, ListItems LegacyPaging ConvId, MemberStore, + ProposalStore, + P.TinyLog, TeamFeatureStore db, - TeamStore, - P.TinyLog + TeamStore ] r => TeamFeatures.FeaturePersistentConstraint db Public.LegalholdConfig => @@ -498,29 +510,31 @@ approveDevice :: Members '[ BrigAccess, ConversationStore, - Error InternalError, Error AuthenticationError, + Error InternalError, ErrorS 'AccessDenied, ErrorS ('ActionDenied 'RemoveConversationMember), - ErrorS 'NotATeamMember, + ErrorS 'LegalHoldCouldNotBlockConnections, ErrorS 'LegalHoldNotEnabled, - ErrorS 'UserLegalHoldNotPending, - ErrorS 'NoLegalHoldDeviceAllocated, ErrorS 'LegalHoldServiceNotRegistered, + ErrorS 'NoLegalHoldDeviceAllocated, + ErrorS 'NotATeamMember, ErrorS 'UserLegalHoldAlreadyEnabled, ErrorS 'UserLegalHoldIllegalOperation, - ErrorS 'LegalHoldCouldNotBlockConnections, - Input (Local ()), + ErrorS 'UserLegalHoldNotPending, ExternalAccess, FederatorAccess, GundeckAccess, + Input (Local ()), + Input Env, Input UTCTime, LegalHoldStore, ListItems LegacyPaging ConvId, MemberStore, + ProposalStore, + P.TinyLog, TeamFeatureStore db, - TeamStore, - P.TinyLog + TeamStore ] r => TeamFeatures.FeaturePersistentConstraint db Public.LegalholdConfig => @@ -577,24 +591,26 @@ disableForUser :: Members '[ BrigAccess, ConversationStore, - Error InternalError, Error AuthenticationError, + Error InternalError, ErrorS ('ActionDenied 'RemoveConversationMember), + ErrorS 'LegalHoldCouldNotBlockConnections, + ErrorS 'LegalHoldServiceNotRegistered, ErrorS 'NotATeamMember, ErrorS OperationDenied, - ErrorS 'LegalHoldServiceNotRegistered, ErrorS 'UserLegalHoldIllegalOperation, - ErrorS 'LegalHoldCouldNotBlockConnections, - Input (Local ()), ExternalAccess, FederatorAccess, GundeckAccess, + Input Env, + Input (Local ()), Input UTCTime, LegalHoldStore, ListItems LegacyPaging ConvId, MemberStore, - TeamStore, - P.TinyLog + ProposalStore, + P.TinyLog, + TeamStore ] r => Local UserId -> @@ -640,11 +656,13 @@ changeLegalholdStatus :: ExternalAccess, FederatorAccess, GundeckAccess, + Input Env, Input UTCTime, LegalHoldStore, ListItems LegacyPaging ConvId, MemberStore, TeamStore, + ProposalStore, P.TinyLog ] r => @@ -755,9 +773,12 @@ handleGroupConvPolicyConflicts :: ExternalAccess, FederatorAccess, GundeckAccess, + Input Env, Input UTCTime, ListItems LegacyPaging ConvId, MemberStore, + ProposalStore, + P.TinyLog, TeamStore ] r => diff --git a/services/galley/src/Galley/API/MLS.hs b/services/galley/src/Galley/API/MLS.hs index fc85496141..242414a442 100644 --- a/services/galley/src/Galley/API/MLS.hs +++ b/services/galley/src/Galley/API/MLS.hs @@ -16,8 +16,9 @@ -- with this program. If not, see . module Galley.API.MLS - ( postMLSWelcome, + ( postMLSWelcomeFromLocalUser, postMLSMessage, + postMLSCommitBundleFromLocalUser, postMLSMessageFromLocalUser, postMLSMessageFromLocalUserV1, getMLSPublicKeys, diff --git a/services/galley/src/Galley/API/MLS/GroupInfo.hs b/services/galley/src/Galley/API/MLS/GroupInfo.hs new file mode 100644 index 0000000000..40637193b6 --- /dev/null +++ b/services/galley/src/Galley/API/MLS/GroupInfo.hs @@ -0,0 +1,98 @@ +-- 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 Galley.API.MLS.GroupInfo where + +import Data.Id as Id +import Data.Json.Util +import Data.Qualified +import Galley.API.MLS.Util +import Galley.API.Util +import Galley.Effects +import qualified Galley.Effects.ConversationStore as E +import qualified Galley.Effects.FederatorAccess as E +import Imports +import Polysemy +import Polysemy.Error +import Polysemy.Input +import Wire.API.Error +import Wire.API.Error.Galley +import Wire.API.Federation.API +import Wire.API.Federation.API.Galley +import Wire.API.Federation.Error +import Wire.API.MLS.PublicGroupState + +type MLSGroupInfoStaticErrors = + '[ ErrorS 'ConvNotFound, + ErrorS 'MLSMissingGroupInfo + ] + +getGroupInfo :: + Members + '[ ConversationStore, + Error FederationError, + FederatorAccess, + Input (Local ()), + MemberStore + ] + r => + Members MLSGroupInfoStaticErrors r => + Local UserId -> + Qualified ConvId -> + Sem r OpaquePublicGroupState +getGroupInfo lusr qcnvId = + foldQualified + lusr + (getGroupInfoFromLocalConv . qUntagged $ lusr) + (getGroupInfoFromRemoteConv lusr) + qcnvId + +getGroupInfoFromLocalConv :: + Members + '[ ConversationStore, + MemberStore, + Input (Local ()) + ] + r => + Members MLSGroupInfoStaticErrors r => + Qualified UserId -> + Local ConvId -> + Sem r OpaquePublicGroupState +getGroupInfoFromLocalConv qusr lcnvId = do + void $ getLocalConvForUser qusr lcnvId + E.getPublicGroupState (tUnqualified lcnvId) + >>= noteS @'MLSMissingGroupInfo + +getGroupInfoFromRemoteConv :: + Members '[Error FederationError, FederatorAccess] r => + Members MLSGroupInfoStaticErrors r => + Local UserId -> + Remote ConvId -> + Sem r OpaquePublicGroupState +getGroupInfoFromRemoteConv lusr rcnv = do + let getRequest = + GetGroupInfoRequest + { ggireqSender = tUnqualified lusr, + ggireqConv = tUnqualified rcnv + } + response <- E.runFederated rcnv (fedClient @'Galley @"query-group-info" getRequest) + case response of + GetGroupInfoResponseError e -> rethrowErrors @MLSGroupInfoStaticErrors e + GetGroupInfoResponseState s -> + pure . OpaquePublicGroupState + . fromBase64ByteString + $ s diff --git a/services/galley/src/Galley/API/MLS/KeyPackage.hs b/services/galley/src/Galley/API/MLS/KeyPackage.hs index 2acbab1185..c5e42031a4 100644 --- a/services/galley/src/Galley/API/MLS/KeyPackage.hs +++ b/services/galley/src/Galley/API/MLS/KeyPackage.hs @@ -17,6 +17,7 @@ module Galley.API.MLS.KeyPackage where +import qualified Data.ByteString as BS import Galley.Effects.BrigAccess import Imports import Polysemy @@ -25,6 +26,9 @@ import Wire.API.Error.Galley import Wire.API.MLS.Credential import Wire.API.MLS.KeyPackage +nullKeyPackageRef :: KeyPackageRef +nullKeyPackageRef = KeyPackageRef (BS.replicate 16 0) + derefKeyPackage :: Members '[ BrigAccess, diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 8e0ea8ef87..f90829e766 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -14,20 +14,20 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -{-# LANGUAGE RecordWildCards #-} module Galley.API.MLS.Message - ( postMLSMessageFromLocalUser, + ( postMLSCommitBundle, + postMLSCommitBundleFromLocalUser, + postMLSMessageFromLocalUser, postMLSMessageFromLocalUserV1, postMLSMessage, MLSMessageStaticErrors, + MLSBundleStaticErrors, ) where import Control.Comonad import Control.Lens (preview, to) -import Data.Bifunctor -import Data.Domain import Data.Id import Data.Json.Util import Data.List.NonEmpty (NonEmpty, nonEmpty) @@ -39,11 +39,13 @@ import Data.Time import Galley.API.Action import Galley.API.Error import Galley.API.MLS.KeyPackage -import Galley.API.Push +import Galley.API.MLS.Propagate +import Galley.API.MLS.Types +import Galley.API.MLS.Util +import Galley.API.MLS.Welcome (postMLSWelcome) import Galley.API.Util import Galley.Data.Conversation.Types hiding (Conversation) import qualified Galley.Data.Conversation.Types as Data -import Galley.Data.Services import Galley.Data.Types import Galley.Effects import Galley.Effects.BrigAccess @@ -51,17 +53,16 @@ import Galley.Effects.ConversationStore import Galley.Effects.FederatorAccess import Galley.Effects.MemberStore import Galley.Effects.ProposalStore +import Galley.Env import Galley.Options import Galley.Types.Conversations.Members import Imports -import Network.Wai.Utilities.Server import Polysemy import Polysemy.Error import Polysemy.Input import Polysemy.Internal import Polysemy.Resource (Resource, bracket) import Polysemy.TinyLog -import qualified System.Logger.Class as Logger import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role @@ -74,13 +75,18 @@ import Wire.API.Federation.API.Galley import Wire.API.Federation.Error import Wire.API.MLS.CipherSuite import Wire.API.MLS.Commit +import Wire.API.MLS.CommitBundle import Wire.API.MLS.Credential +import Wire.API.MLS.GroupInfoBundle import Wire.API.MLS.KeyPackage import Wire.API.MLS.Message import Wire.API.MLS.Proposal import qualified Wire.API.MLS.Proposal as Proposal +import Wire.API.MLS.PublicGroupState import Wire.API.MLS.Serialisation +import Wire.API.MLS.Welcome import Wire.API.Message +import Wire.API.Routes.Internal.Brig import Wire.API.User.Client type MLSMessageStaticErrors = @@ -100,6 +106,11 @@ type MLSMessageStaticErrors = ErrorS 'MLSGroupConversationMismatch ] +type MLSBundleStaticErrors = + Append + MLSMessageStaticErrors + '[ErrorS 'MLSWelcomeMismatch] + postMLSMessageFromLocalUserV1 :: ( HasProposalEffects r, Members @@ -167,22 +178,191 @@ postMLSMessageFromLocalUser lusr conn msg = do t <- toUTCTimeMillis <$> input pure $ MLSMessageSendingStatus events t +postMLSCommitBundle :: + ( HasProposalEffects r, + Members MLSBundleStaticErrors r, + Members + '[ BrigAccess, + Error FederationError, + Error InternalError, + Error MLSProtocolError, + Input (Local ()), + Input Opts, + Input UTCTime, + MemberStore, + ProposalStore, + Resource, + TinyLog + ] + r + ) => + Local x -> + Qualified UserId -> + Qualified ConvId -> + Maybe ConnId -> + RawMLS CommitBundle -> + Sem r [LocalConversationUpdate] +postMLSCommitBundle loc qusr qcnv conn rawBundle = + foldQualified + loc + (postMLSCommitBundleToLocalConv qusr conn (rmValue rawBundle)) + (postMLSCommitBundleToRemoteConv loc qusr conn rawBundle) + qcnv + +postMLSCommitBundleFromLocalUser :: + ( HasProposalEffects r, + Members MLSBundleStaticErrors r, + Members + '[ BrigAccess, + Error FederationError, + Error InternalError, + Input (Local ()), + Input Opts, + Input UTCTime, + MemberStore, + ProposalStore, + Resource, + TinyLog + ] + r + ) => + Local UserId -> + ConnId -> + RawMLS CommitBundle -> + Sem r MLSMessageSendingStatus +postMLSCommitBundleFromLocalUser lusr conn bundle = do + let msg = rmValue (cbCommitMsg (rmValue bundle)) + qcnv <- getConversationIdByGroupId (msgGroupId msg) >>= noteS @'ConvNotFound + events <- + map lcuEvent + <$> postMLSCommitBundle lusr (qUntagged lusr) qcnv (Just conn) bundle + t <- toUTCTimeMillis <$> input + pure $ MLSMessageSendingStatus events t + +postMLSCommitBundleToLocalConv :: + ( HasProposalEffects r, + Members MLSBundleStaticErrors r, + Members + '[ BrigAccess, + Error FederationError, + Error InternalError, + Error MLSProtocolError, + Input (Local ()), + Input UTCTime, + Input Opts, + ProposalStore, + BrigAccess, + Resource, + TinyLog + ] + r + ) => + Qualified UserId -> + Maybe ConnId -> + CommitBundle -> + Local ConvId -> + Sem r [LocalConversationUpdate] +postMLSCommitBundleToLocalConv qusr conn bundle lcnv = do + let msg = rmValue (cbCommitMsg bundle) + conv <- getLocalConvForUser qusr lcnv + let lconv = qualifyAs lcnv conv + cm <- lookupMLSClients lcnv + + senderClient <- fmap ciClient <$> getSenderClient qusr SMLSPlainText msg + + events <- case msgPayload msg of + CommitMessage commit -> + do + (groupId, action) <- getCommitData lconv (msgEpoch msg) commit + -- check that the welcome message matches the action + for_ (cbWelcome bundle) $ \welcome -> + when + ( Set.fromList (map gsNewMember (welSecrets (rmValue welcome))) + /= Set.fromList (map (snd . snd) (cmAssocs (paAdd action))) + ) + $ throwS @'MLSWelcomeMismatch + processCommitWithAction + qusr + senderClient + conn + lconv + cm + (msgEpoch msg) + groupId + action + (msgSender msg) + (Just . cbGroupInfoBundle $ bundle) + commit + ApplicationMessage _ -> throwS @'MLSUnsupportedMessage + ProposalMessage _ -> throwS @'MLSUnsupportedMessage + + propagateMessage qusr (qualifyAs lcnv conv) cm conn (rmRaw (cbCommitMsg bundle)) + + for_ (cbWelcome bundle) $ + postMLSWelcome lcnv conn + + pure events + +postMLSCommitBundleToRemoteConv :: + ( Members MLSBundleStaticErrors r, + Members + '[ Error FederationError, + Error MLSProtocolError, + Error MLSProposalFailure, + ExternalAccess, + FederatorAccess, + GundeckAccess, + MemberStore, + TinyLog + ] + r + ) => + Local x -> + Qualified UserId -> + Maybe ConnId -> + RawMLS CommitBundle -> + Remote ConvId -> + Sem r [LocalConversationUpdate] +postMLSCommitBundleToRemoteConv loc qusr con bundle rcnv = do + -- only local users can send messages to remote conversations + lusr <- foldQualified loc pure (\_ -> throwS @'ConvAccessDenied) qusr + -- only members may send commit bundles to a remote conversation + flip unless (throwS @'ConvMemberNotFound) =<< checkLocalMemberRemoteConv (tUnqualified lusr) rcnv + + resp <- + runFederated rcnv $ + fedClient @'Galley @"send-mls-commit-bundle" $ + MessageSendRequest + { msrConvId = tUnqualified rcnv, + msrSender = tUnqualified lusr, + msrRawMessage = Base64ByteString (rmRaw bundle) + } + updates <- case resp of + MLSMessageResponseError e -> rethrowErrors @MLSBundleStaticErrors e + MLSMessageResponseProtocolError e -> throw (mlsProtocolError e) + MLSMessageResponseProposalFailure e -> throw (MLSProposalFailure e) + MLSMessageResponseUpdates updates -> pure updates + + for updates $ \update -> do + e <- notifyRemoteConversationAction loc (qualifyAs rcnv update) con + pure (LocalConversationUpdate e update) + postMLSMessage :: ( HasProposalEffects r, Members '[ Error FederationError, Error InternalError, ErrorS 'ConvAccessDenied, - ErrorS 'ConvNotFound, ErrorS 'ConvMemberNotFound, - ErrorS 'MLSUnsupportedMessage, - ErrorS 'MLSStaleMessage, - ErrorS 'MLSProposalNotFound, - ErrorS 'MissingLegalholdConsent, - ErrorS 'MLSCommitMissingReferences, - ErrorS 'MLSSelfRemovalNotAllowed, + ErrorS 'ConvNotFound, ErrorS 'MLSClientSenderUserMismatch, + ErrorS 'MLSCommitMissingReferences, ErrorS 'MLSGroupConversationMismatch, + ErrorS 'MLSProposalNotFound, + ErrorS 'MLSSelfRemovalNotAllowed, + ErrorS 'MLSStaleMessage, + ErrorS 'MLSUnsupportedMessage, + ErrorS 'MissingLegalholdConsent, Resource, TinyLog, ProposalStore, @@ -197,49 +377,40 @@ postMLSMessage :: RawMLS SomeMessage -> Sem r [LocalConversationUpdate] postMLSMessage loc qusr qcnv con smsg = case rmValue smsg of - SomeMessage _ msg -> do - unless (msgEpoch msg == Epoch 0) $ - flip unless (throwS @'MLSClientSenderUserMismatch) =<< isUserSender qusr smsg + SomeMessage tag msg -> do + mcid <- fmap ciClient <$> getSenderClient qusr tag msg foldQualified loc - (postMLSMessageToLocalConv qusr con smsg) - (postMLSMessageToRemoteConv loc qusr con smsg) + (postMLSMessageToLocalConv qusr mcid con smsg) + (postMLSMessageToRemoteConv loc qusr mcid con smsg) qcnv --- | Check that the MLS client who created the message belongs to the user who +-- Check that the MLS client who created the message belongs to the user who -- is the sender of the REST request, identified by HTTP header. -- --- This is only relevant in an ongoing conversation. The check should be skipped --- in case of --- * encrypted messages in which we don't have access to the sending client's --- key package, --- * messages sent by the backend, and --- * external add proposals which propose fresh key packages for new clients and --- thus the validity of the key package cannot be known at the time of this --- check. --- For these cases the function will return True. -isUserSender :: +-- The check is skipped in case of conversation creation and encrypted messages. +getSenderClient :: ( Members '[ ErrorS 'MLSKeyPackageRefNotFound, + ErrorS 'MLSClientSenderUserMismatch, BrigAccess ] r ) => Qualified UserId -> - RawMLS SomeMessage -> - Sem r Bool -isUserSender qusr smsg = case rmValue smsg of - SomeMessage tag msg -> case tag of - -- skip encrypted message - SMLSCipherText -> pure True - SMLSPlainText -> case msgSender msg of - -- skip message sent by backend - PreconfiguredSender _ -> pure True - -- skip external add proposal - NewMemberSender -> pure True - MemberSender ref -> do - ci <- derefKeyPackage ref - pure $ fmap fst (cidQualifiedClient ci) == qusr + SWireFormatTag tag -> + Message tag -> + Sem r (Maybe ClientIdentity) +getSenderClient _ SMLSCipherText _ = pure Nothing +getSenderClient _ _ msg | msgEpoch msg == Epoch 0 = pure Nothing +getSenderClient qusr SMLSPlainText msg = case msgSender msg of + PreconfiguredSender _ -> pure Nothing + NewMemberSender -> pure Nothing + MemberSender ref -> do + cid <- derefKeyPackage ref + when (fmap fst (cidQualifiedClient cid) /= qusr) $ + throwS @'MLSClientSenderUserMismatch + pure (Just cid) postMLSMessageToLocalConv :: ( HasProposalEffects r, @@ -261,24 +432,24 @@ postMLSMessageToLocalConv :: r ) => Qualified UserId -> + Maybe ClientId -> Maybe ConnId -> RawMLS SomeMessage -> Local ConvId -> Sem r [LocalConversationUpdate] -postMLSMessageToLocalConv qusr con smsg lcnv = case rmValue smsg of +postMLSMessageToLocalConv qusr senderClient con smsg lcnv = case rmValue smsg of SomeMessage tag msg -> do - conv <- getConversation (tUnqualified lcnv) >>= noteS @'ConvNotFound + conv <- getLocalConvForUser qusr lcnv - -- check that sender is part of conversation - loc <- qualifyLocal () - isMember' <- foldQualified loc (fmap isJust . getLocalMember (convId conv) . tUnqualified) (fmap isJust . getRemoteMember (convId conv)) qusr - unless isMember' $ throwS @'ConvNotFound + -- construct client map + cm <- lookupMLSClients lcnv + let lconv = qualifyAs lcnv conv -- validate message events <- case tag of SMLSPlainText -> case msgPayload msg of CommitMessage c -> - processCommit qusr con (qualifyAs lcnv conv) (msgEpoch msg) (msgSender msg) c + processCommit qusr senderClient con lconv cm (msgEpoch msg) (msgSender msg) c ApplicationMessage _ -> throwS @'MLSUnsupportedMessage ProposalMessage prop -> processProposal qusr conv msg prop $> mempty @@ -289,7 +460,7 @@ postMLSMessageToLocalConv qusr con smsg lcnv = case rmValue smsg of Left _ -> throwS @'MLSUnsupportedMessage -- forward message - propagateMessage lcnv qusr conv con (rmRaw smsg) + propagateMessage qusr lconv cm con (rmRaw smsg) pure events @@ -300,11 +471,12 @@ postMLSMessageToRemoteConv :: ) => Local x -> Qualified UserId -> + Maybe ClientId -> Maybe ConnId -> RawMLS SomeMessage -> Remote ConvId -> Sem r [LocalConversationUpdate] -postMLSMessageToRemoteConv loc qusr con smsg rcnv = do +postMLSMessageToRemoteConv loc qusr _senderClient con smsg rcnv = do -- only local users can send messages to remote conversations lusr <- foldQualified loc pure (\_ -> throwS @'ConvAccessDenied) qusr -- only members may send messages to the remote conversation @@ -331,23 +503,27 @@ postMLSMessageToRemoteConv loc qusr con smsg rcnv = do type HasProposalEffects r = ( Member BrigAccess r, Member ConversationStore r, + Member (Error InternalError) r, Member (Error MLSProposalFailure) r, Member (Error MLSProtocolError) r, - Member (ErrorS 'MLSKeyPackageRefNotFound) r, Member (ErrorS 'MLSClientMismatch) r, + Member (ErrorS 'MLSKeyPackageRefNotFound) r, Member (ErrorS 'MLSUnsupportedProposal) r, Member ExternalAccess r, Member FederatorAccess r, Member GundeckAccess r, + Member (Input Env) r, + Member (Input (Local ())) r, Member (Input Opts) r, Member (Input UTCTime) r, Member LegalHoldStore r, Member MemberStore r, - Member TeamStore r + Member ProposalStore r, + Member TeamStore r, + Member TeamStore r, + Member TinyLog r ) -type ClientMap = Map (Qualified UserId) (Set ClientId) - data ProposalAction = ProposalAction { paAdd :: ClientMap, paRemove :: ClientMap @@ -362,12 +538,41 @@ instance Semigroup ProposalAction where instance Monoid ProposalAction where mempty = ProposalAction mempty mempty -paAddClient :: Qualified (UserId, ClientId) -> ProposalAction +paAddClient :: Qualified (UserId, (ClientId, KeyPackageRef)) -> ProposalAction paAddClient quc = mempty {paAdd = Map.singleton (fmap fst quc) (Set.singleton (snd (qUnqualified quc)))} -paRemoveClient :: Qualified (UserId, ClientId) -> ProposalAction +paRemoveClient :: Qualified (UserId, (ClientId, KeyPackageRef)) -> ProposalAction paRemoveClient quc = mempty {paRemove = Map.singleton (fmap fst quc) (Set.singleton (snd (qUnqualified quc)))} +getCommitData :: + ( HasProposalEffects r, + Member (ErrorS 'ConvNotFound) r, + Member (Error MLSProtocolError) r, + Member (ErrorS 'MLSProposalNotFound) r, + Member (ErrorS 'MLSStaleMessage) r, + Member (Input (Local ())) r, + Member (Input Env) r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member TinyLog r + ) => + Local Data.Conversation -> + Epoch -> + Commit -> + Sem r (GroupId, ProposalAction) +getCommitData lconv epoch commit = do + convMeta <- + preview (to convProtocol . _ProtocolMLS) (tUnqualified lconv) + & noteS @'ConvNotFound + + let curEpoch = cnvmlsEpoch convMeta + groupId = cnvmlsGroupId convMeta + + -- check epoch number + when (epoch /= curEpoch) $ throwS @'MLSStaleMessage + action <- foldMap (applyProposalRef (tUnqualified lconv) groupId epoch) (cProposals commit) + pure (groupId, action) + processCommit :: ( HasProposalEffects r, Member (Error FederationError) r, @@ -384,24 +589,47 @@ processCommit :: Member Resource r ) => Qualified UserId -> + Maybe ClientId -> Maybe ConnId -> Local Data.Conversation -> + ClientMap -> Epoch -> Sender 'MLSPlainText -> Commit -> Sem r [LocalConversationUpdate] -processCommit qusr con lconv epoch sender commit = do - self <- noteS @'ConvNotFound $ getConvMember lconv (tUnqualified lconv) qusr +processCommit qusr senderClient con lconv cm epoch sender commit = do + (groupId, action) <- getCommitData lconv epoch commit + processCommitWithAction qusr senderClient con lconv cm epoch groupId action sender Nothing commit - -- check epoch number - convMeta <- - preview (to convProtocol . _ProtocolMLS) (tUnqualified lconv) - & noteS @'ConvNotFound - - let curEpoch = cnvmlsEpoch convMeta - groupId = cnvmlsGroupId convMeta - - when (epoch /= curEpoch) $ throwS @'MLSStaleMessage +processCommitWithAction :: + ( HasProposalEffects r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'MLSCommitMissingReferences) r, + Member (ErrorS 'MLSProposalNotFound) r, + Member (ErrorS 'MLSSelfRemovalNotAllowed) r, + Member (ErrorS 'MLSStaleMessage) r, + Member (ErrorS 'MissingLegalholdConsent) r, + Member (Input (Local ())) r, + Member ProposalStore r, + Member BrigAccess r, + Member Resource r + ) => + Qualified UserId -> + Maybe ClientId -> + Maybe ConnId -> + Local Data.Conversation -> + ClientMap -> + Epoch -> + GroupId -> + ProposalAction -> + Sender 'MLSPlainText -> + Maybe GroupInfoBundle -> + Commit -> + Sem r [LocalConversationUpdate] +processCommitWithAction qusr senderClient con lconv cm epoch groupId action sender mGIBundle commit = do + self <- noteS @'ConvNotFound $ getConvMember lconv (tUnqualified lconv) qusr let ttlSeconds :: Int = 600 -- 10 minutes withCommitLock groupId epoch (fromIntegral ttlSeconds) $ do @@ -411,27 +639,24 @@ processCommit qusr con lconv epoch sender commit = do then do -- this is a newly created conversation, and it should contain exactly one -- client (the creator) - case (sender, first (toList . lmMLSClients) self) of - (MemberSender currentRef, Left [creatorClient]) -> do - -- use update path as sender reference and if not existing fall back to sender - senderRef <- - maybe - (pure currentRef) - ( note (mlsProtocolError "Could not compute key package ref") - . kpRef' - . upLeaf - ) - $ cPath commit - -- register the creator client - addKeyPackageRef - senderRef - qusr - creatorClient - (qUntagged (fmap Data.convId lconv)) + case (sender, self, cmAssocs cm) of + (MemberSender currentRef, Left lm, [(qu, (creatorClient, _))]) + | qu == qUntagged (qualifyAs lconv (lmId lm)) -> do + -- use update path as sender reference and if not existing fall back to sender + senderRef <- + maybe + (pure currentRef) + ( note (mlsProtocolError "Could not compute key package ref") + . kpRef' + . upLeaf + ) + $ cPath commit + -- register the creator client + updateKeyPackageMapping lconv qusr creatorClient Nothing senderRef -- remote clients cannot send the first commit - (_, Right _) -> throwS @'MLSStaleMessage + (_, Right _, _) -> throwS @'MLSStaleMessage -- uninitialised conversations should contain exactly one client - (MemberSender _, _) -> + (MemberSender _, _, _) -> throw (InternalErrorWithDescription "Unexpected creator client set") -- the sender of the first commit must be a member _ -> throw (mlsProtocolError "Unexpected sender") @@ -440,11 +665,9 @@ processCommit qusr con lconv epoch sender commit = do (MemberSender senderRef, Just updatedKeyPackage) -> do updatedRef <- kpRef' updatedKeyPackage & note (mlsProtocolError "Could not compute key package ref") -- postpone key package ref update until other checks/processing passed - pure . updateKeyPackageRef $ - KeyPackageUpdate - { kpupPrevious = senderRef, - kpupNext = updatedRef - } + case senderClient of + Just cli -> pure $ updateKeyPackageMapping lconv qusr cli (Just senderRef) updatedRef + Nothing -> pure $ pure () (_, Nothing) -> pure $ pure () -- ignore commits without update path _ -> throw (mlsProtocolError "Unexpected sender") @@ -455,16 +678,47 @@ processCommit qusr con lconv epoch sender commit = do throwS @'MLSCommitMissingReferences -- process and execute proposals - action <- foldMap (applyProposalRef (tUnqualified lconv) groupId epoch) (cProposals commit) - updates <- executeProposalAction qusr con lconv action + updates <- executeProposalAction qusr con lconv cm action -- update key package ref if necessary postponedKeyPackageRefUpdate -- increment epoch number setConversationEpoch (Data.convId (tUnqualified lconv)) (succ epoch) + -- set the group info + for_ mGIBundle $ + setPublicGroupState (Data.convId (tUnqualified lconv)) + . toOpaquePublicGroupState + . gipGroupState pure updates +-- | Note: Use this only for KeyPackage that are already validated +updateKeyPackageMapping :: + Members '[BrigAccess, MemberStore] r => + Local Data.Conversation -> + Qualified UserId -> + ClientId -> + Maybe KeyPackageRef -> + KeyPackageRef -> + Sem r () +updateKeyPackageMapping lconv qusr cid mOld new = do + let lcnv = fmap Data.convId lconv + -- update actual mapping in brig + case mOld of + Nothing -> + addKeyPackageRef new qusr cid (qUntagged lcnv) + Just old -> + updateKeyPackageRef + KeyPackageUpdate + { kpupPrevious = old, + kpupNext = new + } + + -- remove old (client, key package) pair + removeMLSClients lcnv qusr (Set.singleton cid) + -- add new (client, key package) pair + addMLSClients lcnv qusr (Set.singleton (cid, new)) + applyProposalRef :: ( HasProposalEffects r, Members @@ -484,25 +738,51 @@ applyProposalRef conv groupId epoch (Ref ref) = do p <- getProposal groupId epoch ref >>= noteS @'MLSProposalNotFound checkEpoch epoch conv checkGroup groupId conv - applyProposal (rmValue p) + applyProposal (convId conv) (rmValue p) applyProposalRef conv _groupId _epoch (Inline p) = do suite <- preview (to convProtocol . _ProtocolMLS . to cnvmlsCipherSuite) conv & noteS @'ConvNotFound checkProposalCipherSuite suite p - applyProposal p + applyProposal (convId conv) p -applyProposal :: HasProposalEffects r => Proposal -> Sem r ProposalAction -applyProposal (AddProposal kp) = do - ref <- - kpRef' kp - & note (mlsProtocolError "Could not compute ref of a key package in an Add proposal") - qclient <- cidQualifiedClient <$> derefKeyPackage ref - pure (paAddClient qclient) -applyProposal (RemoveProposal ref) = do +applyProposal :: + HasProposalEffects r => + ConvId -> + Proposal -> + Sem r ProposalAction +applyProposal convId (AddProposal kp) = do + ref <- kpRef' kp & note (mlsProtocolError "Could not compute ref of a key package in an Add proposal") + mbClientIdentity <- getClientByKeyPackageRef ref + clientIdentity <- case mbClientIdentity of + Nothing -> do + -- external add proposal for a new key package unknown to the backend + lconvId <- qualifyLocal convId + addKeyPackageMapping lconvId ref (KeyPackageData (rmRaw kp)) + Just ci -> + -- ad-hoc add proposal in commit, the key package has been claimed before + pure ci + pure (paAddClient . (<$$>) (,ref) . cidQualifiedClient $ clientIdentity) + where + addKeyPackageMapping lconv ref kpdata = do + -- validate and update mapping in brig + mCid <- + nkpresClientIdentity + <$$> validateAndAddKeyPackageRef + NewKeyPackage + { nkpConversation = qUntagged lconv, + nkpKeyPackage = kpdata + } + cid <- mCid & note (mlsProtocolError "Tried to add invalid KeyPackage") + let qcid = cidQualifiedClient cid + let qusr = fst <$> qcid + -- update mapping in galley + addMLSClients lconv qusr (Set.singleton (ciClient cid, ref)) + pure cid +applyProposal _conv (RemoveProposal ref) = do qclient <- cidQualifiedClient <$> derefKeyPackage ref - pure (paRemoveClient qclient) -applyProposal _ = pure mempty + pure (paRemoveClient ((,ref) <$$> qclient)) +applyProposal _conv _ = pure mempty checkProposalCipherSuite :: Members @@ -616,7 +896,9 @@ checkExternalProposalUser qusr prop = do either (const $ throwS @'MLSUnsupportedProposal) pure - $ decodeMLS' @ClientIdentity (bcIdentity . kpCredential . rmValue $ keyPackage) + . kpIdentity + . rmValue + $ keyPackage -- requesting user must match key package owner when (tUnqualified lusr /= ciUser) $ throwS @'MLSUnsupportedProposal -- client referenced in key package must be one of the user's clients @@ -634,6 +916,7 @@ executeProposalAction :: forall r. ( Member BrigAccess r, Member ConversationStore r, + Member (Error InternalError) r, Member (ErrorS 'ConvNotFound) r, Member (Error FederationError) r, Member (ErrorS 'MLSClientMismatch) r, @@ -645,21 +928,24 @@ executeProposalAction :: Member ExternalAccess r, Member FederatorAccess r, Member GundeckAccess r, + Member (Input Env) r, Member (Input Opts) r, Member (Input UTCTime) r, Member LegalHoldStore r, Member MemberStore r, - Member TeamStore r + Member ProposalStore r, + Member TeamStore r, + Member TinyLog r ) => Qualified UserId -> Maybe ConnId -> Local Data.Conversation -> + ClientMap -> ProposalAction -> Sem r [LocalConversationUpdate] -executeProposalAction qusr con lconv action = do +executeProposalAction qusr con lconv cm action = do cs <- preview (to convProtocol . _ProtocolMLS . to cnvmlsCipherSuite) (tUnqualified lconv) & noteS @'ConvNotFound let ss = csSignatureScheme cs - cm = convClientMap lconv newUserClients = Map.assocs (paAdd action) removeUserClients = Map.assocs (paRemove action) @@ -673,7 +959,7 @@ executeProposalAction qusr con lconv action = do -- new user Nothing -> do -- final set of clients in the conversation - let clients = newclients <> Map.findWithDefault mempty qtarget cm + let clients = Set.map fst (newclients <> Map.findWithDefault mempty qtarget cm) -- get list of mls clients from brig clientInfo <- getMLSClients lconv qtarget ss let allClients = Set.map ciId clientInfo @@ -700,19 +986,29 @@ executeProposalAction qusr con lconv action = do -- add users to the conversation and send events addEvents <- foldMap addMembers . nonEmpty . map fst $ newUserClients - -- add clients to the database + + -- add clients in the conversation state for_ newUserClients $ \(qtarget, newClients) -> do addMLSClients (fmap convId lconv) qtarget newClients -- remove users from the conversation and send events removeEvents <- foldMap removeMembers (nonEmpty membersToRemove) + -- remove clients in the conversation state + for_ removeUserClients $ \(qtarget, clients) -> do + removeMLSClients (fmap convId lconv) qtarget (Set.map fst clients) + pure (addEvents <> removeEvents) where -- This also filters out client removals for clients that don't exist anymore -- For these clients there is nothing left to do - checkRemoval :: Local x -> SignatureSchemeTag -> Qualified UserId -> Set ClientId -> Sem r (Maybe (Qualified UserId)) - checkRemoval loc ss qtarget clients = do + checkRemoval :: + Local x -> + SignatureSchemeTag -> + Qualified UserId -> + Set (ClientId, KeyPackageRef) -> + Sem r (Maybe (Qualified UserId)) + checkRemoval loc ss qtarget (Set.map fst -> clients) = do allClients <- Set.map ciId <$> getMLSClients loc qtarget ss let allClientsDontExist = Set.null (clients `Set.intersection` allClients) if allClientsDontExist @@ -727,109 +1023,47 @@ executeProposalAction qusr con lconv action = do throwS @'MLSSelfRemovalNotAllowed pure (Just qtarget) + existingLocalMembers :: Set (Qualified UserId) + existingLocalMembers = + (Set.fromList . map (fmap lmId . qUntagged)) (traverse convLocalMembers lconv) + + existingRemoteMembers :: Set (Qualified UserId) + existingRemoteMembers = + Set.fromList . map (qUntagged . rmId) . convRemoteMembers . tUnqualified $ + lconv + + existingMembers :: Set (Qualified UserId) + existingMembers = existingLocalMembers <> existingRemoteMembers + addMembers :: NonEmpty (Qualified UserId) -> Sem r [LocalConversationUpdate] - addMembers users = + addMembers = -- FUTUREWORK: update key package ref mapping to reflect conversation membership - handleNoChanges - . handleMLSProposalFailures @ProposalErrors - . fmap pure - . updateLocalConversationUnchecked - @'ConversationJoinTag - lconv - qusr - con - $ ConversationJoin users roleNameWireMember + foldMap + ( handleNoChanges + . handleMLSProposalFailures @ProposalErrors + . fmap pure + . updateLocalConversationUnchecked @'ConversationJoinTag lconv qusr con + . flip ConversationJoin roleNameWireMember + ) + . nonEmpty + . filter (flip Set.notMember existingMembers) + . toList removeMembers :: NonEmpty (Qualified UserId) -> Sem r [LocalConversationUpdate] removeMembers = - handleNoChanges - . handleMLSProposalFailures @ProposalErrors - . fmap pure - . updateLocalConversationUnchecked - @'ConversationRemoveMembersTag - lconv - qusr - con + foldMap + ( handleNoChanges + . handleMLSProposalFailures @ProposalErrors + . fmap pure + . updateLocalConversationUnchecked @'ConversationRemoveMembersTag lconv qusr con + ) + . nonEmpty + . filter (flip Set.member existingMembers) + . toList handleNoChanges :: Monoid a => Sem (Error NoChanges ': r) a -> Sem r a handleNoChanges = fmap fold . runError -convClientMap :: Local Data.Conversation -> ClientMap -convClientMap lconv = - mconcat - [ foldMap localMember . convLocalMembers, - foldMap remoteMember . convRemoteMembers - ] - (tUnqualified lconv) - where - localMember lm = Map.singleton (qUntagged (qualifyAs lconv (lmId lm))) (lmMLSClients lm) - remoteMember rm = Map.singleton (qUntagged (rmId rm)) (rmMLSClients rm) - --- | Propagate a message. -propagateMessage :: - ( Member ExternalAccess r, - Member FederatorAccess r, - Member GundeckAccess r, - Member (Input UTCTime) r, - Member TinyLog r - ) => - Local x -> - Qualified UserId -> - Data.Conversation -> - Maybe ConnId -> - ByteString -> - Sem r () -propagateMessage loc qusr conv con raw = do - -- FUTUREWORK: check the epoch - let lmems = Data.convLocalMembers conv - botMap = Map.fromList $ do - m <- lmems - b <- maybeToList $ newBotMember m - pure (lmId m, b) - mm = defMessageMetadata - now <- input @UTCTime - let lcnv = qualifyAs loc (Data.convId conv) - qcnv = qUntagged lcnv - e = Event qcnv qusr now $ EdMLSMessage raw - lclients = tUnqualified . clients <$> lmems - mkPush :: UserId -> ClientId -> MessagePush 'NormalMessage - mkPush u c = newMessagePush lcnv botMap con mm (u, c) e - runMessagePush loc (Just qcnv) $ - foldMap (uncurry mkPush) (cToList =<< lclients) - - -- send to remotes - traverse_ handleError <=< runFederatedConcurrentlyEither (map remoteMemberQualify (Data.convRemoteMembers conv)) $ - \(tUnqualified -> rs) -> - fedClient @'Galley @"on-mls-message-sent" $ - RemoteMLSMessage - { rmmTime = now, - rmmSender = qusr, - rmmMetadata = mm, - rmmConversation = tUnqualified lcnv, - rmmRecipients = rs >>= remoteMemberMLSClients, - rmmMessage = Base64ByteString raw - } - where - cToList :: (UserId, Set ClientId) -> [(UserId, ClientId)] - cToList (u, s) = (u,) <$> Set.toList s - - clients :: LocalMember -> Local (UserId, Set ClientId) - clients LocalMember {..} = qualifyAs loc (lmId, lmMLSClients) - - remoteMemberMLSClients :: RemoteMember -> [(UserId, ClientId)] - remoteMemberMLSClients rm = - map - (tUnqualified (rmId rm),) - (toList (rmMLSClients rm)) - - handleError :: Member TinyLog r => Either (Remote [a], FederationError) x -> Sem r () - handleError (Right _) = pure () - handleError (Left (r, e)) = - warn $ - Logger.msg ("A message could not be delivered to a remote backend" :: ByteString) - . Logger.field "remote_domain" (domainText (tDomain r)) - . logErrorMsg (toWai e) - getMLSClients :: Members '[BrigAccess, FederatorAccess] r => Local x -> diff --git a/services/galley/src/Galley/API/MLS/Propagate.hs b/services/galley/src/Galley/API/MLS/Propagate.hs new file mode 100644 index 0000000000..8356619baa --- /dev/null +++ b/services/galley/src/Galley/API/MLS/Propagate.hs @@ -0,0 +1,114 @@ +-- 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 Galley.API.MLS.Propagate where + +import Control.Comonad +import Data.Domain +import Data.Id +import Data.Json.Util +import qualified Data.Map as Map +import Data.Qualified +import Data.Time +import Galley.API.MLS.Types +import Galley.API.Push +import qualified Galley.Data.Conversation.Types as Data +import Galley.Data.Services +import Galley.Effects +import Galley.Effects.FederatorAccess +import Galley.Types.Conversations.Members +import Imports +import Network.Wai.Utilities.Server +import Polysemy +import Polysemy.Input +import Polysemy.TinyLog +import qualified System.Logger.Class as Logger +import Wire.API.Error +import Wire.API.Event.Conversation +import Wire.API.Federation.API +import Wire.API.Federation.API.Galley +import Wire.API.Federation.Error +import Wire.API.Message + +-- | Propagate a message. +propagateMessage :: + ( Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input UTCTime) r, + Member TinyLog r + ) => + Qualified UserId -> + Local Data.Conversation -> + ClientMap -> + Maybe ConnId -> + ByteString -> + Sem r () +propagateMessage qusr lconv cm con raw = do + -- FUTUREWORK: check the epoch + let lmems = Data.convLocalMembers . tUnqualified $ lconv + botMap = Map.fromList $ do + m <- lmems + b <- maybeToList $ newBotMember m + pure (lmId m, b) + mm = defMessageMetadata + now <- input @UTCTime + let lcnv = fmap Data.convId lconv + qcnv = qUntagged lcnv + e = Event qcnv qusr now $ EdMLSMessage raw + mkPush :: UserId -> ClientId -> MessagePush 'NormalMessage + mkPush u c = newMessagePush lcnv botMap con mm (u, c) e + runMessagePush lconv (Just qcnv) $ + foldMap (uncurry mkPush) (lmems >>= localMemberMLSClients lcnv) + + -- send to remotes + traverse_ handleError + <=< runFederatedConcurrentlyEither (map remoteMemberQualify (Data.convRemoteMembers . tUnqualified $ lconv)) + $ \(tUnqualified -> rs) -> + fedClient @'Galley @"on-mls-message-sent" $ + RemoteMLSMessage + { rmmTime = now, + rmmSender = qusr, + rmmMetadata = mm, + rmmConversation = tUnqualified lcnv, + rmmRecipients = rs >>= remoteMemberMLSClients, + rmmMessage = Base64ByteString raw + } + where + localMemberMLSClients :: Local x -> LocalMember -> [(UserId, ClientId)] + localMemberMLSClients loc lm = + let localUserQId = qUntagged (qualifyAs loc localUserId) + localUserId = lmId lm + in map + (\(c, _) -> (localUserId, c)) + (toList (Map.findWithDefault mempty localUserQId cm)) + + remoteMemberMLSClients :: RemoteMember -> [(UserId, ClientId)] + remoteMemberMLSClients rm = + let remoteUserQId = qUntagged (rmId rm) + remoteUserId = qUnqualified remoteUserQId + in map + (\(c, _) -> (remoteUserId, c)) + (toList (Map.findWithDefault mempty remoteUserQId cm)) + + handleError :: Member TinyLog r => Either (Remote [a], FederationError) x -> Sem r () + handleError (Right _) = pure () + handleError (Left (r, e)) = + warn $ + Logger.msg ("A message could not be delivered to a remote backend" :: ByteString) + . Logger.field "remote_domain" (domainText (tDomain r)) + . logErrorMsg (toWai e) diff --git a/services/galley/src/Galley/API/MLS/Removal.hs b/services/galley/src/Galley/API/MLS/Removal.hs new file mode 100644 index 0000000000..43c6375a1c --- /dev/null +++ b/services/galley/src/Galley/API/MLS/Removal.hs @@ -0,0 +1,157 @@ +-- 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 Galley.API.MLS.Removal + ( removeClientsWithClientMap, + removeClient, + removeUserWithClientMap, + removeUser, + ) +where + +import Control.Comonad +import Control.Lens (view) +import Data.Id +import qualified Data.Map as Map +import Data.Qualified +import qualified Data.Set as Set +import Data.Time +import Galley.API.Error +import Galley.API.MLS.Propagate +import Galley.API.MLS.Types +import qualified Galley.Data.Conversation.Types as Data +import Galley.Effects +import Galley.Effects.MemberStore +import Galley.Effects.ProposalStore +import Galley.Env +import Imports +import Polysemy +import Polysemy.Error +import Polysemy.Input +import Polysemy.TinyLog +import Wire.API.Conversation.Protocol +import Wire.API.MLS.Credential +import Wire.API.MLS.KeyPackage +import Wire.API.MLS.Keys +import Wire.API.MLS.Message +import Wire.API.MLS.Proposal +import Wire.API.MLS.Serialisation + +-- | Send remove proposals for a set of clients to clients in the ClientMap. +removeClientsWithClientMap :: + ( Members + '[ Input UTCTime, + TinyLog, + ExternalAccess, + FederatorAccess, + GundeckAccess, + Error InternalError, + ProposalStore, + Input Env + ] + r + ) => + Local Data.Conversation -> + Set (ClientId, KeyPackageRef) -> + ClientMap -> + Qualified UserId -> + Sem r () +removeClientsWithClientMap lc cs cm qusr = do + case Data.convProtocol (tUnqualified lc) of + ProtocolProteus -> pure () + ProtocolMLS meta -> do + keyPair <- mlsKeyPair_ed25519 <$> (inputs (view mlsKeys) <*> pure RemovalPurpose) + (secKey, pubKey) <- note (InternalErrorWithDescription "backend removal key missing") $ keyPair + for_ cs $ \(_client, kpref) -> do + let proposal = mkRemoveProposal kpref + msg = mkSignedMessage secKey pubKey (cnvmlsGroupId meta) (cnvmlsEpoch meta) (ProposalMessage proposal) + msgEncoded = encodeMLS' msg + storeProposal + (cnvmlsGroupId meta) + (cnvmlsEpoch meta) + (proposalRef (cnvmlsCipherSuite meta) proposal) + proposal + propagateMessage qusr lc cm Nothing msgEncoded + +-- | Send remove proposals for a single client of a user to the local conversation. +removeClient :: + ( Members + '[ Error InternalError, + ExternalAccess, + FederatorAccess, + GundeckAccess, + Input Env, + Input UTCTime, + MemberStore, + ProposalStore, + TinyLog + ] + r + ) => + Local Data.Conversation -> + Qualified UserId -> + ClientId -> + Sem r () +removeClient lc qusr cid = do + cm <- lookupMLSClients (fmap Data.convId lc) + let cidAndKP = Set.filter ((==) cid . fst) $ Map.findWithDefault mempty qusr cm + removeClientsWithClientMap lc cidAndKP cm qusr + +-- | Send remove proposals for all clients of the user to clients in the ClientMap. +-- +-- All clients of the user have to be contained in the ClientMap. +removeUserWithClientMap :: + ( Members + '[ Input UTCTime, + TinyLog, + ExternalAccess, + FederatorAccess, + GundeckAccess, + Error InternalError, + ProposalStore, + Input Env + ] + r + ) => + Local Data.Conversation -> + ClientMap -> + Qualified UserId -> + Sem r () +removeUserWithClientMap lc cm qusr = + removeClientsWithClientMap lc (Map.findWithDefault mempty qusr cm) cm qusr + +-- | Send remove proposals for all clients of the user to the local conversation. +removeUser :: + ( Members + '[ Error InternalError, + ExternalAccess, + FederatorAccess, + GundeckAccess, + Input Env, + Input UTCTime, + MemberStore, + ProposalStore, + TinyLog + ] + r + ) => + Local Data.Conversation -> + Qualified UserId -> + Sem r () +removeUser lc qusr = do + cm <- lookupMLSClients (fmap Data.convId lc) + removeUserWithClientMap lc cm qusr diff --git a/services/galley/src/Galley/API/MLS/Types.hs b/services/galley/src/Galley/API/MLS/Types.hs new file mode 100644 index 0000000000..f9b6cefb8e --- /dev/null +++ b/services/galley/src/Galley/API/MLS/Types.hs @@ -0,0 +1,43 @@ +-- 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 Galley.API.MLS.Types + ( ClientMap, + mkClientMap, + cmAssocs, + ) +where + +import Data.Domain +import Data.Id +import qualified Data.Map as Map +import Data.Qualified +import qualified Data.Set as Set +import Imports +import Wire.API.MLS.KeyPackage + +type ClientMap = Map (Qualified UserId) (Set (ClientId, KeyPackageRef)) + +mkClientMap :: [(Domain, UserId, ClientId, KeyPackageRef)] -> ClientMap +mkClientMap = foldr addEntry mempty + where + addEntry :: (Domain, UserId, ClientId, KeyPackageRef) -> ClientMap -> ClientMap + addEntry (dom, usr, c, kpr) = + Map.insertWith (<>) (Qualified usr dom) (Set.singleton (c, kpr)) + +cmAssocs :: ClientMap -> [(Qualified UserId, (ClientId, KeyPackageRef))] +cmAssocs cm = Map.assocs cm >>= traverse toList diff --git a/services/galley/src/Galley/API/MLS/Util.hs b/services/galley/src/Galley/API/MLS/Util.hs new file mode 100644 index 0000000000..304926137c --- /dev/null +++ b/services/galley/src/Galley/API/MLS/Util.hs @@ -0,0 +1,54 @@ +-- 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 Galley.API.MLS.Util where + +import Control.Comonad +import Data.Id +import Data.Qualified +import Galley.API.Util +import Galley.Data.Conversation.Types hiding (Conversation) +import qualified Galley.Data.Conversation.Types as Data +import Galley.Effects +import Galley.Effects.ConversationStore +import Galley.Effects.MemberStore +import Imports +import Polysemy +import Polysemy.Input +import Wire.API.Error +import Wire.API.Error.Galley + +getLocalConvForUser :: + Members + '[ ErrorS 'ConvNotFound, + ConversationStore, + Input (Local ()), + MemberStore + ] + r => + Qualified UserId -> + Local ConvId -> + Sem r Data.Conversation +getLocalConvForUser qusr lcnv = do + conv <- getConversation (tUnqualified lcnv) >>= noteS @'ConvNotFound + + -- check that sender is part of conversation + loc <- qualifyLocal () + isMember' <- foldQualified loc (fmap isJust . getLocalMember (convId conv) . tUnqualified) (fmap isJust . getRemoteMember (convId conv)) qusr + unless isMember' $ throwS @'ConvNotFound + + pure conv diff --git a/services/galley/src/Galley/API/MLS/Welcome.hs b/services/galley/src/Galley/API/MLS/Welcome.hs index c249e3d03d..2e52592d9c 100644 --- a/services/galley/src/Galley/API/MLS/Welcome.hs +++ b/services/galley/src/Galley/API/MLS/Welcome.hs @@ -17,6 +17,7 @@ module Galley.API.MLS.Welcome ( postMLSWelcome, + postMLSWelcomeFromLocalUser, sendLocalWelcomes, ) where @@ -60,17 +61,33 @@ postMLSWelcome :: P.TinyLog ] r => - Local UserId -> - ConnId -> + Local x -> + Maybe ConnId -> RawMLS Welcome -> Sem r () -postMLSWelcome lusr con wel = do +postMLSWelcome loc con wel = do now <- input rcpts <- welcomeRecipients (rmValue wel) - let (locals, remotes) = partitionQualified lusr rcpts - sendLocalWelcomes (Just con) now (rmRaw wel) (qualifyAs lusr locals) + let (locals, remotes) = partitionQualified loc rcpts + sendLocalWelcomes con now (rmRaw wel) (qualifyAs loc locals) sendRemoteWelcomes (rmRaw wel) remotes +postMLSWelcomeFromLocalUser :: + Members + '[ BrigAccess, + FederatorAccess, + GundeckAccess, + ErrorS 'MLSKeyPackageRefNotFound, + Input UTCTime, + P.TinyLog + ] + r => + Local x -> + ConnId -> + RawMLS Welcome -> + Sem r () +postMLSWelcomeFromLocalUser loc con wel = postMLSWelcome loc (Just con) wel + welcomeRecipients :: Members '[ BrigAccess, diff --git a/services/galley/src/Galley/API/Mapping.hs b/services/galley/src/Galley/API/Mapping.hs index 3481feb6f6..6e97ab8adc 100644 --- a/services/galley/src/Galley/API/Mapping.hs +++ b/services/galley/src/Galley/API/Mapping.hs @@ -27,7 +27,6 @@ where import Data.Domain (Domain) import Data.Id (UserId, idToText) import Data.Qualified -import qualified Data.Set as Set import Galley.API.Error import qualified Galley.Data.Conversation as Data import Galley.Data.Types (convId) @@ -95,8 +94,7 @@ remoteConversationView uid status (qUntagged -> Qualified rconv rDomain) = { lmId = tUnqualified uid, lmService = Nothing, lmStatus = status, - lmConvRoleName = rcmSelfRole mems, - lmMLSClients = Set.empty + lmConvRoleName = rcmSelfRole mems } in Conversation (Qualified (rcnvId rconv) rDomain) diff --git a/services/galley/src/Galley/API/Public/Servant.hs b/services/galley/src/Galley/API/Public/Servant.hs index 21378d5278..9e32a4aa93 100644 --- a/services/galley/src/Galley/API/Public/Servant.hs +++ b/services/galley/src/Galley/API/Public/Servant.hs @@ -21,6 +21,7 @@ import Galley.API.Create import Galley.API.CustomBackend import Galley.API.LegalHold import Galley.API.MLS +import Galley.API.MLS.GroupInfo import Galley.API.Query import Galley.API.Teams import Galley.API.Teams.Features @@ -47,8 +48,10 @@ servantSitemap = where conversations = mkNamedAPI @"get-unqualified-conversation" getUnqualifiedConversation + <@> mkNamedAPI @"get-unqualified-conversation-legalhold-alias" getUnqualifiedConversation <@> mkNamedAPI @"get-conversation" getConversation <@> mkNamedAPI @"get-conversation-roles" getConversationRoles + <@> mkNamedAPI @"get-group-info" getGroupInfo <@> mkNamedAPI @"list-conversation-ids-unqualified" conversationIdsPageFromUnqualified <@> mkNamedAPI @"list-conversation-ids" conversationIdsPageFrom <@> mkNamedAPI @"get-conversations" getConversations @@ -110,6 +113,7 @@ servantSitemap = <@> mkNamedAPI @"get-team" getTeamH <@> mkNamedAPI @"delete-team" deleteTeam + features :: API FeatureAPI GalleyEffects features = mkNamedAPI @'("get", SSOConfig) (getFeatureStatus @Cassandra . DoAuth) <@> mkNamedAPI @'("get", LegalholdConfig) (getFeatureStatus @Cassandra . DoAuth) @@ -138,6 +142,8 @@ servantSitemap = <@> mkNamedAPI @'("put", SndFactorPasswordChallengeConfig) (setFeatureStatus @Cassandra . DoAuth) <@> mkNamedAPI @'("get", MLSConfig) (getFeatureStatus @Cassandra . DoAuth) <@> mkNamedAPI @'("put", MLSConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", ExposeInvitationURLsToTeamAdminConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("put", ExposeInvitationURLsToTeamAdminConfig) (setFeatureStatus @Cassandra . DoAuth) <@> mkNamedAPI @'("get", SearchVisibilityInboundConfig) (getFeatureStatus @Cassandra . DoAuth) <@> mkNamedAPI @'("put", SearchVisibilityInboundConfig) (setFeatureStatus @Cassandra . DoAuth) <@> mkNamedAPI @"get-all-feature-configs-for-user" (getAllFeatureConfigsForUser @Cassandra) @@ -158,9 +164,10 @@ servantSitemap = mls :: API MLSAPI GalleyEffects mls = - mkNamedAPI @"mls-welcome-message" postMLSWelcome + mkNamedAPI @"mls-welcome-message" postMLSWelcomeFromLocalUser <@> mkNamedAPI @"mls-message-v1" postMLSMessageFromLocalUserV1 <@> mkNamedAPI @"mls-message" postMLSMessageFromLocalUser + <@> mkNamedAPI @"mls-commit-bundle" postMLSCommitBundleFromLocalUser <@> mkNamedAPI @"mls-public-keys" getMLSPublicKeys customBackend :: API CustomBackendAPI GalleyEffects diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index ca2643e561..bff05ef910 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -579,12 +579,12 @@ getTeamMembersCSV lusr tid = do lookupInviterHandle :: Member BrigAccess r => [TeamMember] -> Sem r (UserId -> Maybe Handle.Handle) lookupInviterHandle members = do let inviterIds :: [UserId] - inviterIds = nub $ catMaybes $ fmap fst . view invitation <$> members + inviterIds = nub $ mapMaybe (fmap fst . view invitation) members userList :: [User] <- accountUser <$$> E.getUsers inviterIds let userMap :: M.Map UserId Handle.Handle - userMap = M.fromList . catMaybes $ extract <$> userList + userMap = M.fromList (mapMaybe extract userList) where extract u = (U.userId u,) <$> U.userHandle u @@ -1061,6 +1061,7 @@ deleteTeamConversation :: ExternalAccess, FederatorAccess, GundeckAccess, + Input Env, Input UTCTime, TeamStore ] diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index 260781cec4..4556f935d7 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -52,10 +52,12 @@ import Galley.API.LegalHold (isLegalHoldEnabledForTeam) import qualified Galley.API.LegalHold as LegalHold import Galley.API.Teams (ensureNotTooLargeToActivateLegalHold) import Galley.API.Util (assertTeamExists, getTeamMembersForFanout, membersToRecipients, permissionCheck) +import Galley.App import Galley.Effects import Galley.Effects.BrigAccess (getAccountConferenceCallingConfigClient, updateSearchVisibilityInbound) import Galley.Effects.ConversationStore as ConversationStore import Galley.Effects.GundeckAccess +import Galley.Effects.ProposalStore import qualified Galley.Effects.SearchVisibilityStore as SearchVisibilityData import Galley.Effects.TeamFeatureStore import qualified Galley.Effects.TeamFeatureStore as TeamFeatures @@ -177,7 +179,8 @@ type FeaturePersistentAllFeatures db = FeaturePersistentConstraint db GuestLinksConfig, FeaturePersistentConstraint db SndFactorPasswordChallengeConfig, FeaturePersistentConstraint db MLSConfig, - FeaturePersistentConstraint db SearchVisibilityInboundConfig + FeaturePersistentConstraint db SearchVisibilityInboundConfig, + FeaturePersistentConstraint db ExposeInvitationURLsToTeamAdminConfig ) getFeatureStatus :: @@ -438,6 +441,7 @@ getAllFeatureConfigsForServer = <*> getConfigForServer @db @GuestLinksConfig <*> getConfigForServer @db @SndFactorPasswordChallengeConfig <*> getConfigForServer @db @MLSConfig + <*> getConfigForServer @db @ExposeInvitationURLsToTeamAdminConfig getAllFeatureConfigsUser :: forall db r. @@ -471,6 +475,7 @@ getAllFeatureConfigsUser uid = <*> getConfigForUser @db @GuestLinksConfig uid <*> getConfigForUser @db @SndFactorPasswordChallengeConfig uid <*> getConfigForUser @db @MLSConfig uid + <*> getConfigForUser @db @ExposeInvitationURLsToTeamAdminConfig uid getAllFeatureConfigsTeam :: forall db r. @@ -503,6 +508,7 @@ getAllFeatureConfigsTeam tid = <*> getConfigForTeam @db @GuestLinksConfig tid <*> getConfigForTeam @db @SndFactorPasswordChallengeConfig tid <*> getConfigForTeam @db @MLSConfig tid + <*> getConfigForTeam @db @ExposeInvitationURLsToTeamAdminConfig tid -- | Note: this is an internal function which doesn't cover all features, e.g. LegalholdConfig genericGetConfigForTeam :: @@ -726,10 +732,12 @@ instance SetFeatureConfig db LegalholdConfig where FireAndForget, GundeckAccess, Input (Local ()), + Input Env, Input UTCTime, LegalHoldStore, ListItems LegacyPaging ConvId, MemberStore, + ProposalStore, TeamFeatureStore db, TeamStore, TeamMemberStore InternalPaging, @@ -846,6 +854,44 @@ instance SetFeatureConfig db MLSConfig where setConfigForTeam tid wsnl = do persistAndPushEvent @db tid wsnl +instance GetFeatureConfig db ExposeInvitationURLsToTeamAdminConfig where + getConfigForServer = + -- we could look at the galley settings, but we don't have a team here, so there is not much else we can say. + pure $ + withStatus + FeatureStatusDisabled + LockStatusLocked + ExposeInvitationURLsToTeamAdminConfig + FeatureTTLUnlimited + + getConfigForTeam tid = do + allowList <- input <&> view (optSettings . setExposeInvitationURLsTeamAllowlist . to (fromMaybe [])) + mbOldStatus <- TeamFeatures.getFeatureConfig @db (Proxy @ExposeInvitationURLsToTeamAdminConfig) tid <&> fmap wssStatus + let teamAllowed = tid `elem` allowList + pure $ computeConfigForTeam teamAllowed (fromMaybe FeatureStatusDisabled mbOldStatus) + where + computeConfigForTeam :: Bool -> FeatureStatus -> WithStatus ExposeInvitationURLsToTeamAdminConfig + computeConfigForTeam teamAllowed teamDbStatus = + if teamAllowed + then makeConfig LockStatusUnlocked teamDbStatus + else makeConfig LockStatusLocked FeatureStatusDisabled + + makeConfig :: LockStatus -> FeatureStatus -> WithStatus ExposeInvitationURLsToTeamAdminConfig + makeConfig lockStatus status = + withStatus + status + lockStatus + ExposeInvitationURLsToTeamAdminConfig + FeatureTTLUnlimited + +instance SetFeatureConfig db ExposeInvitationURLsToTeamAdminConfig where + type SetConfigForTeamConstraints db ExposeInvitationURLsToTeamAdminConfig (r :: EffectRow) = (Member (ErrorS OperationDenied) r) + setConfigForTeam tid wsnl = do + lockStatus <- getConfigForTeam @db @ExposeInvitationURLsToTeamAdminConfig tid <&> wsLockStatus + case lockStatus of + LockStatusLocked -> throwS @OperationDenied + LockStatusUnlocked -> persistAndPushEvent @db tid wsnl + -- -- | If second factor auth is enabled, make sure that end-points that don't support it, but should, are blocked completely. (This is a workaround until we have 2FA for those end-points as well.) -- -- -- This function exists to resolve a cyclic dependency. diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index 4257c7b863..27b2d6c806 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -85,6 +85,7 @@ import Galley.API.Mapping import Galley.API.Message import qualified Galley.API.Query as Query import Galley.API.Util +import Galley.App import qualified Galley.Data.Conversation as Data import Galley.Data.Services as Data import Galley.Data.Types hiding (Conversation) @@ -96,6 +97,7 @@ import qualified Galley.Effects.ExternalAccess as E import qualified Galley.Effects.FederatorAccess as E import qualified Galley.Effects.GundeckAccess as E import qualified Galley.Effects.MemberStore as E +import Galley.Effects.ProposalStore import qualified Galley.Effects.ServiceStore as E import Galley.Effects.TeamFeatureStore (FeaturePersistentConstraint) import Galley.Effects.WaiRoutes @@ -265,20 +267,24 @@ type UpdateConversationAccessEffects = BrigAccess, CodeStore, ConversationStore, - ExternalAccess, - FederatorAccess, - FireAndForget, - GundeckAccess, - MemberStore, - TeamStore, - Error InvalidInput, Error FederationError, + Error InternalError, + Error InvalidInput, ErrorS ('ActionDenied 'ModifyConversationAccess), ErrorS ('ActionDenied 'RemoveConversationMember), ErrorS 'ConvNotFound, ErrorS 'InvalidOperation, ErrorS 'InvalidTargetAccess, - Input UTCTime + ExternalAccess, + FederatorAccess, + FireAndForget, + GundeckAccess, + Input Env, + Input UTCTime, + MemberStore, + ProposalStore, + TeamStore, + TinyLog ] updateConversationAccess :: @@ -310,18 +316,19 @@ updateConversationAccessUnqualified lusr con cnv update = updateConversationReceiptMode :: Members - '[ Error FederationError, + '[ BrigAccess, + ConversationStore, + Error FederationError, ErrorS ('ActionDenied 'ModifyConversationReceiptMode), ErrorS 'ConvNotFound, ErrorS 'InvalidOperation, ExternalAccess, FederatorAccess, GundeckAccess, - BrigAccess, - ConversationStore, - MemberStore, - Input UTCTime, Input (Local ()), + Input Env, + Input UTCTime, + MemberStore, TinyLog ] r => @@ -385,7 +392,8 @@ updateRemoteConversation rcnv lusr conn action = getUpdateResult $ do updateConversationReceiptModeUnqualified :: Members - '[ ConversationStore, + '[ BrigAccess, + ConversationStore, Error FederationError, ErrorS ('ActionDenied 'ModifyConversationReceiptMode), ErrorS 'ConvNotFound, @@ -393,10 +401,10 @@ updateConversationReceiptModeUnqualified :: ExternalAccess, FederatorAccess, GundeckAccess, - BrigAccess, - MemberStore, - Input UTCTime, Input (Local ()), + Input Env, + Input UTCTime, + MemberStore, TinyLog ] r => @@ -417,6 +425,7 @@ updateConversationMessageTimer :: ExternalAccess, FederatorAccess, GundeckAccess, + Input Env, Input UTCTime ] r => @@ -451,6 +460,7 @@ updateConversationMessageTimerUnqualified :: ExternalAccess, FederatorAccess, GundeckAccess, + Input Env, Input UTCTime ] r => @@ -473,6 +483,7 @@ deleteLocalConversation :: ExternalAccess, FederatorAccess, GundeckAccess, + Input Env, Input UTCTime, TeamStore ] @@ -765,6 +776,7 @@ joinConversation lusr zcon conv access = do <$> notifyConversationAction (sing @'ConversationJoinTag) (qUntagged lusr) + False (Just zcon) (qualifyAs lusr conv) (convBotsAndMembers conv <> extraTargets) @@ -775,6 +787,7 @@ addMembers :: '[ BrigAccess, ConversationStore, Error FederationError, + Error InternalError, ErrorS ('ActionDenied 'AddConversationMember), ErrorS ('ActionDenied 'LeaveConversation), ErrorS 'ConvAccessDenied, @@ -787,11 +800,14 @@ addMembers :: ExternalAccess, FederatorAccess, GundeckAccess, + Input Env, Input Opts, Input UTCTime, LegalHoldStore, MemberStore, - TeamStore + ProposalStore, + TeamStore, + TinyLog ] r => Local UserId -> @@ -810,6 +826,7 @@ addMembersUnqualifiedV2 :: '[ BrigAccess, ConversationStore, Error FederationError, + Error InternalError, ErrorS ('ActionDenied 'AddConversationMember), ErrorS ('ActionDenied 'LeaveConversation), ErrorS 'ConvAccessDenied, @@ -822,11 +839,14 @@ addMembersUnqualifiedV2 :: ExternalAccess, FederatorAccess, GundeckAccess, + Input Env, Input Opts, Input UTCTime, LegalHoldStore, MemberStore, - TeamStore + ProposalStore, + TeamStore, + TinyLog ] r => Local UserId -> @@ -845,6 +865,7 @@ addMembersUnqualified :: '[ BrigAccess, ConversationStore, Error FederationError, + Error InternalError, ErrorS ('ActionDenied 'AddConversationMember), ErrorS ('ActionDenied 'LeaveConversation), ErrorS 'ConvAccessDenied, @@ -857,11 +878,14 @@ addMembersUnqualified :: ExternalAccess, FederatorAccess, GundeckAccess, + Input Env, Input Opts, Input UTCTime, LegalHoldStore, MemberStore, - TeamStore + ProposalStore, + TeamStore, + TinyLog ] r => Local UserId -> @@ -952,6 +976,7 @@ updateOtherMemberLocalConv :: ExternalAccess, FederatorAccess, GundeckAccess, + Input Env, Input UTCTime, MemberStore ] @@ -979,6 +1004,7 @@ updateOtherMemberUnqualified :: ExternalAccess, FederatorAccess, GundeckAccess, + Input Env, Input UTCTime, MemberStore ] @@ -1006,6 +1032,7 @@ updateOtherMember :: ExternalAccess, FederatorAccess, GundeckAccess, + Input Env, Input UTCTime, MemberStore ] @@ -1033,14 +1060,18 @@ updateOtherMemberRemoteConv _ _ _ _ _ = throw FederationNotImplemented removeMemberUnqualified :: Members '[ ConversationStore, + Error InternalError, ErrorS ('ActionDenied 'RemoveConversationMember), ErrorS 'ConvNotFound, ErrorS 'InvalidOperation, ExternalAccess, FederatorAccess, GundeckAccess, + Input Env, Input UTCTime, - MemberStore + MemberStore, + ProposalStore, + TinyLog ] r => Local UserId -> @@ -1056,14 +1087,18 @@ removeMemberUnqualified lusr con cnv victim = do removeMemberQualified :: Members '[ ConversationStore, + Error InternalError, ErrorS ('ActionDenied 'RemoveConversationMember), ErrorS 'ConvNotFound, ErrorS 'InvalidOperation, ExternalAccess, FederatorAccess, GundeckAccess, + Input Env, Input UTCTime, - MemberStore + MemberStore, + ProposalStore, + TinyLog ] r => Local UserId -> @@ -1120,6 +1155,7 @@ removeMemberFromRemoteConv cnv lusr victim removeMemberFromLocalConv :: Members '[ ConversationStore, + Error InternalError, ErrorS ('ActionDenied 'LeaveConversation), ErrorS ('ActionDenied 'RemoveConversationMember), ErrorS 'ConvNotFound, @@ -1127,8 +1163,11 @@ removeMemberFromLocalConv :: ExternalAccess, FederatorAccess, GundeckAccess, + Input Env, Input UTCTime, - MemberStore + MemberStore, + ProposalStore, + TinyLog ] r => Local ConvId -> @@ -1141,8 +1180,7 @@ removeMemberFromLocalConv lcnv lusr con victim fmap (fmap lcuEvent . hush) . runError @NoChanges . updateLocalConversation @'ConversationLeaveTag lcnv (qUntagged lusr) con - . pure - $ victim + $ () | otherwise = fmap (fmap lcuEvent . hush) . runError @NoChanges @@ -1335,6 +1373,7 @@ updateConversationName :: ExternalAccess, FederatorAccess, GundeckAccess, + Input Env, Input UTCTime ] r => @@ -1361,6 +1400,7 @@ updateUnqualifiedConversationName :: ExternalAccess, FederatorAccess, GundeckAccess, + Input Env, Input UTCTime ] r => @@ -1383,6 +1423,7 @@ updateLocalConversationName :: ExternalAccess, FederatorAccess, GundeckAccess, + Input Env, Input UTCTime ] r => diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index 62908700dd..34d890bb55 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -391,6 +391,7 @@ data BotsAndMembers = BotsAndMembers bmRemotes :: Set (Remote UserId), bmBots :: Set BotMember } + deriving (Show) bmQualifiedMembers :: Local x -> BotsAndMembers -> [Qualified UserId] bmQualifiedMembers loc bm = diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index a145b9b446..97cd4b2bf4 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -165,8 +165,8 @@ initCassandra o l = do c <- maybe (C.initialContactsPlain (o ^. optCassandra . casEndpoint . epHost)) - (C.initialContactsDisco "cassandra_galley") - (unpack <$> o ^. optDiscoUrl) + (C.initialContactsDisco "cassandra_galley" . unpack) + (o ^. optDiscoUrl) C.init . C.setLogger (C.mkLogger (Logger.clone (Just "cassandra.galley") l)) . C.setContacts (NE.head c) (NE.tail c) diff --git a/services/galley/src/Galley/Cassandra.hs b/services/galley/src/Galley/Cassandra.hs index 4d70622b88..f01b95a4f3 100644 --- a/services/galley/src/Galley/Cassandra.hs +++ b/services/galley/src/Galley/Cassandra.hs @@ -20,4 +20,4 @@ module Galley.Cassandra (schemaVersion) where import Imports schemaVersion :: Int32 -schemaVersion = 70 +schemaVersion = 75 diff --git a/services/galley/src/Galley/Cassandra/Conversation.hs b/services/galley/src/Galley/Cassandra/Conversation.hs index 9956105bb6..a245c29269 100644 --- a/services/galley/src/Galley/Cassandra/Conversation.hs +++ b/services/galley/src/Galley/Cassandra/Conversation.hs @@ -54,6 +54,7 @@ import Wire.API.Conversation hiding (Conversation, Member) import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite import Wire.API.MLS.Group +import Wire.API.MLS.PublicGroupState createConversation :: Local ConvId -> NewConversation -> Client Conversation createConversation lcnv nc = do @@ -133,6 +134,17 @@ conversationMeta conv = accessRoles = maybeRole t $ parseAccessRoles r mbAccessRolesV2 pure $ ConversationMetadata t c (defAccess t a) accessRoles n i mt rm +getPublicGroupState :: ConvId -> Client (Maybe OpaquePublicGroupState) +getPublicGroupState cid = do + fmap join $ + runIdentity + <$$> retry + x1 + ( query1 + Cql.selectPublicGroupState + (params LocalQuorum (Identity cid)) + ) + isConvAlive :: ConvId -> Client Bool isConvAlive cid = do result <- retry x1 (query1 Cql.isConvDeleted (params LocalQuorum (Identity cid))) @@ -164,6 +176,10 @@ updateConvMessageTimer cid mtimer = retry x5 $ write Cql.updateConvMessageTimer updateConvEpoch :: ConvId -> Epoch -> Client () updateConvEpoch cid epoch = retry x5 $ write Cql.updateConvEpoch (params LocalQuorum (epoch, cid)) +setPublicGroupState :: ConvId -> OpaquePublicGroupState -> Client () +setPublicGroupState conv gib = + write Cql.updatePublicGroupState (params LocalQuorum (gib, conv)) + getConversation :: ConvId -> Client (Maybe Conversation) getConversation conv = do cdata <- UnliftIO.async $ retry x1 (query1 Cql.selectConv (params LocalQuorum (Identity conv))) @@ -302,6 +318,7 @@ interpretConversationStoreToCassandra = interpret $ \case GetConversationIdByGroupId gId -> embedClient $ lookupGroupId gId GetConversations cids -> localConversations cids GetConversationMetadata cid -> embedClient $ conversationMeta cid + GetPublicGroupState cid -> embedClient $ getPublicGroupState cid IsConversationAlive cid -> embedClient $ isConvAlive cid SelectConversations uid cids -> embedClient $ localConversationIdsOf uid cids GetRemoteConversationStatus uid cids -> embedClient $ remoteConversationStatus uid cids @@ -313,5 +330,6 @@ interpretConversationStoreToCassandra = interpret $ \case SetConversationEpoch cid epoch -> embedClient $ updateConvEpoch cid epoch DeleteConversation cid -> embedClient $ deleteConversation cid SetGroupId gId cid -> embedClient $ mapGroupId gId cid + SetPublicGroupState cid gib -> embedClient $ setPublicGroupState cid gib AcquireCommitLock gId epoch ttl -> embedClient $ acquireCommitLock gId epoch ttl ReleaseCommitLock gId epoch -> embedClient $ releaseCommitLock gId epoch diff --git a/services/galley/src/Galley/Cassandra/Conversation/Members.hs b/services/galley/src/Galley/Cassandra/Conversation/Members.hs index 12fa07c4d1..4d2c03fc9d 100644 --- a/services/galley/src/Galley/Cassandra/Conversation/Members.hs +++ b/services/galley/src/Galley/Cassandra/Conversation/Members.hs @@ -32,6 +32,7 @@ import qualified Data.List.Extra as List import Data.Monoid import Data.Qualified import qualified Data.Set as Set +import Galley.API.MLS.Types import Galley.Cassandra.Instances () import qualified Galley.Cassandra.Queries as Cql import Galley.Cassandra.Services @@ -46,6 +47,7 @@ import Polysemy.Input import qualified UnliftIO import Wire.API.Conversation.Member hiding (Member) import Wire.API.Conversation.Role +import Wire.API.MLS.KeyPackage import Wire.API.Provider.Service -- | Add members to a local conversation. @@ -73,7 +75,7 @@ addMembers conv (fmap toUserRole -> UserList lusers rusers) = do setConsistency LocalQuorum for_ chunk $ \(u, r) -> do -- User is local, too, so we add it to both the member and the user table - addPrepQuery Cql.insertMember (conv, u, Nothing, Nothing, r, Nothing) + addPrepQuery Cql.insertMember (conv, u, Nothing, Nothing, r) addPrepQuery Cql.insertUserConv (u, conv) for_ (List.chunksOf 32 rusers) $ \chunk -> do @@ -156,18 +158,16 @@ toMember :: Maybe Bool, Maybe Text, -- conversation role name - Maybe RoleName, - Maybe (Cassandra.Set ClientId) + Maybe RoleName ) -> Maybe LocalMember -toMember (usr, srv, prv, Just 0, omus, omur, oar, oarr, hid, hidr, crn, cs) = +toMember (usr, srv, prv, Just 0, omus, omur, oar, oarr, hid, hidr, crn) = Just $ LocalMember { lmId = usr, lmService = newServiceRef <$> srv <*> prv, lmStatus = toMemberStatus (omus, omur, oar, oarr, hid, hidr), - lmConvRoleName = fromMaybe roleNameWireAdmin crn, - lmMLSClients = maybe Set.empty (Set.fromList . fromSet) cs + lmConvRoleName = fromMaybe roleNameWireAdmin crn } toMember _ = Nothing @@ -175,30 +175,27 @@ newRemoteMemberWithRole :: Remote (UserId, RoleName) -> RemoteMember newRemoteMemberWithRole ur@(qUntagged -> (Qualified (u, r) _)) = RemoteMember { rmId = qualifyAs ur u, - rmConvRoleName = r, - rmMLSClients = mempty + rmConvRoleName = r } lookupRemoteMember :: ConvId -> Domain -> UserId -> Client (Maybe RemoteMember) lookupRemoteMember conv domain usr = do mkMem <$$> retry x1 (query1 Cql.selectRemoteMember (params LocalQuorum (conv, domain, usr))) where - mkMem (role, clients) = + mkMem (Identity role) = RemoteMember { rmId = toRemoteUnsafe domain usr, - rmConvRoleName = role, - rmMLSClients = Set.fromList (fromSet clients) + rmConvRoleName = role } lookupRemoteMembers :: ConvId -> Client [RemoteMember] lookupRemoteMembers conv = do fmap (map mkMem) . retry x1 $ query Cql.selectRemoteMembers (params LocalQuorum (Identity conv)) where - mkMem (domain, usr, role, clients) = + mkMem (domain, usr, role) = RemoteMember { rmId = toRemoteUnsafe domain usr, - rmConvRoleName = role, - rmMLSClients = Set.fromList (fromSet clients) + rmConvRoleName = role } member :: @@ -344,32 +341,26 @@ removeLocalMembersFromRemoteConv (qUntagged -> Qualified conv convDomain) victim setConsistency LocalQuorum for_ victims $ \u -> addPrepQuery Cql.deleteUserRemoteConv (u, convDomain, conv) -addMLSClients :: Local ConvId -> Qualified UserId -> Set.Set ClientId -> Client () -addMLSClients lcnv = - foldQualified - lcnv - (addLocalMLSClients (tUnqualified lcnv)) - (addRemoteMLSClients (tUnqualified lcnv)) +addMLSClients :: Local ConvId -> Qualified UserId -> Set.Set (ClientId, KeyPackageRef) -> Client () +addMLSClients lcnv (Qualified usr domain) cs = retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + for_ cs $ \(c, kpr) -> + addPrepQuery Cql.addMLSClient (tUnqualified lcnv, domain, usr, c, kpr) -addRemoteMLSClients :: ConvId -> Remote UserId -> Set.Set ClientId -> Client () -addRemoteMLSClients cid ruid cs = - retry x5 $ - write - Cql.addRemoteMLSClients - ( params - LocalQuorum - (Cassandra.Set (toList cs), cid, tDomain ruid, tUnqualified ruid) - ) +removeMLSClients :: Local ConvId -> Qualified UserId -> Set.Set ClientId -> Client () +removeMLSClients lcnv (Qualified usr domain) cs = retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + for_ cs $ \c -> + addPrepQuery Cql.removeMLSClient (tUnqualified lcnv, domain, usr, c) -addLocalMLSClients :: ConvId -> Local UserId -> Set.Set ClientId -> Client () -addLocalMLSClients cid lusr cs = - retry x5 $ - write - Cql.addLocalMLSClients - ( params - LocalQuorum - (Cassandra.Set (toList cs), cid, tUnqualified lusr) - ) +lookupMLSClients :: Local ConvId -> Client ClientMap +lookupMLSClients lcnv = + mkClientMap + <$> retry + x5 + (query Cql.lookupMLSClients (params LocalQuorum (Identity (tUnqualified lcnv)))) interpretMemberStoreToCassandra :: Members '[Embed IO, Input ClientState] r => @@ -394,3 +385,5 @@ interpretMemberStoreToCassandra = interpret $ \case embedClient $ removeLocalMembersFromRemoteConv rcnv uids AddMLSClients lcnv quid cs -> embedClient $ addMLSClients lcnv quid cs + RemoveMLSClients lcnv quid cs -> embedClient $ removeMLSClients lcnv quid cs + LookupMLSClients lcnv -> embedClient $ lookupMLSClients lcnv diff --git a/services/galley/src/Galley/Cassandra/Instances.hs b/services/galley/src/Galley/Cassandra/Instances.hs index f25ffc2a27..4860fdd3e1 100644 --- a/services/galley/src/Galley/Cassandra/Instances.hs +++ b/services/galley/src/Galley/Cassandra/Instances.hs @@ -39,6 +39,7 @@ import Wire.API.Conversation import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite import Wire.API.MLS.Proposal +import Wire.API.MLS.PublicGroupState import Wire.API.MLS.Serialisation import Wire.API.Team import qualified Wire.API.Team.Feature as Public @@ -118,9 +119,8 @@ instance Cql ConvTeamInfo where toCql t = CqlUdt [("teamid", toCql (cnvTeamId t)), ("managed", toCql False)] - fromCql (CqlUdt u) = do - t <- note "missing 'teamid' in teaminfo" ("teamid" `lookup` u) >>= fromCql - pure (ConvTeamInfo t) + fromCql (CqlUdt u) = + note "missing 'teamid' in teaminfo" ("teamid" `lookup` u) >>= fmap ConvTeamInfo . fromCql fromCql _ = Left "teaminfo: udt expected" instance Cql TeamBinding where @@ -199,6 +199,13 @@ instance Cql GroupId where fromCql (CqlBlob b) = Right . GroupId . LBS.toStrict $ b fromCql _ = Left "group_id: blob expected" +instance Cql OpaquePublicGroupState where + ctype = Tagged BlobColumn + + toCql = CqlBlob . LBS.fromStrict . unOpaquePublicGroupState + fromCql (CqlBlob b) = Right $ OpaquePublicGroupState (LBS.toStrict b) + fromCql _ = Left "OpaquePublicGroupState: blob expected" + instance Cql Icon where ctype = Tagged TextColumn toCql = CqlText . T.decodeUtf8 . toByteString' diff --git a/services/galley/src/Galley/Cassandra/Proposal.hs b/services/galley/src/Galley/Cassandra/Proposal.hs index e033d97372..eb5e5d9dd2 100644 --- a/services/galley/src/Galley/Cassandra/Proposal.hs +++ b/services/galley/src/Galley/Cassandra/Proposal.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index 5d77b62df4..9e50d5808e 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -34,6 +34,8 @@ import Wire.API.Conversation.Code import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.MLS.CipherSuite +import Wire.API.MLS.KeyPackage +import Wire.API.MLS.PublicGroupState import Wire.API.Provider import Wire.API.Provider.Service import Wire.API.Team @@ -231,6 +233,12 @@ deleteConv = "delete from conversation using timestamp 32503680000000000 where c markConvDeleted :: PrepQuery W (Identity ConvId) () markConvDeleted = "update conversation set deleted = true where conv = ?" +selectPublicGroupState :: PrepQuery R (Identity ConvId) (Identity (Maybe OpaquePublicGroupState)) +selectPublicGroupState = "select public_group_state from conversation where conv = ?" + +updatePublicGroupState :: PrepQuery W (OpaquePublicGroupState, ConvId) () +updatePublicGroupState = "update conversation set public_group_state = ? where conv = ?" + -- Conversations accessible by code ----------------------------------------- insertCode :: PrepQuery W (Key, Value, ConvId, Scope, Int32) () @@ -271,14 +279,14 @@ lookupGroupId = "SELECT conv_id, domain from group_id_conv_id where group_id = ? type MemberStatus = Int32 -selectMember :: PrepQuery R (ConvId, UserId) (UserId, Maybe ServiceId, Maybe ProviderId, Maybe MemberStatus, Maybe MutedStatus, Maybe Text, Maybe Bool, Maybe Text, Maybe Bool, Maybe Text, Maybe RoleName, Maybe (C.Set ClientId)) -selectMember = "select user, service, provider, status, otr_muted_status, otr_muted_ref, otr_archived, otr_archived_ref, hidden, hidden_ref, conversation_role, mls_clients from member where conv = ? and user = ?" +selectMember :: PrepQuery R (ConvId, UserId) (UserId, Maybe ServiceId, Maybe ProviderId, Maybe MemberStatus, Maybe MutedStatus, Maybe Text, Maybe Bool, Maybe Text, Maybe Bool, Maybe Text, Maybe RoleName) +selectMember = "select user, service, provider, status, otr_muted_status, otr_muted_ref, otr_archived, otr_archived_ref, hidden, hidden_ref, conversation_role from member where conv = ? and user = ?" -selectMembers :: PrepQuery R (Identity ConvId) (UserId, Maybe ServiceId, Maybe ProviderId, Maybe MemberStatus, Maybe MutedStatus, Maybe Text, Maybe Bool, Maybe Text, Maybe Bool, Maybe Text, Maybe RoleName, Maybe (C.Set ClientId)) -selectMembers = "select user, service, provider, status, otr_muted_status, otr_muted_ref, otr_archived, otr_archived_ref, hidden, hidden_ref, conversation_role, mls_clients from member where conv = ?" +selectMembers :: PrepQuery R (Identity ConvId) (UserId, Maybe ServiceId, Maybe ProviderId, Maybe MemberStatus, Maybe MutedStatus, Maybe Text, Maybe Bool, Maybe Text, Maybe Bool, Maybe Text, Maybe RoleName) +selectMembers = "select user, service, provider, status, otr_muted_status, otr_muted_ref, otr_archived, otr_archived_ref, hidden, hidden_ref, conversation_role from member where conv = ?" -insertMember :: PrepQuery W (ConvId, UserId, Maybe ServiceId, Maybe ProviderId, RoleName, Maybe (C.Set ClientId)) () -insertMember = "insert into member (conv, user, service, provider, status, conversation_role, mls_clients) values (?, ?, ?, ?, 0, ?, ?)" +insertMember :: PrepQuery W (ConvId, UserId, Maybe ServiceId, Maybe ProviderId, RoleName) () +insertMember = "insert into member (conv, user, service, provider, status, conversation_role) values (?, ?, ?, ?, 0, ?)" removeMember :: PrepQuery W (ConvId, UserId) () removeMember = "delete from member where conv = ? and user = ?" @@ -307,11 +315,11 @@ insertRemoteMember = "insert into member_remote_user (conv, user_remote_domain, removeRemoteMember :: PrepQuery W (ConvId, Domain, UserId) () removeRemoteMember = "delete from member_remote_user where conv = ? and user_remote_domain = ? and user_remote_id = ?" -selectRemoteMember :: PrepQuery R (ConvId, Domain, UserId) (RoleName, C.Set ClientId) -selectRemoteMember = "select conversation_role, mls_clients from member_remote_user where conv = ? and user_remote_domain = ? and user_remote_id = ?" +selectRemoteMember :: PrepQuery R (ConvId, Domain, UserId) (Identity RoleName) +selectRemoteMember = "select conversation_role from member_remote_user where conv = ? and user_remote_domain = ? and user_remote_id = ?" -selectRemoteMembers :: PrepQuery R (Identity ConvId) (Domain, UserId, RoleName, C.Set ClientId) -selectRemoteMembers = "select user_remote_domain, user_remote_id, conversation_role, mls_clients from member_remote_user where conv = ?" +selectRemoteMembers :: PrepQuery R (Identity ConvId) (Domain, UserId, RoleName) +selectRemoteMembers = "select user_remote_domain, user_remote_id, conversation_role from member_remote_user where conv = ?" updateRemoteMemberConvRoleName :: PrepQuery W (RoleName, ConvId, Domain, UserId) () updateRemoteMemberConvRoleName = "update member_remote_user set conversation_role = ? where conv = ? and user_remote_domain = ? and user_remote_id = ?" @@ -367,11 +375,14 @@ rmMemberClient c = -- MLS Clients -------------------------------------------------------------- -addLocalMLSClients :: PrepQuery W (C.Set ClientId, ConvId, UserId) () -addLocalMLSClients = "update member set mls_clients = mls_clients + ? where conv = ? and user = ?" +addMLSClient :: PrepQuery W (ConvId, Domain, UserId, ClientId, KeyPackageRef) () +addMLSClient = "insert into member_client (conv, user_domain, user, client, key_package_ref) values (?, ?, ?, ?, ?)" + +removeMLSClient :: PrepQuery W (ConvId, Domain, UserId, ClientId) () +removeMLSClient = "delete from member_client where conv = ? and user_domain = ? and user = ? and client = ?" -addRemoteMLSClients :: PrepQuery W (C.Set ClientId, ConvId, Domain, UserId) () -addRemoteMLSClients = "update member_remote_user set mls_clients = mls_clients + ? where conv = ? and user_remote_domain = ? and user_remote_id = ?" +lookupMLSClients :: PrepQuery R (Identity ConvId) (Domain, UserId, ClientId, KeyPackageRef) +lookupMLSClients = "select user_domain, user, client, key_package_ref from member_client where conv = ?" acquireCommitLock :: PrepQuery W (GroupId, Epoch, Int32) Row acquireCommitLock = "insert into mls_commit_locks (group_id, epoch) values (?, ?) if not exists using ttl ?" diff --git a/services/galley/src/Galley/Cassandra/Team.hs b/services/galley/src/Galley/Cassandra/Team.hs index 3156f2fc61..7494b24b6d 100644 --- a/services/galley/src/Galley/Cassandra/Team.hs +++ b/services/galley/src/Galley/Cassandra/Team.hs @@ -415,7 +415,11 @@ newTeamMember' lh tid (uid, perms, minvu, minvt, fromMaybe defUserLegalHoldStatu mk Nothing Nothing = pure $ mkTeamMember uid perms Nothing lhStatus mk _ _ = throwM $ ErrorCall "TeamMember with incomplete metadata." -teamConversationsForPagination :: TeamId -> Maybe ConvId -> Range 1 HardTruncationLimit Int32 -> Client (Page TeamConversation) +teamConversationsForPagination :: + TeamId -> + Maybe ConvId -> + Range 1 HardTruncationLimit Int32 -> + Client (Page TeamConversation) teamConversationsForPagination tid start (fromRange -> max) = fmap (newTeamConversation . runIdentity) <$> case start of Just c -> paginate Cql.selectTeamConvsFrom (paramsP LocalQuorum (tid, c) max) diff --git a/services/galley/src/Galley/Cassandra/TeamFeatures.hs b/services/galley/src/Galley/Cassandra/TeamFeatures.hs index eacdde5653..169280c51d 100644 --- a/services/galley/src/Galley/Cassandra/TeamFeatures.hs +++ b/services/galley/src/Galley/Cassandra/TeamFeatures.hs @@ -313,3 +313,7 @@ instance FeatureStatusCassandra MLSConfig where insert = "insert into team_features (team_id, mls_status, mls_default_protocol, \ \mls_protocol_toggle_users, mls_allowed_ciphersuites, mls_default_ciphersuite) values (?, ?, ?, ?, ?, ?)" + +instance FeatureStatusCassandra ExposeInvitationURLsToTeamAdminConfig where + getFeatureConfig _ = getTrivialConfigC "expose_invitation_urls_to_team_admin" + setFeatureConfig _ tid statusNoLock = setFeatureStatusC "expose_invitation_urls_to_team_admin" tid (wssStatus statusNoLock) diff --git a/services/galley/src/Galley/Effects/BrigAccess.hs b/services/galley/src/Galley/Effects/BrigAccess.hs index 31390adcd9..eb0c3e754d 100644 --- a/services/galley/src/Galley/Effects/BrigAccess.hs +++ b/services/galley/src/Galley/Effects/BrigAccess.hs @@ -51,6 +51,7 @@ module Galley.Effects.BrigAccess getClientByKeyPackageRef, getLocalMLSClients, addKeyPackageRef, + validateAndAddKeyPackageRef, updateKeyPackageRef, -- * Features @@ -73,6 +74,7 @@ import Wire.API.Connection import Wire.API.Error.Galley import Wire.API.MLS.Credential import Wire.API.MLS.KeyPackage +import Wire.API.Routes.Internal.Brig import Wire.API.Routes.Internal.Brig.Connection import qualified Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti as Multi import Wire.API.Team.Feature @@ -129,6 +131,7 @@ data BrigAccess m a where GetClientByKeyPackageRef :: KeyPackageRef -> BrigAccess m (Maybe ClientIdentity) GetLocalMLSClients :: Local UserId -> SignatureSchemeTag -> BrigAccess m (Set ClientInfo) AddKeyPackageRef :: KeyPackageRef -> Qualified UserId -> ClientId -> Qualified ConvId -> BrigAccess m () + ValidateAndAddKeyPackageRef :: NewKeyPackage -> BrigAccess m (Maybe NewKeyPackageResult) UpdateKeyPackageRef :: KeyPackageUpdate -> BrigAccess m () UpdateSearchVisibilityInbound :: Multi.TeamStatus SearchVisibilityInboundConfig -> diff --git a/services/galley/src/Galley/Effects/ConversationStore.hs b/services/galley/src/Galley/Effects/ConversationStore.hs index 442d2cfe8c..8a4b699a72 100644 --- a/services/galley/src/Galley/Effects/ConversationStore.hs +++ b/services/galley/src/Galley/Effects/ConversationStore.hs @@ -30,6 +30,7 @@ module Galley.Effects.ConversationStore getConversationIdByGroupId, getConversations, getConversationMetadata, + getPublicGroupState, isConversationAlive, getRemoteConversationStatus, selectConversations, @@ -43,6 +44,7 @@ module Galley.Effects.ConversationStore setConversationEpoch, acceptConnectConversation, setGroupId, + setPublicGroupState, -- * Delete conversation deleteConversation, @@ -65,6 +67,7 @@ import Imports import Polysemy import Wire.API.Conversation hiding (Conversation, Member) import Wire.API.MLS.Epoch +import Wire.API.MLS.PublicGroupState data ConversationStore m a where CreateConversationId :: ConversationStore m ConvId @@ -74,6 +77,9 @@ data ConversationStore m a where GetConversationIdByGroupId :: GroupId -> ConversationStore m (Maybe (Qualified ConvId)) GetConversations :: [ConvId] -> ConversationStore m [Conversation] GetConversationMetadata :: ConvId -> ConversationStore m (Maybe ConversationMetadata) + GetPublicGroupState :: + ConvId -> + ConversationStore m (Maybe OpaquePublicGroupState) IsConversationAlive :: ConvId -> ConversationStore m Bool GetRemoteConversationStatus :: UserId -> @@ -87,6 +93,10 @@ data ConversationStore m a where SetConversationMessageTimer :: ConvId -> Maybe Milliseconds -> ConversationStore m () SetConversationEpoch :: ConvId -> Epoch -> ConversationStore m () SetGroupId :: GroupId -> Qualified ConvId -> ConversationStore m () + SetPublicGroupState :: + ConvId -> + OpaquePublicGroupState -> + ConversationStore m () AcquireCommitLock :: GroupId -> Epoch -> NominalDiffTime -> ConversationStore m LockAcquired ReleaseCommitLock :: GroupId -> Epoch -> ConversationStore m () diff --git a/services/galley/src/Galley/Effects/MemberStore.hs b/services/galley/src/Galley/Effects/MemberStore.hs index 11dbe2f836..1bd42b5533 100644 --- a/services/galley/src/Galley/Effects/MemberStore.hs +++ b/services/galley/src/Galley/Effects/MemberStore.hs @@ -39,6 +39,8 @@ module Galley.Effects.MemberStore setSelfMember, setOtherMember, addMLSClients, + removeMLSClients, + lookupMLSClients, -- * Delete members deleteMembers, @@ -55,6 +57,7 @@ import Galley.Types.UserList import Imports import Polysemy import Wire.API.Conversation.Member hiding (Member) +import Wire.API.MLS.KeyPackage import Wire.API.Provider.Service data MemberStore m a where @@ -71,7 +74,11 @@ data MemberStore m a where SetOtherMember :: Local ConvId -> Qualified UserId -> OtherMemberUpdate -> MemberStore m () DeleteMembers :: ConvId -> UserList UserId -> MemberStore m () DeleteMembersInRemoteConversation :: Remote ConvId -> [UserId] -> MemberStore m () - AddMLSClients :: Local ConvId -> Qualified UserId -> Set ClientId -> MemberStore m () + AddMLSClients :: Local ConvId -> Qualified UserId -> Set (ClientId, KeyPackageRef) -> MemberStore m () + RemoveMLSClients :: Local ConvId -> Qualified UserId -> Set ClientId -> MemberStore m () + LookupMLSClients :: + Local ConvId -> + MemberStore m (Map (Qualified UserId) (Set (ClientId, KeyPackageRef))) makeSem ''MemberStore diff --git a/services/galley/src/Galley/Intra/Client.hs b/services/galley/src/Galley/Intra/Client.hs index 43ff1ab5b1..fdd4514de9 100644 --- a/services/galley/src/Galley/Intra/Client.hs +++ b/services/galley/src/Galley/Intra/Client.hs @@ -26,6 +26,7 @@ module Galley.Intra.Client getLocalMLSClients, addKeyPackageRef, updateKeyPackageRef, + validateAndAddKeyPackageRef, ) where @@ -34,6 +35,7 @@ import Bilge.RPC import Brig.Types.Intra import Brig.Types.Team.LegalHold (LegalHoldClientRequest (..)) import Brig.Types.User.Auth (LegalHoldLogin (..)) +import Control.Monad.Catch import Data.ByteString.Conversion (toByteString') import Data.Id import Data.Misc @@ -47,6 +49,8 @@ import Galley.External.LegalHoldService.Types import Galley.Intra.Util import Galley.Monad import Imports +import qualified Network.HTTP.Client as Rq +import qualified Network.HTTP.Types as HTTP import Network.HTTP.Types.Method import Network.HTTP.Types.Status import Network.Wai.Utilities.Error hiding (Error) @@ -222,3 +226,18 @@ updateKeyPackageRef keyPackageRef = . json (kpupNext keyPackageRef) . expect2xx ) + +validateAndAddKeyPackageRef :: NewKeyPackage -> App (Maybe NewKeyPackageResult) +validateAndAddKeyPackageRef nkp = do + res <- + call + Brig + ( method PUT + . paths ["i", "mls", "key-package-add"] + . json nkp + ) + let statusCode = HTTP.statusCode (Rq.responseStatus res) + if + | statusCode `div` 100 == 2 -> Just <$> parseResponse (mkError status502 "server-error") res + | statusCode `div` 100 == 4 -> pure Nothing + | otherwise -> throwM (mkError status502 "server-error" "Unexpected http status returned from /i/mls/key-packages/add") diff --git a/services/galley/src/Galley/Intra/Effects.hs b/services/galley/src/Galley/Intra/Effects.hs index 46e017d813..c42b7f1d63 100644 --- a/services/galley/src/Galley/Intra/Effects.hs +++ b/services/galley/src/Galley/Intra/Effects.hs @@ -82,6 +82,9 @@ interpretBrigAccess = interpret $ \case AddKeyPackageRef ref qusr cl qcnv -> embedApp $ addKeyPackageRef ref qusr cl qcnv + ValidateAndAddKeyPackageRef nkp -> + embedApp $ + validateAndAddKeyPackageRef nkp UpdateKeyPackageRef update -> embedApp $ updateKeyPackageRef update diff --git a/services/galley/src/Galley/Intra/User.hs b/services/galley/src/Galley/Intra/User.hs index 0a5ed99992..90b24af2e5 100644 --- a/services/galley/src/Galley/Intra/User.hs +++ b/services/galley/src/Galley/Intra/User.hs @@ -205,7 +205,7 @@ getUsers = chunkify $ \uids -> do . expect2xx pure . fromMaybe [] . responseJsonMaybe $ resp --- | Calls 'Brig.API.deleteUserNoVerifyH'. +-- | Calls 'Brig.API.deleteUserNoAuthH'. deleteUser :: UserId -> App () deleteUser uid = do void $ diff --git a/services/galley/src/Galley/Options.hs b/services/galley/src/Galley/Options.hs index 2d82241913..edb3850d29 100644 --- a/services/galley/src/Galley/Options.hs +++ b/services/galley/src/Galley/Options.hs @@ -22,6 +22,7 @@ module Galley.Options setHttpPoolSize, setMaxTeamSize, setMaxFanoutSize, + setExposeInvitationURLsTeamAllowlist, setMaxConvSize, setIntraListing, setConversationCodeURI, @@ -56,6 +57,7 @@ where import Control.Lens hiding (Level, (.=)) import Data.Aeson.TH (deriveFromJSON) import Data.Domain (Domain) +import Data.Id (TeamId) import Data.Misc import Data.Range import Galley.Keys @@ -76,6 +78,10 @@ data Settings = Settings -- This defaults to setMaxTeamSize and cannot be > HardTruncationLimit. Useful -- to tune mainly for testing purposes. _setMaxFanoutSize :: !(Maybe (Range 1 HardTruncationLimit Int32)), + -- | List of teams for which the invitation URL can be added to the list of all + -- invitations retrievable by team admins. See also: + -- 'ExposeInvitationURLsToTeamAdminConfig'. + _setExposeInvitationURLsTeamAllowlist :: !(Maybe [TeamId]), -- | Max number of members in a conversation. NOTE: This must be in sync with Brig _setMaxConvSize :: !Word16, -- | Whether to call Brig for device listing diff --git a/services/galley/src/Galley/Run.hs b/services/galley/src/Galley/Run.hs index a82a7f3a72..81dfa216da 100644 --- a/services/galley/src/Galley/Run.hs +++ b/services/galley/src/Galley/Run.hs @@ -81,7 +81,7 @@ run opts = lowerCodensity $ do void $ Codensity $ Async.withAsync $ collectAuthMetrics (env ^. monitor) (aws ^. awsEnv) void $ Codensity $ Async.withAsync $ runApp env deleteLoop void $ Codensity $ Async.withAsync $ runApp env refreshMetrics - lift $ finally (runSettingsWithShutdown settings app 5) (shutdown (env ^. cstate)) + lift $ finally (runSettingsWithShutdown settings app Nothing) (shutdown (env ^. cstate)) mkApp :: Opts -> Codensity IO (Application, Env) mkApp opts = diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index b6fc4c9aa1..f8d9b3092f 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -247,7 +247,7 @@ tests s = status :: TestM () status = do - g <- view tsGalley + g <- viewGalley get (g . path "/i/status") !!! const 200 === statusCode Bilge.head (g . path "/i/status") @@ -255,7 +255,7 @@ status = do metrics :: TestM () metrics = do - g <- view tsGalley + g <- viewGalley get (g . path "/i/metrics") !!! do const 200 === statusCode -- Should contain the request duration metric in its output @@ -388,6 +388,9 @@ postCryptoMessageVerifyMsgSentAndRejectIfMissingClient = do -- Deleted eve WS.bracketR2 c bob eve $ \(wsB, wsE) -> do deleteClient eve ec (Just defPassword) !!! const 200 === statusCode + liftIO $ + WS.assertMatch_ (5 # WS.Second) wsE $ + wsAssertClientRemoved ec let m4 = [(bob, bc, toBase64Text "ciphertext4"), (eve, ec, toBase64Text "ciphertext4")] postOtrMessage id alice ac conv m4 !!! do const 201 === statusCode @@ -438,7 +441,6 @@ postCryptoMessageVerifyMsgSentAndRejectIfMissingClient = do -- This test verifies basic mismatch behavior of the the JSON endpoint. postCryptoMessageVerifyRejectMissingClientAndRepondMissingPrekeysJson :: TestM () postCryptoMessageVerifyRejectMissingClientAndRepondMissingPrekeysJson = do - b <- view tsBrig (alice, ac) <- randomUserWithClient (head someLastPrekeys) (bob, bc) <- randomUserWithClient (someLastPrekeys !! 1) (eve, ec) <- randomUserWithClient (someLastPrekeys !! 2) @@ -452,8 +454,9 @@ postCryptoMessageVerifyRejectMissingClientAndRepondMissingPrekeysJson = do assertMismatchWithMessage (Just "client mismatch") [(eve, Set.singleton ec)] [] [] let x = responseJsonUnsafeWithMsg "ClientMismatch" r1 -- Fetch all missing clients prekeys + b <- view tsUnversionedBrig r2 <- - post (b . zUser alice . path "/users/prekeys" . json (missingClients x)) + post (b . zUser alice . path "v1/users/prekeys" . json (missingClients x)) postTeamConv teamId owner [] (Just "testConversation") [CodeAccess] (Just noGuestsAccess) Nothing @@ -1253,7 +1256,7 @@ testPostCodeRejectedIfGuestLinksDisabled = do -- Check if guests cannot join anymore if guest invite feature was disabled on team level testJoinTeamConvGuestLinksDisabled :: TestM () testJoinTeamConvGuestLinksDisabled = do - galley <- view tsGalley + galley <- viewGalley let convName = "testConversation" (owner, teamId, [alice]) <- Util.createBindingTeamWithNMembers 1 eve <- ephemeralUser @@ -1312,7 +1315,7 @@ testJoinTeamConvGuestLinksDisabled = do testJoinNonTeamConvGuestLinksDisabled :: TestM () testJoinNonTeamConvGuestLinksDisabled = do - galley <- view tsGalley + galley <- viewGalley let convName = "testConversation" (owner, teamId, []) <- Util.createBindingTeamWithNMembers 0 userNotInTeam <- randomUser @@ -1449,7 +1452,7 @@ postConvertTeamConv = do dave <- view Teams.userId <$> addUserToTeam alice tid assertQueue "team member (dave) join" $ tUpdate 3 [alice] refreshIndex - eve <- randomUser + (eve, qeve) <- randomUserTuple connectUsers alice (singleton eve) let acc = Just $ Set.fromList [InviteAccess, CodeAccess] -- creating a team-only conversation containing eve should fail @@ -1478,9 +1481,11 @@ postConvertTeamConv = do WS.assertMatchN (5 # Second) [wsA, wsB, wsE, wsM] $ wsAssertConvAccessUpdate qconv qalice teamAccess -- non-team members get kicked out - void . liftIO $ - WS.assertMatchN (5 # Second) [wsA, wsB, wsE, wsM] $ - wsAssertMemberLeave qconv qalice $ (`Qualified` localDomain) <$> [eve, mallory] + liftIO $ do + WS.assertMatchN_ (5 # Second) [wsA, wsB, wsE, wsM] $ + wsAssertMemberLeave qconv qalice (pure qeve) + WS.assertMatchN_ (5 # Second) [wsA, wsB, wsE, wsM] $ + wsAssertMemberLeave qconv qalice (pure qmallory) -- joining (for mallory) is no longer possible postJoinCodeConv mallory j !!! const 403 === statusCode -- team members (dave) can still join @@ -1532,16 +1537,42 @@ testAccessUpdateGuestRemoved = do -- note that removing users happens asynchronously, so this check should -- happen while the mock federator is still available WS.assertMatchN_ (5 # Second) [wsA, wsB, wsC] $ - wsAssertMembersLeave (cnvQualifiedId conv) alice [charlie, dee] + wsAssertMembersLeave (cnvQualifiedId conv) alice [charlie] + WS.assertMatchN_ (5 # Second) [wsA, wsB, wsC] $ + wsAssertMembersLeave (cnvQualifiedId conv) alice [dee] -- dee's remote receives a notification - liftIO . assertBool "remote users are not notified" . isJust . flip find reqs $ \freq -> - (frComponent freq == Galley) - && ( frRPC freq == "on-conversation-updated" - ) - && ( fmap F.cuAction (eitherDecode (frBody freq)) - == Right (SomeConversationAction (sing @'ConversationLeaveTag) (charlie :| [dee])) - ) + let compareLists [] ys = [] @?= ys + compareLists (x : xs) ys = case break (== x) ys of + (ys1, _ : ys2) -> compareLists xs (ys1 <> ys2) + _ -> assertFailure $ "Could not find " <> show x <> " in " <> show ys + liftIO $ + compareLists + ( map + ( \fr -> do + cu <- eitherDecode (frBody fr) + pure (F.cuOrigUserId cu, F.cuAction cu) + ) + ( filter + ( \fr -> + frComponent fr == Galley + && frRPC fr == "on-conversation-updated" + ) + reqs + ) + ) + [ Right (alice, SomeConversationAction (sing @'ConversationRemoveMembersTag) (pure charlie)), + Right (alice, SomeConversationAction (sing @'ConversationRemoveMembersTag) (pure dee)), + Right + ( alice, + SomeConversationAction + (sing @'ConversationAccessDataTag) + ConversationAccessData + { cupAccess = mempty, + cupAccessRoles = Set.fromList [TeamMemberAccessRole] + } + ) + ] -- only alice and bob remain conv2 <- @@ -1575,7 +1606,7 @@ testTeamMemberCantJoinViaGuestLinkIfAccessRoleRemoved = do getGuestLinksStatusFromForeignTeamConv :: TestM () getGuestLinksStatusFromForeignTeamConv = do localDomain <- viewFederationDomain - galley <- view tsGalley + galley <- viewGalley let setTeamStatus u tid tfStatus = TeamFeatures.putTeamFeatureFlagWithGalley @Public.GuestLinksConfig galley u tid (Public.WithStatusNoLock tfStatus Public.GuestLinksConfig Public.FeatureTTLUnlimited) !!! do const 200 === statusCode @@ -2033,7 +2064,7 @@ postConvQualifiedFederationNotEnabled = do connectWithRemoteUser alice bob let federatorNotConfigured = optFederator .~ Nothing withSettingsOverrides federatorNotConfigured $ do - g <- view tsGalley + g <- viewGalley postConvHelper g alice [bob] !!! do const 400 === statusCode const (Just "federation-not-enabled") === fmap label . responseJsonUnsafe @@ -2069,7 +2100,7 @@ postO2OConvOk = do postConvO2OFailWithSelf :: TestM () postConvO2OFailWithSelf = do - g <- view tsGalley + g <- viewGalley alice <- randomUser let inv = NewConv [alice] [] Nothing mempty Nothing Nothing Nothing Nothing roleNameWireAdmin ProtocolProteusTag Nothing post (g . path "/conversations/one2one" . zUser alice . zConn "conn" . zType "access" . json inv) !!! do @@ -2190,7 +2221,7 @@ postRepeatConnectConvCancel = do privateAccess @=? cnvAccess cnv4 where cancel u c = do - g <- view tsGalley + g <- viewGalley let cnvId = qUnqualified . cnvQualifiedId put (g . paths ["/i/conversations", toByteString' (cnvId c), "block"] . zUser u) !!! const 200 === statusCode @@ -2198,7 +2229,7 @@ postRepeatConnectConvCancel = do putBlockConvOk :: TestM () putBlockConvOk = do - g <- view tsGalley + g <- viewGalley alice <- randomUser bob <- randomUser conv <- responseJsonUnsafeWithMsg "conversation" <$> postConnectConv alice bob "Alice" "connect with me!" (Just "me@me.com") @@ -2258,7 +2289,7 @@ getConvQualifiedOk = do accessConvMeta :: TestM () accessConvMeta = do - g <- view tsGalley + g <- viewGalley alice <- randomUser bob <- randomUser chuck <- randomUser @@ -2409,7 +2440,7 @@ testGetQualifiedRemoteConv = do remoteConvId = Qualified convId remoteDomain bobAsOtherMember = OtherMember bobQ Nothing roleNameWireAdmin aliceAsLocal = - LocalMember aliceId defMemberStatus Nothing roleNameWireAdmin Set.empty + LocalMember aliceId defMemberStatus Nothing roleNameWireAdmin aliceAsOtherMember = localMemberToOther (qDomain aliceQ) aliceAsLocal aliceAsSelfMember = localMemberToSelf loc aliceAsLocal @@ -3083,7 +3114,7 @@ putQualifiedConvRenameWithRemotesOk = do putConvDeprecatedRenameOk :: TestM () putConvDeprecatedRenameOk = do c <- view tsCannon - g <- view tsGalley + g <- viewGalley alice <- randomUser qbob <- randomQualifiedUser let bob = qUnqualified qbob @@ -3372,7 +3403,6 @@ putRemoteConvMemberOk update = do defMemberStatus Nothing roleNameWireAdmin - Set.empty let mockConversation = mkProteusConv (qUnqualified qconv) @@ -3570,7 +3600,7 @@ putReceiptModeWithRemotesOk = do postTypingIndicators :: TestM () postTypingIndicators = do - g <- view tsGalley + g <- viewGalley alice <- randomUser bob <- randomUser connectUsers alice (singleton bob) @@ -3740,25 +3770,29 @@ removeUser = do bConvUpdates <- mapM (assertRight . eitherDecode . frBody) bConvUpdateRPCs bConvUpdatesA2 <- assertOne $ filter (\cu -> cuConvId cu == convA2) bConvUpdates - cuAction bConvUpdatesA2 @?= SomeConversationAction (sing @'ConversationLeaveTag) (pure alexDel) + cuOrigUserId bConvUpdatesA2 @?= alexDel + cuAction bConvUpdatesA2 @?= SomeConversationAction (sing @'ConversationLeaveTag) () cuAlreadyPresentUsers bConvUpdatesA2 @?= [qUnqualified berta] bConvUpdatesA4 <- assertOne $ filter (\cu -> cuConvId cu == convA4) bConvUpdates - cuAction bConvUpdatesA4 @?= SomeConversationAction (sing @'ConversationLeaveTag) (pure alexDel) + cuOrigUserId bConvUpdatesA4 @?= alexDel + cuAction bConvUpdatesA4 @?= SomeConversationAction (sing @'ConversationLeaveTag) () cuAlreadyPresentUsers bConvUpdatesA4 @?= [qUnqualified bart] liftIO $ do cConvUpdateRPC <- assertOne $ filter (matchFedRequest cDomain "on-conversation-updated") fedRequests Right convUpdate <- pure . eitherDecode . frBody $ cConvUpdateRPC cuConvId convUpdate @?= convA4 - cuAction convUpdate @?= SomeConversationAction (sing @'ConversationLeaveTag) (pure alexDel) + cuOrigUserId convUpdate @?= alexDel + cuAction convUpdate @?= SomeConversationAction (sing @'ConversationLeaveTag) () cuAlreadyPresentUsers convUpdate @?= [qUnqualified carl] liftIO $ do dConvUpdateRPC <- assertOne $ filter (matchFedRequest dDomain "on-conversation-updated") fedRequests Right convUpdate <- pure . eitherDecode . frBody $ dConvUpdateRPC cuConvId convUpdate @?= convA2 - cuAction convUpdate @?= SomeConversationAction (sing @'ConversationLeaveTag) (pure alexDel) + cuOrigUserId convUpdate @?= alexDel + cuAction convUpdate @?= SomeConversationAction (sing @'ConversationLeaveTag) () cuAlreadyPresentUsers convUpdate @?= [qUnqualified dwight] -- Check memberships diff --git a/services/galley/test/integration/API/CustomBackend.hs b/services/galley/test/integration/API/CustomBackend.hs index 4f427c2f7d..3da26252be 100644 --- a/services/galley/test/integration/API/CustomBackend.hs +++ b/services/galley/test/integration/API/CustomBackend.hs @@ -20,9 +20,9 @@ module API.CustomBackend ) where +import API.Util import Bilge hiding (timeout) import Bilge.Assert -import Control.Lens (view) import Data.Aeson hiding (json) import Data.Aeson.QQ (aesonQQ) import Imports @@ -43,13 +43,13 @@ tests s = getByDomainNotFound :: TestM () getByDomainNotFound = do - galley <- view tsGalley + galley <- viewGalley get (galley . path "/custom-backend/by-domain/domain.no1") !!! do const 404 === statusCode getByDomainInvalidDomain :: TestM () getByDomainInvalidDomain = do - galley <- view tsGalley + galley <- viewGalley -- contains invalid character '+' -- this used to respond with '400 bad request' -- but after servantification it returns '404 not found' @@ -59,7 +59,7 @@ getByDomainInvalidDomain = do getByDomainFound :: TestM () getByDomainFound = do - galley <- view tsGalley + galley <- viewGalley let jsonBody :: Value jsonBody = [aesonQQ|{ @@ -74,7 +74,7 @@ getByDomainFound = do getByDomainDeleted :: TestM () getByDomainDeleted = do - galley <- view tsGalley + galley <- viewGalley let jsonBody :: Value jsonBody = [aesonQQ|{ @@ -90,7 +90,7 @@ getByDomainDeleted = do getByDomainIsCaseInsensitive :: TestM () getByDomainIsCaseInsensitive = do - galley <- view tsGalley + galley <- viewGalley let jsonBody :: Value jsonBody = [aesonQQ|{ diff --git a/services/galley/test/integration/API/Federation.hs b/services/galley/test/integration/API/Federation.hs index 9d9eda39be..436a283970 100644 --- a/services/galley/test/integration/API/Federation.hs +++ b/services/galley/test/integration/API/Federation.hs @@ -19,7 +19,6 @@ module API.Federation where -import API.MLS.Util import API.Util import Bilge hiding (head) import Bilge.Assert @@ -27,7 +26,6 @@ import Control.Lens hiding ((#)) import Data.Aeson (ToJSON (..)) import qualified Data.Aeson as A import Data.ByteString.Conversion (toByteString') -import Data.Default import Data.Domain import Data.Id (ConvId, Id (..), UserId, newClientId, randomId) import Data.Json.Util hiding ((#)) @@ -90,9 +88,7 @@ tests s = test s "POST /federation/on-message-sent : Receive a message from another backend" onMessageSent, test s "POST /federation/send-message : Post a message sent from another backend" sendMessage, test s "POST /federation/on-user-deleted-conversations : Remove deleted remote user from local conversations" onUserDeleted, - test s "POST /federation/update-conversation : Update local conversation by a remote admin " updateConversationByRemoteAdmin, - test s "POST /federation/mls-welcome : Post an MLS welcome message received from another backend" sendMLSWelcome, - test s "POST /federation/mls-welcome : Post an MLS welcome message (key package ref not found)" sendMLSWelcomeKeyPackageNotFound + test s "POST /federation/update-conversation : Update local conversation by a remote admin " updateConversationByRemoteAdmin ] getConversationsAllFound :: TestM () @@ -335,11 +331,11 @@ removeLocalUser = do cuRemove = FedGalley.ConversationUpdate { FedGalley.cuTime = addUTCTime (secondsToNominalDiffTime 5) now, - FedGalley.cuOrigUserId = qBob, + FedGalley.cuOrigUserId = qAlice, FedGalley.cuConvId = conv, FedGalley.cuAlreadyPresentUsers = [alice], FedGalley.cuAction = - SomeConversationAction (sing @'ConversationLeaveTag) (pure qAlice) + SomeConversationAction (sing @'ConversationLeaveTag) () } connectWithRemoteUser alice qBob @@ -351,7 +347,7 @@ removeLocalUser = do void . WS.assertMatch (3 # Second) ws $ wsAssertMemberJoinWithRole qconv qBob [qAlice] roleNameWireMember void . WS.assertMatch (3 # Second) ws $ - wsAssertMembersLeave qconv qBob [qAlice] + wsAssertMembersLeave qconv qAlice [qAlice] afterRemoval <- listRemoteConvs remoteDomain alice liftIO $ do afterAddition @?= [qconv] @@ -403,7 +399,7 @@ removeRemoteUser = do FedGalley.cuConvId = conv, FedGalley.cuAlreadyPresentUsers = [alice, charlie, dee], FedGalley.cuAction = - SomeConversationAction (sing @'ConversationLeaveTag) (pure user) + SomeConversationAction (sing @'ConversationRemoveMembersTag) (pure user) } WS.bracketRN c [alice, charlie, dee, flo] $ \[wsA, wsC, wsD, wsF] -> do @@ -686,7 +682,7 @@ leaveConversationSuccess = do liftIO $ fedRequestsForDomain remoteDomain1 Galley federatedRequests @?= [] let [remote2GalleyFederatedRequest] = fedRequestsForDomain remoteDomain2 Galley federatedRequests - assertLeaveUpdate remote2GalleyFederatedRequest qconvId qChad [qUnqualified qEve] qChad + assertLeaveUpdate remote2GalleyFederatedRequest qconvId qChad [qUnqualified qEve] leaveConversationNonExistent :: TestM () leaveConversationNonExistent = do @@ -1035,7 +1031,7 @@ onUserDeleted = do FedGalley.cuOrigUserId cDomainRPCReq @?= qUntagged bob FedGalley.cuConvId cDomainRPCReq @?= qUnqualified groupConvId FedGalley.cuAlreadyPresentUsers cDomainRPCReq @?= [qUnqualified carl] - FedGalley.cuAction cDomainRPCReq @?= SomeConversationAction (sing @'ConversationLeaveTag) (pure $ qUntagged bob) + FedGalley.cuAction cDomainRPCReq @?= SomeConversationAction (sing @'ConversationLeaveTag) () -- | We test only ReceiptMode update here -- @@ -1134,55 +1130,6 @@ updateConversationByRemoteAdmin = do let convUpdate :: ConversationUpdate = fromRight (error $ "Could not parse ConversationUpdate from " <> show (frBody rpc)) $ A.eitherDecode (frBody rpc) pure (rpc, convUpdate) -sendMLSWelcome :: TestM () -sendMLSWelcome = do - let aliceDomain = Domain "a.far-away.example.com" - -- Alice is from the originating domain and Bob is local, i.e., on the receiving domain - MessagingSetup {..} <- aliceInvitesBob (1, LocalUser) def {creatorOrigin = RemoteUser aliceDomain} - let bob = head users - - fedGalleyClient <- view tsFedGalleyClient - cannon <- view tsCannon - - WS.bracketR cannon (qUnqualified (pUserId bob)) $ \wsB -> do - -- send welcome message - void $ - runFedClient @"mls-welcome" fedGalleyClient aliceDomain $ - MLSWelcomeRequest - (Base64ByteString welcome) - - -- check that the corresponding event is received - liftIO $ do - WS.assertMatch_ (5 # WS.Second) wsB $ - wsAssertMLSWelcome (pUserId bob) welcome - -sendMLSWelcomeKeyPackageNotFound :: TestM () -sendMLSWelcomeKeyPackageNotFound = do - let aliceDomain = Domain "a.far-away.example.com" - -- Alice is from the originating domain and Bob is local, i.e., on the receiving domain - MessagingSetup {..} <- - aliceInvitesBob - (1, LocalUser) - def - { creatorOrigin = RemoteUser aliceDomain, - createClients = DontCreateClients -- no key package upload will happen - } - let bob = head users - - fedGalleyClient <- view tsFedGalleyClient - cannon <- view tsCannon - - WS.bracketR cannon (qUnqualified (pUserId bob)) $ \wsB -> do - -- send welcome message - void $ - runFedClient @"mls-welcome" fedGalleyClient aliceDomain $ - MLSWelcomeRequest - (Base64ByteString welcome) - - liftIO $ do - -- check that no event is received - WS.assertNoEvent (1 # Second) [wsB] - getConvAction :: Sing tag -> SomeConversationAction -> Maybe (ConversationAction tag) getConvAction tquery (SomeConversationAction tag action) = case (tag, tquery) of diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 086a206f7d..352c3b356f 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -25,23 +25,17 @@ import API.Util import Bilge hiding (head) import Bilge.Assert import Cassandra -import Control.Arrow -import Control.Lens (view, (^..)) +import Control.Lens (view) +import qualified Control.Monad.State as State import Crypto.Error -import qualified Crypto.PubKey.Ed25519 as C +import qualified Crypto.PubKey.Ed25519 as Ed25519 import qualified Data.Aeson as Aeson -import Data.Aeson.Lens import Data.Binary.Put import qualified Data.ByteString as BS -import qualified Data.ByteString.Base64.URL as B64U -import Data.ByteString.Conversion import qualified Data.ByteString.Lazy as LBS -import Data.Default import Data.Domain import Data.Id import Data.Json.Util hiding ((#)) -import qualified Data.List.NonEmpty as NE -import qualified Data.List.NonEmpty as NonEmpty import Data.List1 hiding (head) import qualified Data.Map as Map import Data.Qualified @@ -54,8 +48,7 @@ import Data.Time import Federator.MockServer hiding (withTempMockFederator) import Imports import qualified Network.Wai.Utilities.Error as Wai -import System.FilePath -import System.IO.Temp +import Test.QuickCheck (Arbitrary (arbitrary), generate) import Test.Tasty import Test.Tasty.Cannon (TimeoutUnit (Second), (#)) import qualified Test.Tasty.Cannon as WS @@ -67,18 +60,13 @@ import Wire.API.Conversation.Action import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Error.Galley -import Wire.API.Event.Conversation import Wire.API.Federation.API.Common import Wire.API.Federation.API.Galley -import Wire.API.MLS.CipherSuite import Wire.API.MLS.Credential -import Wire.API.MLS.Group (convToGroupId) -import Wire.API.MLS.KeyPackage import Wire.API.MLS.Keys -import Wire.API.MLS.Message import Wire.API.MLS.Serialisation +import Wire.API.MLS.Welcome import Wire.API.Message -import Wire.API.Routes.Version import Wire.API.User.Client tests :: IO TestSetup -> TestTree @@ -94,7 +82,9 @@ tests s = "Welcome" [ test s "local welcome" testLocalWelcome, test s "local welcome (client with no public key)" testWelcomeNoKey, - test s "remote welcome" testRemoteWelcome + test s "remote welcome" testRemoteWelcome, + test s "post a remote MLS welcome message" sendRemoteMLSWelcome, + test s "post a remote MLS welcome message (key package ref not found)" sendRemoteMLSWelcomeKPNotFound ], testGroup "Creation" @@ -104,22 +94,21 @@ tests s = testGroup "Commit" [ test s "add user to a conversation" testAddUser, + test s "add user with a commit bundle" testAddUserWithBundle, + test s "add user with an incomplete welcome" testAddUserWithBundleIncompleteWelcome, test s "add user (not connected)" testAddUserNotConnected, test s "add user (partial client list)" testAddUserPartial, test s "add client of existing user" testAddClientPartial, test s "add user with some non-MLS clients" testAddUserWithProteusClients, - test s "add new client of an already-present user to a conversation" testAddNewClient, test s "send a stale commit" testStaleCommit, test s "add remote user to a conversation" testAddRemoteUser, + test s "add remote user with a commit bundle" testAddRemoteUserWithBundle, test s "return error when commit is locked" testCommitLock, test s "add user to a conversation with proposal + commit" testAddUserBareProposalCommit, test s "post commit that references a unknown proposal" testUnknownProposalRefCommit, test s "post commit that is not referencing all proposals" testCommitNotReferencingAllProposals, test s "admin removes user from a conversation" testAdminRemovesUserFromConv, - test s "admin removes user from a conversation but doesn't list all clients" testRemoveClientsIncomplete, - test s "anyone removes a non-existing client from a group" (testRemoveDeletedClient True), - test s "anyone removes an existing client from group, but the user has other clients" (testRemoveDeletedClient False), - test s "admin removes only strict subset of clients from a user" testRemoveSubset + test s "admin removes user from a conversation but doesn't list all clients" testRemoveClientsIncomplete ], testGroup "Application Message" @@ -153,11 +142,22 @@ tests s = test s "forward an unsupported proposal" propUnsupported ], testGroup - "External Proposal" + "External Add Proposal" [ test s "member adds new client" testExternalAddProposal, + test s "non-admin commits external add proposal" testExternalAddProposalNonAdminCommit, test s "non-member adds new client" testExternalAddProposalWrongUser, test s "member adds unknown new client" testExternalAddProposalWrongClient ], + testGroup + "Backend-side External Remove Proposals" + [ test s "local conversation, local user deleted" testBackendRemoveProposalLocalConvLocalUser, + test s "local conversation, remote user deleted" testBackendRemoveProposalLocalConvRemoteUser, + test s "local conversation, creator leaving" testBackendRemoveProposalLocalConvLocalLeaverCreator, + test s "local conversation, local committer leaving" testBackendRemoveProposalLocalConvLocalLeaverCommitter, + test s "local conversation, remote user leaving" testBackendRemoveProposalLocalConvRemoteLeaver, + test s "local conversation, local client deleted" testBackendRemoveProposalLocalConvLocalClient, + test s "local conversation, remote client deleted" testBackendRemoveProposalLocalConvRemoteClient + ], testGroup "Protocol mismatch" [ test s "send a commit to a proteus conversation" testAddUsersToProteus, @@ -165,7 +165,13 @@ tests s = test s "remove users bypassing MLS" testRemoveUsersDirectly, test s "send proteus message to an MLS conversation" testProteusMessage ], - test s "public keys" testPublicKeys + test s "public keys" testPublicKeys, + testGroup + "GroupInfo" + [ test s "get group info for a local conversation" testGetGroupInfoOfLocalConv, + test s "get group info for a remote conversation" testGetGroupInfoOfRemoteConv, + test s "get group info for a remote user" testFederatedGetGroupInfo + ] ] postMLSConvFail :: TestM () @@ -201,500 +207,508 @@ postMLSConvOk = do testSenderNotInConversation :: TestM () testSenderNotInConversation = do - withSystemTempDirectory "mls" $ \tmp -> do - (alice, [bob]) <- withLastPrekeys $ setupParticipants tmp def [(1, LocalUser)] - _ <- setupGroup tmp CreateConv alice "group" + -- create users + [alice, bob] <- createAndConnectUsers (replicate 2 Nothing) - (_commit, _welcome) <- - liftIO $ - setupCommit tmp alice "group" "group" $ - toList (pClients bob) - liftIO $ mergeWelcome tmp (pClientQid bob) "group" "group" "welcome" - message <- liftIO $ createMessage tmp bob "group" "some text" + runMLSTest $ do + [alice1, bob1] <- traverse createMLSClient [alice, bob] + + -- upload key packages + void $ uploadNewKeyPackage bob1 + + -- create group with alice1 and bob1, but do not commit adding Bob + void $ setupMLSGroup alice1 + mp <- createAddCommit alice1 [bob] + + traverse_ consumeWelcome (mpWelcome mp) + + message <- createApplicationMessage bob1 "some text" -- send the message as bob, who is not in the conversation err <- responseJsonError - =<< postMessage (qUnqualified (pUserId bob)) message + =<< postMessage (qUnqualified bob) (mpMessage message) do - -- send welcome message - postWelcome (qUnqualified $ pUserId creator) welcome - !!! const 201 === statusCode + users@[alice, bob] <- createAndConnectUsers [Nothing, Nothing] + runMLSTest $ do + [alice1, bob1] <- traverse createMLSClient users + void $ uploadNewKeyPackage bob1 + (_, qcnv) <- setupMLSGroup alice1 + commit <- createAddCommit alice1 [bob] + welcome <- liftIO $ case mpWelcome commit of + Nothing -> assertFailure "Expected welcome message" + Just w -> pure w + events <- mlsBracket [bob1] $ \wss -> do + es <- sendAndConsumeCommit commit + + WS.assertMatchN_ (5 # Second) wss $ + wsAssertMLSWelcome (cidQualifiedUser bob1) welcome + + pure es - -- check that the corresponding event is received - void . liftIO $ - WS.assertMatch (5 # WS.Second) wsB $ - wsAssertMLSWelcome (pUserId bob) welcome + event <- assertOne events + liftIO $ assertJoinEvent qcnv alice [bob] roleNameWireMember event testWelcomeNoKey :: TestM () testWelcomeNoKey = do - MessagingSetup {..} <- aliceInvitesBob (1, LocalUser) def {createClients = CreateWithoutKey} + users <- createAndConnectUsers [Nothing, Nothing] + runMLSTest $ do + [alice1, bob1] <- traverse createMLSClient users + void $ setupMLSGroup alice1 + + -- add bob using an "out-of-band" key package + (_, ref) <- generateKeyPackage bob1 + kp <- keyPackageFile bob1 ref + commit <- createAddCommitWithKeyPackages alice1 [(bob1, kp)] + welcome <- liftIO $ case mpWelcome commit of + Nothing -> assertFailure "Expected welcome message" + Just w -> pure w - postWelcome (qUnqualified (pUserId creator)) welcome - !!! const 404 === statusCode + err <- + responseJsonError =<< postWelcome (ciUser alice1) welcome + pure (Aeson.encode okResp) + "mls-welcome" -> pure (Aeson.encode EmptyResponse) ms -> assertFailure ("unmocked endpoint called: " <> cs ms) - (_resp, reqs) <- - withTempMockFederator' mockedResponse $ - postWelcome (qUnqualified $ pUserId alice) welcome - !!! const 201 === statusCode - - -- Assert the correct federated call is made. - fedWelcome <- assertOne (filter ((== "mls-welcome") . frRPC) reqs) - let req :: Maybe MLSWelcomeRequest = Aeson.decode (frBody fedWelcome) - liftIO $ req @?= (Just . MLSWelcomeRequest . Base64ByteString) welcome - --- | Send a commit message, and assert that all participants see an event with --- the given list of new members. -testSuccessfulCommitWithNewUsers :: HasCallStack => MessagingSetup -> [Qualified UserId] -> TestM () -testSuccessfulCommitWithNewUsers setup@MessagingSetup {..} newUsers = do - cannon <- view tsCannon - - WS.bracketRN cannon (map (qUnqualified . pUserId) users) $ \wss -> do - -- send commit message - events <- postCommit setup - - let alreadyPresent = - map snd - . filter (\(p, _) -> pUserId p `notElem` newUsers) - $ zip users wss + runMLSTest $ do + alice1 <- createMLSClient alice + _bob1 <- createFakeMLSClient bob + + void $ setupMLSGroup alice1 + commit <- createAddCommit alice1 [bob] + welcome <- liftIO $ case mpWelcome commit of + Nothing -> assertFailure "Expected welcome message" + Just w -> pure w + (_, reqs) <- + withTempMockFederator' mockedResponse $ + postWelcome (ciUser (mpSender commit)) welcome + !!! const 201 === statusCode + consumeWelcome welcome + fedWelcome <- assertOne (filter ((== "mls-welcome") . frRPC) reqs) + let req :: Maybe MLSWelcomeRequest = Aeson.decode (frBody fedWelcome) + liftIO $ req @?= (Just . MLSWelcomeRequest . Base64ByteString) welcome + +testAddUserWithBundle :: TestM () +testAddUserWithBundle = do + [alice, bob] <- createAndConnectUsers [Nothing, Nothing] + + qcnv <- runMLSTest $ do + (alice1 : bobClients) <- traverse createMLSClient [alice, bob, bob] + traverse_ uploadNewKeyPackage bobClients + (_, qcnv) <- setupMLSGroup alice1 + commit <- createAddCommit alice1 [bob] + welcome <- assertJust (mpWelcome commit) + + events <- mlsBracket bobClients $ \wss -> do + events <- sendAndConsumeCommitBundle commit + for_ (zip bobClients wss) $ \(c, ws) -> + WS.assertMatch (5 # Second) ws $ + wsAssertMLSWelcome (cidQualifiedUser c) welcome + pure events - liftIO $ - if null newUsers - then do - -- check that alice receives no events - events @?= [] - - -- check that no users receive join events - when (null alreadyPresent) $ - WS.assertNoEvent (1 # WS.Second) wss - else do - -- check that alice receives a join event - case events of - [e] -> assertJoinEvent conversation (pUserId creator) newUsers roleNameWireMember e - [] -> assertFailure "expected join event to be returned to alice" - es -> assertFailure $ "expected one event, found: " <> show es - - -- check that all users receive a join event, - for_ wss $ \ws -> do - WS.assertMatch_ (5 # WS.Second) ws $ - wsAssertMemberJoinWithRole conversation (pUserId creator) newUsers roleNameWireMember - - -- and that the already-present users in the conversation receive a commit - for_ alreadyPresent $ \ws -> do - WS.assertMatch_ (5 # WS.Second) ws $ - wsAssertMLSMessage conversation (pUserId creator) commit - -testSuccessfulRemoveMemberFromConvCommit :: - HasCallStack => - Participant -> - [Participant] -> - Qualified ConvId -> - ByteString -> - [Participant] -> - TestM () -testSuccessfulRemoveMemberFromConvCommit admin users conv commit participantsToRemove = do - cannon <- view tsCannon - - WS.bracketRN cannon (map (qUnqualified . pUserId) users) $ \wss -> do - events :: [Event] <- - fmap mmssEvents . responseJsonError - =<< postMessage (qUnqualified (pUserId admin)) commit - - WS.assertMatch_ (5 # WS.Second) ws $ - wsAssertMembersLeave conv (pUserId admin) (map pUserId participantsToRemove) - - -- all users (including the removed ones) receive the commit - for_ wss $ \ws -> do - WS.assertMatch_ (5 # WS.Second) ws $ - wsAssertMLSMessage conv (pUserId admin) commit - -testFailedCommit :: HasCallStack => MessagingSetup -> Int -> TestM Wai.Error -testFailedCommit MessagingSetup {..} status = do - cannon <- view tsCannon + event <- assertOne events + liftIO $ assertJoinEvent qcnv alice [bob] roleNameWireMember event + pure qcnv - WS.bracketRN cannon (map (qUnqualified . pUserId) users) $ \wss -> do - galley <- viewGalley + -- check that bob can now see the conversation + convs <- + responseJsonError =<< getConvs (qUnqualified bob) Nothing Nothing + MessagingSetup -> TestM () -testSuccessfulCommit setup = testSuccessfulCommitWithNewUsers setup (map pUserId (users setup)) + =<< postCommitBundle (ciUser (mpSender commit)) bundle + >= sendAndConsumeCommit + event <- assertOne events + liftIO $ assertJoinEvent qcnv alice [bob] roleNameWireMember event + pure qcnv -- check that bob can now see the conversation - let bob = head users convs <- - responseJsonError =<< getConvs (qUnqualified (pUserId bob)) Nothing Nothing + responseJsonError =<< getConvs (qUnqualified bob) Nothing Nothing do + err <- + responseJsonError + =<< postMessage (ciUser (mpSender commit)) (mpMessage commit) + do - (alice, users@[bob]) <- withLastPrekeys $ do - -- bob has 2 MLS clients - participants@(_, [bob]) <- setupParticipants tmp def [(2, LocalUser)] - - -- and a non-MLS client - void $ takeLastPrekey >>= lift . randomClient (qUnqualified (pUserId bob)) - - pure participants - - -- alice creates a conversation and adds Bob's MLS clients - (groupId, conversation) <- setupGroup tmp CreateConv alice "group" - (commit, welcome) <- liftIO $ setupCommit tmp alice "group" "group" (pClients bob) - - pure MessagingSetup {creator = alice, ..} - - testSuccessfulCommit setup + [alice, bob] <- createAndConnectUsers (replicate 2 Nothing) + runMLSTest $ do + alice1 <- createMLSClient alice + -- bob has 2 MLS clients + [bob1, bob2] <- replicateM 2 (createMLSClient bob) + traverse_ uploadNewKeyPackage [bob1, bob2] + -- and a non-MLS client + _bob3 <- createWireClient bob + + void $ setupMLSGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit testAddUserPartial :: TestM () testAddUserPartial = do - (creator, commit) <- withSystemTempDirectory "mls" $ \tmp -> do + [alice, bob, charlie] <- createAndConnectUsers (replicate 3 Nothing) + + runMLSTest $ do -- Bob has 3 clients, Charlie has 2 - (alice, [bob, charlie]) <- withLastPrekeys $ setupParticipants tmp def ((,LocalUser) <$> [3, 2]) - - -- upload one more key package for each of bob's clients - -- this makes sure the unused client has at least one key package, and - -- therefore will be considered MLS-capable - for_ (pClients bob) $ \(cid, c) -> do - kp <- - liftIO $ - decodeMLSError - =<< spawn (cli cid tmp ["key-package", "create"]) Nothing - addKeyPackage def {mapKeyPackage = False, setPublicKey = False} (pUserId bob) c kp - - void $ setupGroup tmp CreateConv alice "group" - (commit, _) <- - liftIO . setupCommit tmp alice "group" "group" $ - -- only 2 out of the 3 clients of Bob's are added to the conversation - NonEmpty.take 2 (pClients bob) <> toList (pClients charlie) - pure (alice, commit) - - galley <- viewGalley - - err <- - responseJsonError - =<< post - ( galley . paths ["mls", "messages"] - . zUser (qUnqualified (pUserId creator)) - . zConn "conn" - . content "message/mls" - . bytes commit - ) - do - withLastPrekeys $ do - (alice, [bob]) <- setupParticipants tmp def ((,LocalUser) <$> [1]) - (groupId, conversation) <- lift $ setupGroup tmp CreateConv alice "group" - (commit, welcome) <- liftIO . setupCommit tmp alice "group" "group" $ pClients bob - let setup = - MessagingSetup - { creator = alice, - users = [bob], - .. - } - lift $ testSuccessfulCommit setup + -- Only the first 2 clients of Bob's have uploaded key packages + traverse_ uploadNewKeyPackage (take 2 bobClients <> charlieClients) - -- create more clients for Bob, only take the first one - nc <- fmap head . replicateM 2 $ do - setupUserClient tmp CreateWithKey True (pUserId bob) + -- alice adds bob's first 2 clients + void $ setupMLSGroup alice1 + commit <- createAddCommit alice1 [bob, charlie] - -- add new client - (commit', welcome') <- - liftIO $ - setupCommit - tmp - alice - "group" - "group" - [(userClientQid (pUserId bob) nc, nc)] - - lift $ testSuccessfulCommitWithNewUsers setup {commit = commit', welcome = welcome'} [] - -testAddNewClient :: TestM () -testAddNewClient = do - withSystemTempDirectory "mls" $ \tmp -> withLastPrekeys $ do - -- bob starts with a single client - (creator, users@[bob]) <- setupParticipants tmp def [(1, LocalUser)] - (groupId, conversation) <- lift $ setupGroup tmp CreateConv creator "group" - - -- creator sends first commit message - do - (commit, welcome) <- liftIO $ setupCommit tmp creator "group" "group" (pClients bob) - lift $ testSuccessfulCommit MessagingSetup {..} + -- before alice can commit, bob3 uploads a key package + void $ uploadNewKeyPackage bob3 - do - -- then bob adds a new client - c <- setupUserClient tmp CreateWithKey True (pUserId bob) - let bobC = (userClientQid (pUserId bob) c, c) - -- which gets added to the group - (commit, welcome) <- liftIO $ setupCommit tmp creator "group" "group" [bobC] - -- and the corresponding commit is sent - lift $ testSuccessfulCommitWithNewUsers MessagingSetup {..} [] + -- alice sends a commit now, and should get a conflict error + err <- + responseJsonError + =<< postMessage (ciUser (mpSender commit)) (mpMessage commit) + >= sendAndConsumeCommit + + -- now bob2 and bob3 upload key packages, and alice adds bob2 only + kp <- uploadNewKeyPackage bob2 >>= keyPackageFile bob2 + void $ uploadNewKeyPackage bob3 + void $ + createAddCommitWithKeyPackages alice1 [(bob2, kp)] + >>= sendAndConsumeCommit testSendAnotherUsersCommit :: TestM () testSendAnotherUsersCommit = do - withSystemTempDirectory "mls" $ \tmp -> withLastPrekeys $ do - -- bob starts with a single client - (creator, users@[bob]) <- setupParticipants tmp def [(1, LocalUser)] - (groupId, conversation) <- lift $ setupGroup tmp CreateConv creator "group" + -- create users + [alice, bob] <- createAndConnectUsers (replicate 2 Nothing) - -- creator sends first commit message - do - (commit, welcome) <- liftIO $ setupCommit tmp creator "group" "group" (pClients bob) - lift $ testSuccessfulCommit MessagingSetup {..} + runMLSTest $ do + [alice1, bob1] <- traverse createMLSClient [alice, bob] - do - -- then bob adds a new client - c <- setupUserClient tmp CreateWithKey True (pUserId bob) - let bobC = (userClientQid (pUserId bob) c, c) - -- which gets added to the group - (commit, _welcome) <- liftIO $ setupCommit tmp creator "group" "group" [bobC] - -- and the corresponding commit is sent from bob instead of the creator - err <- lift (responseJsonError =<< postMessage (qUnqualified (pUserId bob)) commit >= void . sendAndConsumeCommit + + -- Alice creates a commit that adds bob2 + bob2 <- createMLSClient bob + -- upload key packages + void $ uploadNewKeyPackage bob2 + mp <- createAddCommit alice1 [bob] + -- and the corresponding commit is sent from Bob instead of Alice + err <- + responseJsonError + =<< postMessage (qUnqualified bob) (mpMessage mp) + setupMLSGroup alice1 + createAddCommit alice1 [bob] >>= void . sendAndConsumeCommit + e <- + responseJsonError + =<< postMembers + (qUnqualified alice) + (pure charlie) + qcnv + setupMLSGroup alice1 + createAddCommit alice1 [bob] >>= void . sendAndConsumeCommit + e <- + responseJsonError + =<< deleteMemberQualified + (qUnqualified alice) + bob + qcnv + setupMLSGroup alice1 + createAddCommit alice1 [bob] >>= void . sendAndConsumeCommit + e <- + responseJsonError + =<< postProteusMessageQualified + (qUnqualified alice) + (ciClient bob1) + qcnv + [] + "data" + MismatchReportAll + do - (creator, users) <- withLastPrekeys $ setupParticipants tmp def ((,LocalUser) <$> [2, 3]) - (groupId, conversation) <- setupGroup tmp CreateConv creator "group.0" - let (users1, users2) = splitAt 1 users +testStaleCommit = do + (alice : users) <- createAndConnectUsers (replicate 5 Nothing) + let (users1, users2) = splitAt 2 users - -- add the first batch of users to the conversation, but do not overwrite group - do - (commit, welcome) <- - liftIO $ - setupCommit tmp creator "group.0" "group.1" $ - users1 >>= toList . pClients - testSuccessfulCommit MessagingSetup {users = users1, ..} + runMLSTest $ do + (alice1 : clients) <- traverse createMLSClient (alice : users) + traverse_ uploadNewKeyPackage clients + void $ setupMLSGroup alice1 - -- now add the rest of the users to the original group state - do - (commit, welcome) <- - liftIO $ - setupCommit tmp creator "group.0" "group.2" $ - users2 >>= toList . pClients - err <- testFailedCommit MessagingSetup {..} 409 + -- add the first batch of users to the conversation + void $ createAddCommit alice1 users1 >>= sendAndConsumeCommit + + -- now roll back alice1 and try to add the second batch of users + void $ rollBackClient alice1 + commit <- createAddCommit alice1 users2 + err <- + responseJsonError + =<< postMessage (ciUser (mpSender commit)) (mpMessage commit) + pure (Aeson.encode ()) - "on-new-remote-conversation" -> pure (Aeson.encode EmptyResponse) - "get-mls-clients" -> - pure - . Aeson.encode - . Set.fromList - . map (flip ClientInfo True . snd) - . toList - . pClients - $ bob - ms -> assertFailure ("unmocked endpoint called: " <> cs ms) - (events, reqs) <- withTempMockFederator' mock $ do - postCommit setup + users@[alice, bob] <- createAndConnectUsers [Nothing, Just "bob.example.com"] + (events, reqs, qcnv) <- runMLSTest $ 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 EmptyResponse) + ms -> assertFailure ("unmocked endpoint called: " <> cs ms) + + commit <- createAddCommit alice1 [bob] + (events, reqs) <- + withTempMockFederator' mock $ + sendAndConsumeCommit commit + pure (events, reqs, qcnv) liftIO $ do req <- assertOne $ filter ((== "on-conversation-updated") . frRPC) reqs - frTargetDomain req @?= qDomain (pUserId bob) + frTargetDomain req @?= qDomain bob bdy <- case Aeson.eitherDecode (frBody req) of Right b -> pure b Left e -> assertFailure $ "Could not parse on-conversation-updated request body: " <> e - cuOrigUserId bdy @?= pUserId (creator setup) - cuConvId bdy @?= qUnqualified (conversation setup) - cuAlreadyPresentUsers bdy @?= [qUnqualified (pUserId bob)] + cuOrigUserId bdy @?= alice + cuConvId bdy @?= qUnqualified qcnv + cuAlreadyPresentUsers bdy @?= [qUnqualified bob] cuAction bdy @?= SomeConversationAction SConversationJoinTag ConversationJoin - { cjUsers = pure (pUserId bob), + { cjUsers = pure bob, cjRole = roleNameWireMember } liftIO $ do event <- assertOne events - assertJoinEvent - (conversation setup) - (pUserId (creator setup)) - [pUserId bob] - roleNameWireMember - event + assertJoinEvent qcnv alice [bob] roleNameWireMember event + +testAddRemoteUserWithBundle :: TestM () +testAddRemoteUserWithBundle = do + users@[alice, bob] <- createAndConnectUsers [Nothing, Just "bob.example.com"] + (events, reqs, qcnv) <- runMLSTest $ 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 EmptyResponse) + ms -> assertFailure ("unmocked endpoint called: " <> cs ms) + + commit <- createAddCommit alice1 [bob] + (events, reqs) <- + withTempMockFederator' mock $ + sendAndConsumeCommitBundle commit + pure (events, reqs, qcnv) + + liftIO $ do + req <- assertOne $ filter ((== "on-conversation-updated") . frRPC) reqs + frTargetDomain req @?= qDomain bob + bdy <- case Aeson.eitherDecode (frBody req) of + Right b -> pure b + Left e -> assertFailure $ "Could not parse on-conversation-updated request body: " <> e + cuOrigUserId bdy @?= alice + cuConvId bdy @?= qUnqualified qcnv + cuAlreadyPresentUsers bdy @?= [qUnqualified bob] + cuAction bdy + @?= SomeConversationAction + SConversationJoinTag + ConversationJoin + { cjUsers = pure bob, + cjRole = roleNameWireMember + } + + liftIO $ do + event <- assertOne events + assertJoinEvent qcnv alice [bob] roleNameWireMember event testCommitLock :: TestM () -testCommitLock = withSystemTempDirectory "mls" $ \tmp -> do - -- create MLS conversation - (creator, users) <- withLastPrekeys $ setupParticipants tmp def ((,LocalUser) <$> [2, 2, 2]) - (groupId, conversation) <- setupGroup tmp CreateConv creator "group" - let (users1, usersX) = splitAt 1 users - let (users2, users3) = splitAt 1 usersX - void $ assertOne users1 - void $ assertOne users2 - void $ assertOne users3 - - -- initial user can be added - do - (commit, welcome) <- - liftIO $ - setupCommit tmp creator "group" "group" $ - users1 >>= toList . pClients - testSuccessfulCommit MessagingSetup {users = users1, ..} +testCommitLock = do + users <- createAndConnectUsers (replicate 4 Nothing) - -- can commit without blocking - do - (commit, welcome) <- - liftIO $ - setupCommit tmp creator "group" "group" $ - users2 >>= toList . pClients - testSuccessfulCommit MessagingSetup {users = users2, ..} + runMLSTest $ do + [alice1, bob1, charlie1, dee1] <- traverse createMLSClient users + (groupId, _) <- setupMLSGroup alice1 + traverse_ uploadNewKeyPackage [bob1, charlie1, dee1] - -- block epoch - casClient <- view tsCass - runClient casClient $ insertLock (convToGroupId (qTagUnsafe conversation)) (Epoch 2) + -- alice adds add bob + void $ createAddCommit alice1 [cidQualifiedUser bob1] >>= sendAndConsumeCommit - -- commit fails due to competing lock - do - (commit, welcome) <- - liftIO $ - setupCommit tmp creator "group" "group" $ - users3 >>= toList . pClients - -- assert HTTP 409 on next attempt to commit - err <- testFailedCommit MessagingSetup {..} 409 - liftIO $ Wai.label err @?= "mls-stale-message" + -- alice adds charlie + void $ createAddCommit alice1 [cidQualifiedUser charlie1] >>= sendAndConsumeCommit - -- unblock epoch - runClient casClient $ deleteLock (convToGroupId (qTagUnsafe conversation)) (Epoch 2) + -- simulate concurrent commit by blocking epoch + casClient <- view tsCass + runClient casClient $ insertLock groupId (Epoch 2) + + -- commit should fail due to competing lock + do + commit <- createAddCommit alice1 [cidQualifiedUser dee1] + err <- + responseJsonError + =<< postMessage (ciUser alice1) (mpMessage commit) + do LocalQuorum (groupId, epoch) ) - unlock :: PrepQuery W (GroupId, Epoch) () - unlock = "delete from mls_commit_locks where group_id = ? and epoch = ?" - deleteLock groupId epoch = - retry x5 $ - write - unlock - ( params - LocalQuorum - (groupId, epoch) - ) testAddUserBareProposalCommit :: TestM () -testAddUserBareProposalCommit = withSystemTempDirectory "mls" $ \tmp -> do - (alice, [bob]) <- withLastPrekeys $ setupParticipants tmp def [(1, LocalUser)] - - (groupId, conversation) <- setupGroup tmp CreateConv alice "group" - - prop <- liftIO $ bareAddProposal tmp alice bob "group" "group" - postMessage (qUnqualified (pUserId alice)) prop - !!! const 201 === statusCode - - (commit, mbWelcome) <- - liftIO $ - pendingProposalsCommit tmp alice "group" - - welcome <- assertJust mbWelcome - - testSuccessfulCommit MessagingSetup {creator = alice, users = [bob], ..} - - -- check that bob can now see the conversation - convs <- - responseJsonError =<< getConvs (qUnqualified (pUserId bob)) Nothing Nothing - >= traverse_ sendAndConsumeMessage + commit <- createPendingProposalCommit alice1 + void $ assertJust (mpWelcome commit) + void $ sendAndConsumeCommit commit + + -- check that bob can now see the conversation + liftTest $ do + convs <- + responseJsonError =<< getConvs (ciUser bob1) Nothing Nothing + do - (alice, [bob]) <- withLastPrekeys $ setupParticipants tmp def [(1, LocalUser)] - - (groupId, conversation) <- setupGroup tmp CreateConv alice "group" - - -- create proposal, but don't send it to group - void $ liftIO $ bareAddProposal tmp alice bob "group" "group" - - (commit, mbWelcome) <- - liftIO $ - pendingProposalsCommit tmp alice "group" - - welcome <- assertJust mbWelcome - - err <- testFailedCommit (MessagingSetup {creator = alice, users = [bob], ..}) 404 - liftIO $ Wai.label err @?= "mls-proposal-not-found" +testUnknownProposalRefCommit = do + [alice, bob] <- createAndConnectUsers (replicate 2 Nothing) + runMLSTest $ do + [alice1, bob1] <- traverse createMLSClient [alice, bob] + void $ setupMLSGroup alice1 + void $ uploadNewKeyPackage bob1 + + -- create proposal, but don't send it to group + void $ createAddProposals alice1 [bob] + commit <- createPendingProposalCommit alice1 + + -- send commit before proposal + err <- + responseJsonError =<< postMessage (ciUser alice1) (mpMessage commit) + do - (alice, [bob, dee]) <- withLastPrekeys $ setupParticipants tmp def [(1, LocalUser), (1, LocalUser)] +testCommitNotReferencingAllProposals = do + users@[_alice, bob, charlie] <- createAndConnectUsers (replicate 3 Nothing) - (groupId, conversation) <- setupGroup tmp CreateConv alice "group" + runMLSTest $ do + [alice1, bob1, charlie1] <- traverse createMLSClient users + void $ setupMLSGroup alice1 + traverse_ uploadNewKeyPackage [bob1, charlie1] - propBob <- liftIO $ bareAddProposal tmp alice bob "group" "group" - postMessage (qUnqualified (pUserId alice)) propBob - !!! const 201 === statusCode + -- create proposals for bob and charlie + createAddProposals alice1 [bob, charlie] + >>= traverse_ sendAndConsumeMessage - propDee <- liftIO $ bareAddProposal tmp alice dee "group" "group2" - postMessage (qUnqualified (pUserId alice)) propDee - !!! const 201 === statusCode + -- now create a commit referencing only the first proposal + void $ rollBackClient alice1 + commit <- createPendingProposalCommit alice1 - (commit, mbWelcome) <- - liftIO $ - pendingProposalsCommit tmp alice "group" - - welcome <- assertJust mbWelcome - - err <- testFailedCommit (MessagingSetup {creator = alice, users = [bob, dee], ..}) 409 - liftIO $ Wai.label err @?= "mls-commit-missing-references" + -- send commit and expect and error + err <- + responseJsonError =<< postMessage (ciUser alice1) (mpMessage commit) + do - MessagingSetup {..} <- aliceInvitesBobWithTmp tmp (2, LocalUser) def {createConv = CreateConv} - let [bob] = users - - testSuccessfulCommit MessagingSetup {users = [bob], ..} - - (removalCommit, _mbWelcome) <- liftIO $ setupRemoveCommit tmp creator "group" "group" (pClients bob) +testAdminRemovesUserFromConv = do + [alice, bob] <- createAndConnectUsers [Nothing, Nothing] + (qcnv, events) <- runMLSTest $ do + [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] + traverse_ uploadNewKeyPackage [bob1, bob2] + (_, qcnv) <- setupMLSGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit + events <- createRemoveCommit alice1 [bob1, bob2] >>= sendAndConsumeCommit + pure (qcnv, events) + + liftIO $ assertOne events >>= assertLeaveEvent qcnv alice [bob] do convs <- - responseJsonError =<< getConvs (qUnqualified (pUserId bob)) Nothing Nothing - do - MessagingSetup {..} <- aliceInvitesBobWithTmp tmp (2, LocalUser) def {createConv = CreateConv} - let [bob] = users - - testSuccessfulCommit MessagingSetup {users = [bob], ..} - - -- remove only first client of bob - (removalCommit, _mbWelcome) <- liftIO $ setupRemoveCommit tmp creator "group" "group" [NE.head (pClients bob)] - - err <- - responseJsonError - =<< postMessage (qUnqualified (pUserId creator)) removalCommit - TestM () -testRemoveDeletedClient deleteClientBefore = withSystemTempDirectory "mls" $ \tmp -> do - (creator, [bob, dee]) <- withLastPrekeys $ setupParticipants tmp def [(2, LocalUser), (1, LocalUser)] - - -- create a group - (groupId, conversation) <- setupGroup tmp CreateConv creator "group" - - -- add clients to it and get welcome message - (addCommit, welcome) <- - liftIO $ - setupCommit tmp creator "group" "group" $ - NonEmpty.tail (pClients creator) <> toList (pClients bob) <> toList (pClients dee) - - testSuccessfulCommit MessagingSetup {users = [bob, dee], commit = addCommit, ..} - - let (_bobClient1, bobClient2) = assertTwo (toList (pClients bob)) - - when deleteClientBefore $ - deleteClient (qUnqualified (pUserId bob)) (snd bobClient2) (Just defPassword) - !!! statusCode === const 200 - - void . liftIO $ - spawn - ( cli - (pClientQid bob) - tmp - [ "group", - "from-welcome", - "--group-out", - tmp "group", - tmp "welcome" - ] - ) - Nothing - - void . liftIO $ - spawn - ( cli - (pClientQid dee) - tmp - [ "group", - "from-welcome", - "--group-out", - tmp "group", - tmp "welcome" - ] - ) - Nothing - - (removalCommit, _mbWelcome) <- liftIO $ setupRemoveCommit tmp dee "group" "group" [bobClient2] - - -- dee (which is not an admin) commits removal of bob's deleted client - let doCommitRemoval = postMessage (qUnqualified (pUserId dee)) removalCommit - - if deleteClientBefore - then do - events :: [Event] <- - fmap mmssEvents . responseJsonError - =<< doCommitRemoval - do - MessagingSetup {..} <- aliceInvitesBobWithTmp tmp (2, LocalUser) def {createConv = CreateConv} - let [bob] = users - - testSuccessfulCommit MessagingSetup {users = [bob], ..} - - -- attempt to remove only first client of bob - (removalCommit, _mbWelcome) <- liftIO $ setupRemoveCommit tmp creator "group" "group" [NonEmpty.head (pClients bob)] - - err <- - responseJsonError - =<< postMessage (qUnqualified (pUserId creator)) removalCommit - >= sendAndConsumeCommit + commit <- createRemoveCommit alice1 [bob1] - liftIO $ Wai.label err @?= "mls-client-mismatch" + err <- + responseJsonError + =<< postMessage (qUnqualified alice) (mpMessage commit) + do - let opts = - def - { createClients = DontCreateClients, - createConv = CreateConv - } - (alice, [bob]) <- - withLastPrekeys $ - setupParticipants tmp opts [(1, RemoteUser (Domain "faraway.example.com"))] - (groupId, conversation) <- setupGroup tmp CreateConv alice "group" - (commit, welcome) <- liftIO $ setupCommit tmp alice "group" "group" (pClients bob) - message <- - liftIO $ - spawn (cli (pClientQid alice) tmp ["message", "--group", tmp "group", "some text"]) Nothing - - 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 EmptyResponse) - "get-mls-clients" -> - pure - . Aeson.encode - . Set.fromList - . map (flip ClientInfo True . snd) - . toList - . pClients - $ bob - ms -> assertFailure ("unmocked endpoint called: " <> cs ms) - (events :: [Event], reqs) <- fmap (first mmssEvents) . withTempMockFederator' mock $ do - galley <- viewGalley - void $ postCommit MessagingSetup {creator = alice, users = [bob], ..} - let v2 = toByteString' (toLower <$> show V2) - responseJsonError - =<< post - ( galley . paths [v2, "mls", "messages"] - . zUser (qUnqualified (pUserId alice)) - . zConn "conn" - . content "message/mls" - . bytes message - ) - pure (Aeson.encode ()) + "on-new-remote-conversation" -> pure (Aeson.encode EmptyResponse) + "on-mls-message-sent" -> pure (Aeson.encode EmptyResponse) + "mls-welcome" -> pure (Aeson.encode EmptyResponse) + "get-mls-clients" -> + pure + . Aeson.encode + . Set.singleton + $ ClientInfo (ciClient bob1) True + ms -> assertFailure ("unmocked endpoint called: " <> cs ms) - liftIO $ do - req <- assertOne $ filter ((== "on-mls-message-sent") . frRPC) reqs - frTargetDomain req @?= qDomain (pUserId bob) - bdy <- case Aeson.eitherDecode (frBody req) of - Right b -> pure b - Left e -> assertFailure $ "Could not parse on-mls-message-sent request body: " <> e - rmmSender bdy @?= pUserId alice - rmmConversation bdy @?= qUnqualified conversation - rmmRecipients bdy - @?= [(qUnqualified (pUserId bob), c) | (_, c) <- toList (pClients bob)] - rmmMessage bdy @?= Base64ByteString message + ((message, events), reqs) <- withTempMockFederator' mock $ do + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit + message <- createApplicationMessage alice1 "hello" + events <- sendAndConsumeMessage message + pure (message, events) - liftIO $ assertBool "Unexpected events returned" (null events) + liftIO $ do + req <- assertOne $ filter ((== "on-mls-message-sent") . frRPC) reqs + frTargetDomain req @?= qDomain bob + bdy <- case Aeson.eitherDecode (frBody req) of + Right b -> pure b + Left e -> assertFailure $ "Could not parse on-mls-message-sent request body: " <> e + rmmSender bdy @?= alice + rmmConversation bdy @?= qUnqualified qcnv + rmmRecipients bdy @?= [(ciUser bob1, ciClient bob1)] + rmmMessage bdy @?= Base64ByteString (mpMessage message) + + liftIO $ assertBool "Unexpected events returned" (null events) -- The following test happens within backend B -- Alice@A is remote and Bob@B is local @@ -1001,243 +891,143 @@ testRemoteAppMessage = withSystemTempDirectory "mls" $ \tmp -> do -- faked: 4 -- actual test step: 12 14 testLocalToRemote :: TestM () -testLocalToRemote = withSystemTempDirectory "mls" $ \tmp -> do - let domain = Domain "faraway.example.com" - -- step 2 - MessagingSetup {creator = alice, users = [bob], ..} <- - aliceInvitesBobWithTmp - tmp - (1, LocalUser) - def - { creatorOrigin = RemoteUser domain - } - - -- step 10 - liftIO $ mergeWelcome tmp (pClientQid bob) "group" "groupB.json" "welcome" - -- step 11 - message <- - liftIO $ - spawn - ( cli - (pClientQid bob) - tmp - ["message", "--group", tmp "groupB.json", "hi"] - ) - Nothing - - fedGalleyClient <- view tsFedGalleyClient - - -- register remote conversation: step 4 - qcnv <- randomQualifiedId (qDomain (pUserId alice)) - let nrc = - NewRemoteConversation (qUnqualified qcnv) $ - ProtocolMLS (ConversationMLSData groupId (Epoch 1) MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519) - void $ - runFedClient - @"on-new-remote-conversation" - fedGalleyClient - (qDomain (pUserId alice)) - nrc - - -- A notifies B about bob being in the conversation (Join event): step 5 - now <- liftIO getCurrentTime - let cu = - ConversationUpdate - { cuTime = now, - cuOrigUserId = pUserId alice, - cuConvId = qUnqualified qcnv, - cuAlreadyPresentUsers = [qUnqualified $ pUserId bob], - cuAction = - SomeConversationAction - SConversationJoinTag - ConversationJoin - { cjUsers = pure (pUserId bob), - cjRole = roleNameWireMember - } - } - void $ - runFedClient - @"on-conversation-updated" - fedGalleyClient - (qDomain (pUserId alice)) - cu - - 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 $ do - galley <- viewGalley - - -- bob sends a message: step 12 - post - ( galley . paths ["mls", "messages"] - . zUser (qUnqualified (pUserId bob)) - . zConn "conn" - . content "message/mls" - . bytes message - ) - !!! const 201 - === statusCode - - -- check requests to mock federator: step 14 - liftIO $ do - req <- assertOne reqs - frRPC req @?= "send-mls-message" - frTargetDomain req @?= qDomain qcnv - bdy <- case Aeson.eitherDecode (frBody req) of - Right b -> pure b - Left e -> assertFailure $ "Could not parse send-mls-message request body: " <> e - msrConvId bdy @?= qUnqualified qcnv - msrSender bdy @?= qUnqualified (pUserId bob) - msrRawMessage bdy @?= Base64ByteString message +testLocalToRemote = do + -- create users + let aliceDomain = Domain "faraway.example.com" + [alice, bob] <- createAndConnectUsers [Just (domainText aliceDomain), Nothing] + + runMLSTest $ do + [alice1, bob1] <- traverse createMLSClient [alice, bob] + + -- upload key packages + void $ uploadNewKeyPackage bob1 + + -- step 2 + (groupId, qcnv) <- setupFakeMLSGroup alice1 + mp <- createAddCommit alice1 [bob] + -- step 10 + traverse_ consumeWelcome (mpWelcome mp) + -- step 11 + message <- createApplicationMessage bob1 "hi" + + -- register remote conversation: step 4 + receiveNewRemoteConv qcnv groupId + -- 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 $ + -- bob sends a message: step 12 + sendAndConsumeMessage message + + -- check requests to mock federator: step 14 + liftIO $ do + req <- assertOne reqs + frRPC req @?= "send-mls-message" + frTargetDomain req @?= qDomain qcnv + bdy <- case Aeson.eitherDecode (frBody req) of + Right b -> pure b + Left e -> assertFailure $ "Could not parse send-mls-message request body: " <> e + msrConvId bdy @?= qUnqualified qcnv + msrSender bdy @?= qUnqualified bob + msrRawMessage bdy @?= Base64ByteString (mpMessage message) testLocalToRemoteNonMember :: TestM () -testLocalToRemoteNonMember = withSystemTempDirectory "mls" $ \tmp -> do +testLocalToRemoteNonMember = do + -- create users let domain = Domain "faraway.example.com" - -- step 2 - MessagingSetup {creator = alice, users = [bob], ..} <- - aliceInvitesBobWithTmp - tmp - (1, LocalUser) - def - { creatorOrigin = RemoteUser domain - } - - -- step 10 - liftIO $ mergeWelcome tmp (pClientQid bob) "group" "groupB.json" "welcome" - -- step 11 - message <- - liftIO $ - spawn - ( cli - (pClientQid bob) - tmp - ["message", "--group", tmp "groupB.json", "hi"] - ) - Nothing + [alice, bob] <- createAndConnectUsers [Just (domainText domain), Nothing] - fedGalleyClient <- view tsFedGalleyClient + runMLSTest $ do + [alice1, bob1] <- traverse createMLSClient [alice, bob] - -- register remote conversation: step 4 - qcnv <- randomQualifiedId (qDomain (pUserId alice)) - let nrc = - NewRemoteConversation (qUnqualified qcnv) $ - ProtocolMLS (ConversationMLSData groupId (Epoch 1) MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519) - void $ - runFedClient - @"on-new-remote-conversation" - fedGalleyClient - (qDomain (pUserId alice)) - nrc - - 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 - galley <- viewGalley - - -- bob sends a message: step 12 - post - ( galley . paths ["mls", "messages"] - . zUser (qUnqualified (pUserId bob)) - . zConn "conn" - . content "message/mls" - . bytes message - ) - !!! do - const 404 === statusCode - const (Just "no-conversation-member") === fmap Wai.label . responseJsonError + void $ uploadNewKeyPackage bob1 -testAppMessage :: TestM () -testAppMessage = withSystemTempDirectory "mls" $ \tmp -> do - (creator, users) <- withLastPrekeys $ setupParticipants tmp def ((,LocalUser) <$> [1, 2, 3]) - (groupId, conversation) <- setupGroup tmp CreateConv creator "group" + -- step 2 + (groupId, qcnv) <- setupFakeMLSGroup alice1 - (commit, welcome) <- - liftIO $ - setupCommit tmp creator "group" "group" $ - users >>= toList . pClients + mp <- createAddCommit alice1 [bob] + -- step 10 + traverse_ consumeWelcome (mpWelcome mp) + -- step 11 + message <- createApplicationMessage bob1 "hi" - void $ postCommit MessagingSetup {..} - message <- liftIO $ createMessage tmp creator "group" "some text" + -- register remote conversation: step 4 + receiveNewRemoteConv qcnv groupId - galley <- viewGalley - cannon <- view tsCannon + let mock req = case frRPC req of + "send-mls-message" -> pure (Aeson.encode (MLSMessageResponseUpdates [])) + rpc -> assertFailure $ "unmocked RPC called: " <> T.unpack rpc - WS.bracketRN - cannon - (map (qUnqualified . pUserId) (creator : users)) - $ \wss -> do - post - ( galley . paths ["mls", "messages"] - . zUser (qUnqualified (pUserId creator)) - . zConn "conn" - . content "message/mls" - . bytes message - ) - !!! const 201 - === statusCode + void $ + withTempMockFederator' mock $ do + galley <- viewGalley - -- check that the corresponding event is received + -- bob sends a message: step 12 + post + ( galley . paths ["mls", "messages"] + . zUser (qUnqualified bob) + . zConn "conn" + . content "message/mls" + . bytes (mpMessage message) + ) + !!! do + const 404 === statusCode + const (Just "no-conversation-member") + === fmap Wai.label . responseJsonError +testAppMessage :: TestM () +testAppMessage = do + users@(alice : _) <- createAndConnectUsers (replicate 4 Nothing) + + runMLSTest $ do + clients@(alice1 : _) <- traverse createMLSClient users + traverse_ uploadNewKeyPackage (tail clients) + (_, qcnv) <- setupMLSGroup alice1 + void $ createAddCommit alice1 (tail users) >>= sendAndConsumeCommit + message <- createApplicationMessage alice1 "some text" + + mlsBracket clients $ \wss -> do + events <- sendAndConsumeMessage message + liftIO $ events @?= [] liftIO $ WS.assertMatchN_ (5 # WS.Second) wss $ - wsAssertMLSMessage conversation (pUserId creator) message + wsAssertMLSMessage qcnv alice (mpMessage message) testAppMessage2 :: TestM () testAppMessage2 = do - (MessagingSetup {..}, message) <- withSystemTempDirectory "mls" $ \tmp -> do - (creator, users) <- withLastPrekeys $ setupParticipants tmp def ((,LocalUser) <$> [2, 1]) - (groupId, conversation) <- setupGroup tmp CreateConv creator "group" + -- create users + [alice, bob, charlie] <- createAndConnectUsers (replicate 3 Nothing) - (commit, welcome) <- - liftIO $ - setupCommit tmp creator "group" "group" $ - users >>= toList . pClients + runMLSTest $ do + alice1 : clients@[bob1, _bob2, _charlie1] <- + traverse createMLSClient [alice, bob, bob, charlie] - let setup = MessagingSetup {..} - void $ postCommit setup + -- upload key packages + traverse_ uploadNewKeyPackage clients - let bob = head users - liftIO $ mergeWelcome tmp (pClientQid bob) "group" "group" "welcome" - message <- - liftIO $ - createMessage tmp bob "group" "some text" - pure (setup, message) + -- create group with alice1 and other clients + conversation <- snd <$> setupMLSGroup alice1 + mp <- createAddCommit alice1 [bob, charlie] + void $ sendAndConsumeCommit mp - let (bob, charlie) = assertTwo users - galley <- viewGalley - cannon <- view tsCannon + traverse_ consumeWelcome (mpWelcome mp) - let mkClients p = do - c <- pClients p - pure (qUnqualified (pUserId p), snd c) - - WS.bracketAsClientRN - cannon - ( toList (mkClients creator) - <> NonEmpty.tail (mkClients bob) - <> toList (mkClients charlie) - ) - $ \wss -> do - post - ( galley . paths ["mls", "messages"] - . zUser (qUnqualified (pUserId bob)) - . zConn "conn" - . content "message/mls" - . bytes message - ) - !!! const 201 - === statusCode + message <- createApplicationMessage bob1 "some text" + + mlsBracket (alice1 : clients) $ \wss -> do + events <- sendAndConsumeMessage message + liftIO $ events @?= [] -- check that the corresponding event is received liftIO $ WS.assertMatchN_ (5 # WS.Second) wss $ - wsAssertMLSMessage conversation (pUserId bob) message + wsAssertMLSMessage conversation bob (mpMessage message) testRemoteToRemote :: TestM () testRemoteToRemote = do @@ -1300,20 +1090,20 @@ testRemoteToLocal = do -- bob then sends a message to the conversation let bobDomain = Domain "faraway.example.com" + -- create users + [alice, bob] <- createAndConnectUsers [Nothing, Just (domainText bobDomain)] -- Simulate the whole MLS setup for both clients first. In reality, -- backend calls would need to happen in order for bob to get ahold of a -- welcome message, but that should not affect the correctness of the test. - (MessagingSetup {..}, message) <- withSystemTempDirectory "mls" $ \tmp -> do - setup <- - aliceInvitesBobWithTmp - tmp - (1, RemoteUser bobDomain) - def - { createConv = CreateConv - } - bob <- assertOne (users setup) + runMLSTest $ do + [alice1, bob1] <- traverse createMLSClient [alice, bob] + + (_groupId, qcnv) <- setupMLSGroup alice1 + kpb <- claimKeyPackages alice1 bob + mp <- createAddCommit alice1 [bob] + let mockedResponse fedReq = case frRPC fedReq of "mls-welcome" -> pure (Aeson.encode EmptyResponse) @@ -1322,48 +1112,35 @@ testRemoteToLocal = do "get-mls-clients" -> pure . Aeson.encode - . Set.fromList - . map (flip ClientInfo True . snd) - . toList - . pClients - $ bob + . Set.singleton + $ ClientInfo (ciClient bob1) True + "claim-key-packages" -> pure . Aeson.encode $ kpb ms -> assertFailure ("unmocked endpoint called: " <> cs ms) void . withTempMockFederator' mockedResponse $ - postCommit setup - liftIO $ mergeWelcome tmp (pClientQid bob) "group" "groupB.json" "welcome" - message <- - liftIO $ - spawn - ( cli - (pClientQid bob) - tmp - ["message", "--group", tmp "groupB.json", "hello from another backend"] - ) - Nothing - pure (setup, message) + sendAndConsumeCommit mp - let bob = head users - let alice = creator + traverse_ consumeWelcome (mpWelcome mp) + message <- createApplicationMessage bob1 "hello from another backend" - fedGalleyClient <- view tsFedGalleyClient - cannon <- view tsCannon + fedGalleyClient <- view tsFedGalleyClient + cannon <- view tsCannon - -- actual test + -- actual test - let msr = - MessageSendRequest - { msrConvId = qUnqualified conversation, - msrSender = qUnqualified (pUserId bob), - msrRawMessage = Base64ByteString message - } + let msr = + MessageSendRequest + { msrConvId = qUnqualified qcnv, + msrSender = qUnqualified bob, + msrRawMessage = Base64ByteString (mpMessage message) + } - WS.bracketR cannon (qUnqualified (pUserId alice)) $ \ws -> do - resp <- runFedClient @"send-mls-message" fedGalleyClient bobDomain msr - liftIO $ do - resp @?= MLSMessageResponseUpdates [] - WS.assertMatch_ (5 # Second) ws $ - wsAssertMLSMessage conversation (pUserId bob) message + WS.bracketR cannon (qUnqualified alice) $ \ws -> do + resp <- runFedClient @"send-mls-message" fedGalleyClient bobDomain msr + liftIO $ do + resp @?= MLSMessageResponseUpdates [] + WS.assertMatch_ (5 # Second) ws $ + wsAssertMLSMessage qcnv bob (mpMessage message) testRemoteToLocalWrongConversation :: TestM () testRemoteToLocalWrongConversation = do @@ -1372,20 +1149,19 @@ testRemoteToLocalWrongConversation = do -- bob then sends a message to the conversation let bobDomain = Domain "faraway.example.com" + [alice, bob] <- createAndConnectUsers [Nothing, Just (domainText bobDomain)] -- Simulate the whole MLS setup for both clients first. In reality, -- backend calls would need to happen in order for bob to get ahold of a -- welcome message, but that should not affect the correctness of the test. - (MessagingSetup {..}, message) <- withSystemTempDirectory "mls" $ \tmp -> do - setup <- - aliceInvitesBobWithTmp - tmp - (1, RemoteUser bobDomain) - def - { createConv = CreateConv - } - bob <- assertOne (users setup) + runMLSTest $ do + [alice1, bob1] <- traverse createMLSClient [alice, bob] + + void $ claimKeyPackages alice1 bob + void $ setupMLSGroup alice1 + mp <- createAddCommit alice1 [bob] + let mockedResponse fedReq = case frRPC fedReq of "mls-welcome" -> pure (Aeson.encode EmptyResponse) @@ -1394,42 +1170,27 @@ testRemoteToLocalWrongConversation = do "get-mls-clients" -> pure . Aeson.encode - . Set.fromList - . map (flip ClientInfo True . snd) - . toList - . pClients - $ bob + . Set.singleton + $ ClientInfo (ciClient bob1) True ms -> assertFailure ("unmocked endpoint called: " <> cs ms) - void . withTempMockFederator' mockedResponse $ - postCommit setup - liftIO $ mergeWelcome tmp (pClientQid bob) "group" "groupB.json" "welcome" - message <- - liftIO $ - spawn - ( cli - (pClientQid bob) - tmp - ["message", "--group", tmp "groupB.json", "hello from another backend"] - ) - Nothing - pure (setup, message) + void . withTempMockFederator' mockedResponse $ sendAndConsumeCommit mp + traverse_ consumeWelcome (mpWelcome mp) + message <- createApplicationMessage bob1 "hello from another backend" - let bob = head users - - fedGalleyClient <- view tsFedGalleyClient + fedGalleyClient <- view tsFedGalleyClient - -- actual test - randomConfId <- randomId - let msr = - MessageSendRequest - { msrConvId = randomConfId, - msrSender = qUnqualified (pUserId bob), - msrRawMessage = Base64ByteString message - } + -- actual test + randomConfId <- randomId + let msr = + MessageSendRequest + { msrConvId = randomConfId, + msrSender = qUnqualified bob, + msrRawMessage = Base64ByteString (mpMessage message) + } - resp <- runFedClient @"send-mls-message" fedGalleyClient bobDomain msr - liftIO $ resp @?= MLSMessageResponseError MLSGroupConversationMismatch + resp <- runFedClient @"send-mls-message" fedGalleyClient bobDomain msr + liftIO $ resp @?= MLSMessageResponseError MLSGroupConversationMismatch testRemoteNonMemberToLocal :: TestM () testRemoteNonMemberToLocal = do @@ -1438,200 +1199,248 @@ testRemoteNonMemberToLocal = do -- bob then sends a message to the conversation let bobDomain = Domain "faraway.example.com" + [alice, bob] <- createAndConnectUsers [Nothing, Just (domainText bobDomain)] -- Simulate the whole MLS setup for both clients first. In reality, -- backend calls would need to happen in order for bob to get ahold of a -- welcome message, but that should not affect the correctness of the test. - (MessagingSetup {..}, message) <- withSystemTempDirectory "mls" $ \tmp -> do - setup <- - aliceInvitesBobWithTmp - tmp - (1, RemoteUser bobDomain) - def - { createConv = CreateConv - } - bob <- assertOne (users setup) - liftIO $ mergeWelcome tmp (pClientQid bob) "group" "groupB.json" "welcome" - message <- - liftIO $ - spawn - ( cli - (pClientQid bob) - tmp - ["message", "--group", tmp "groupB.json", "hello from another backend"] - ) - Nothing - pure (setup, message) + runMLSTest $ do + [alice1, bob1] <- traverse createMLSClient [alice, bob] - let bob = head users - fedGalleyClient <- view tsFedGalleyClient + qcnv <- snd <$> setupMLSGroup alice1 + void $ claimKeyPackages alice1 bob + mp <- createAddCommit alice1 [bob] + traverse_ consumeWelcome (mpWelcome mp) - -- actual test + message <- createApplicationMessage bob1 "hello from another backend" - let msr = - MessageSendRequest - { msrConvId = qUnqualified conversation, - msrSender = qUnqualified (pUserId bob), - msrRawMessage = Base64ByteString message - } + let msr = + MessageSendRequest + { msrConvId = qUnqualified qcnv, + msrSender = qUnqualified bob, + msrRawMessage = Base64ByteString (mpMessage message) + } - resp <- runFedClient @"send-mls-message" fedGalleyClient bobDomain msr - liftIO $ do - resp @?= MLSMessageResponseError ConvNotFound + fedGalleyClient <- view tsFedGalleyClient + resp <- runFedClient @"send-mls-message" fedGalleyClient bobDomain msr + liftIO $ do + resp @?= MLSMessageResponseError ConvNotFound -- | The group exists in mls-test-cli's store, but not in wire-server's database. propNonExistingConv :: TestM () -propNonExistingConv = withSystemTempDirectory "mls" $ \tmp -> do - (creator, [bob]) <- withLastPrekeys $ setupParticipants tmp def [(1, LocalUser)] - - let groupId = toBase64Text "test_group" - groupJSON <- - liftIO $ - spawn (cli (pClientQid creator) tmp ["group", "create", T.unpack groupId]) Nothing - liftIO $ BS.writeFile (tmp cs groupId) groupJSON - - prop <- - liftIO $ - spawn - ( cli - (pClientQid creator) - tmp - [ "proposal", - "--group-in", - tmp cs groupId, - "--in-place", - "add", - tmp pClientQid bob - ] - ) - Nothing - postMessage (qUnqualified (pUserId creator)) prop !!! do - const 404 === statusCode - const (Just "no-conversation") === fmap Wai.label . responseJsonError +propNonExistingConv = do + [alice, bob] <- createAndConnectUsers (replicate 2 Nothing) + runMLSTest $ do + [alice1, bob1] <- traverse createMLSClient [alice, bob] + void $ uploadNewKeyPackage bob1 + createGroup alice1 "test_group" + + [prop] <- createAddProposals alice1 [bob] + postMessage (ciUser alice1) (mpMessage prop) !!! do + const 404 === statusCode + const (Just "no-conversation") === fmap Wai.label . responseJsonError propExistingConv :: TestM () -propExistingConv = withSystemTempDirectory "mls" $ \tmp -> do - (creator, [bob]) <- withLastPrekeys $ setupParticipants tmp def [(1, LocalUser)] - -- setupGroup :: HasCallStack => FilePath -> CreateConv -> Participant -> String -> TestM (Qualified ConvId) - void $ setupGroup tmp CreateConv creator "group.json" - - prop <- liftIO $ bareAddProposal tmp creator bob "group.json" "group.json" +propExistingConv = do + [alice, bob] <- createAndConnectUsers (replicate 2 Nothing) + runMLSTest $ do + [alice1, bob1] <- traverse createMLSClient [alice, bob] + void $ uploadNewKeyPackage bob1 + void $ setupMLSGroup alice1 + events <- createAddProposals alice1 [bob] >>= traverse sendAndConsumeMessage - events <- - fmap mmssEvents . responseJsonError - =<< postMessage (qUnqualified (pUserId creator)) prop - do - (creator, users) <- withLastPrekeys $ setupParticipants tmp def [(1, LocalUser), (1, LocalUser), (1, LocalUser)] - (groupId, conversation) <- setupGroup tmp CreateConv creator "group.0.json" - - let (bob, charlie, dee) = assertThree users - - -- Add bob -> epoch 1 - do - (commit, welcome) <- - liftIO $ - setupCommit tmp creator "group.0.json" "group.1.json" $ - toList (pClients bob) - testSuccessfulCommit MessagingSetup {users = [bob], ..} +propInvalidEpoch = do + users@[alice, bob, charlie, dee] <- createAndConnectUsers (replicate 4 Nothing) + runMLSTest $ do + [alice1, bob1, charlie1, dee1] <- traverse createMLSClient users + void $ setupMLSGroup alice1 - -- try to request a proposal that with too old epoch (0) - do - prop <- liftIO $ bareAddProposal tmp creator charlie "group.0.json" "group.0.json" - err <- - responseJsonError - =<< postMessage (qUnqualified (pUserId creator)) prop - epoch 1 + void $ uploadNewKeyPackage bob1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit - -- try to request a proposal that is too new epoch (2) - do - void $ - liftIO $ - setupCommit tmp creator "group.1.json" "group.2.json" $ - toList (pClients charlie) - prop <- liftIO $ bareAddProposal tmp creator dee "group.2.json" "group.2.json" - err <- - responseJsonError - =<< postMessage (qUnqualified (pUserId creator)) prop - mls {mlsNewMembers = mempty} + + -- alice send a well-formed proposal and commits it + void $ uploadNewKeyPackage dee1 + createAddProposals alice1 [dee] >>= traverse_ sendAndConsumeMessage + void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommit + +-- scenario: +-- alice1 creates a group and adds bob1 +-- bob2 joins with external proposal (alice1 commits it) +-- bob2 adds charlie1 +-- alice1 sends a message testExternalAddProposal :: TestM () -testExternalAddProposal = withSystemTempDirectory "mls" $ \tmp -> do - (creator, [bob]) <- withLastPrekeys $ setupParticipants tmp def [(1, LocalUser)] - (groupId, conversation) <- setupGroup tmp CreateConv creator "group" - - bobClient1 <- assertOne . toList $ pClients bob - (commit, welcome) <- - liftIO $ - setupCommit tmp creator "group" "group" $ - NonEmpty.tail (pClients creator) <> [bobClient1] - testSuccessfulCommit MessagingSetup {users = [bob], ..} - - liftIO $ mergeWelcome tmp (fst bobClient1) "group" "group" "welcome" - - bobClient2Qid <- - userClientQid (pUserId bob) - <$> withLastPrekeys (setupUserClient tmp CreateWithKey True (pUserId bob)) - externalProposal <- liftIO $ createExternalProposal tmp bobClient2Qid "group" "group" - postMessage (qUnqualified (pUserId bob)) externalProposal !!! const 201 === statusCode - -testExternalAddProposalWrongUser :: TestM () -testExternalAddProposalWrongUser = withSystemTempDirectory "mls" $ \tmp -> do - (creator, [bob, charly]) <- withLastPrekeys $ setupParticipants tmp def [(1, LocalUser), (1, LocalUser)] - (groupId, conversation) <- setupGroup tmp CreateConv creator "group" - - bobClient1 <- assertOne . toList $ pClients bob - charlyClient1 <- assertOne . toList $ pClients charly - (commit, welcome) <- - liftIO $ - setupCommit tmp creator "group" "group" $ - NonEmpty.tail (pClients creator) <> [bobClient1, charlyClient1] - testSuccessfulCommit MessagingSetup {users = [bob, charly], ..} +testExternalAddProposal = do + -- create users + [alice, bob, charlie] <- + createAndConnectUsers (replicate 3 Nothing) + + void . runMLSTest $ do + -- create clients + alice1 <- createMLSClient alice + bob1 <- createMLSClient bob + charlie1 <- createMLSClient charlie + + -- upload key packages + void $ uploadNewKeyPackage bob1 + void $ uploadNewKeyPackage charlie1 + + -- create group with alice1 and bob1 + (_, qcnv) <- setupMLSGroup alice1 + void $ + createAddCommit alice1 [bob] + >>= sendAndConsumeCommit + + -- bob joins with an external proposal + bob2 <- createMLSClient bob + mlsBracket [alice1, bob1] $ \wss -> do + void $ + createExternalAddProposal bob2 + >>= sendAndConsumeMessage + liftTest $ + WS.assertMatchN_ (5 # Second) wss $ + void . wsAssertAddProposal bob qcnv - liftIO $ mergeWelcome tmp (fst bobClient1) "group" "group" "welcome" + void $ + createPendingProposalCommit alice1 + >>= sendAndConsumeCommit - bobClient2Qid <- - userClientQid (pUserId bob) - <$> withLastPrekeys (setupUserClient tmp CreateWithKey True (pUserId bob)) - externalProposal <- liftIO $ createExternalProposal tmp bobClient2Qid "group" "group" - postMessage (qUnqualified (pUserId charly)) externalProposal !!! do - const 422 === statusCode - const (Just "mls-unsupported-proposal") === fmap Wai.label . responseJsonError + -- alice sends a message + do + msg <- createApplicationMessage alice1 "hi bob" + mlsBracket [bob1, bob2] $ \wss -> do + void $ sendAndConsumeMessage msg + liftTest $ + WS.assertMatchN_ (5 # Second) wss $ + wsAssertMLSMessage qcnv alice (mpMessage msg) + + -- bob adds charlie + putOtherMemberQualified + (qUnqualified alice) + bob + (OtherMemberUpdate (Just roleNameWireAdmin)) + qcnv + !!! const 200 === statusCode + createAddCommit bob2 [charlie] + >>= sendAndConsumeCommit + +testExternalAddProposalNonAdminCommit :: TestM () +testExternalAddProposalNonAdminCommit = do + -- create users + [alice, bob, charlie] <- + createAndConnectUsers (replicate 3 Nothing) + + void . runMLSTest $ do + -- create clients + alice1 <- createMLSClient alice + [bob1, bob2] <- replicateM 2 (createMLSClient bob) + charlie1 <- createMLSClient charlie + + -- upload key packages + void $ uploadNewKeyPackage bob1 + void $ uploadNewKeyPackage charlie1 + + -- create group with alice1 and bob1 + (_, qcnv) <- setupMLSGroup alice1 + void $ + createAddCommit alice1 [bob] + >>= sendAndConsumeCommit + + -- bob joins with an external proposal + mlsBracket [alice1, bob1] $ \wss -> do + void $ + createExternalAddProposal bob2 + >>= sendAndConsumeMessage + liftTest $ + WS.assertMatchN_ (5 # Second) wss $ + void . wsAssertAddProposal bob qcnv + + -- bob1 commits + void $ + createPendingProposalCommit bob1 + >>= sendAndConsumeCommit +-- scenario: +-- alice adds bob and charlie +-- charlie sends an external proposal for bob testExternalAddProposalWrongClient :: TestM () -testExternalAddProposalWrongClient = withSystemTempDirectory "mls" $ \tmp -> do - (creator, [bob, charly]) <- withLastPrekeys $ setupParticipants tmp def [(1, LocalUser), (1, LocalUser)] - (groupId, conversation) <- setupGroup tmp CreateConv creator "group" +testExternalAddProposalWrongClient = do + [alice, bob, charlie] <- + createAndConnectUsers (replicate 3 Nothing) + + runMLSTest $ do + -- setup clients + [alice1, bob1, bob2, charlie1] <- + traverse + createMLSClient + [alice, bob, bob, charlie] + void $ uploadNewKeyPackage bob1 + void $ uploadNewKeyPackage charlie1 + + void $ setupMLSGroup alice1 + void $ + createAddCommit alice1 [bob, charlie] + >>= sendAndConsumeCommit + + prop <- createExternalAddProposal bob2 + postMessage (qUnqualified charlie) (mpMessage prop) + !!! do + const 422 === statusCode + const (Just "mls-unsupported-proposal") === fmap Wai.label . responseJsonError + +-- scenario: +-- alice adds bob +-- charlie attempts to join with an external add proposal +testExternalAddProposalWrongUser :: TestM () +testExternalAddProposalWrongUser = do + users@[_, bob, charlie] <- createAndConnectUsers (replicate 3 Nothing) - bobClient1 <- assertOne . toList $ pClients bob - charlyClient1 <- assertOne . toList $ pClients charly - (commit, welcome) <- - liftIO $ - setupCommit tmp creator "group" "group" $ - NonEmpty.tail (pClients creator) <> [bobClient1, charlyClient1] - testSuccessfulCommit MessagingSetup {users = [bob, charly], ..} + runMLSTest $ do + -- setup clients + [alice1, bob1, charlie1] <- traverse createMLSClient users + void $ uploadNewKeyPackage bob1 - liftIO $ mergeWelcome tmp (fst bobClient1) "group" "group" "welcome" + void $ setupMLSGroup alice1 + void $ + createAddCommit alice1 [bob] + >>= sendAndConsumeCommit - bobClient2Qid <- - userClientQid (pUserId bob) - <$> withLastPrekeys (setupUserClient tmp CreateWithoutKey True (pUserId bob)) - externalProposal <- liftIO $ createExternalProposal tmp bobClient2Qid "group" "group" - postMessage (qUnqualified (pUserId charly)) externalProposal !!! do - const 422 === statusCode - const (Just "mls-unsupported-proposal") === fmap Wai.label . responseJsonError + prop <- createExternalAddProposal charlie1 + postMessage (qUnqualified charlie) (mpMessage prop) + !!! do + const 404 === statusCode + const (Just "no-conversation") === fmap Wai.label . responseJsonError -- FUTUREWORK: test processing a commit containing the external proposal testPublicKeys :: TestM () @@ -1661,43 +1470,465 @@ testPublicKeys = do -- 2022 only gets forwarded by the backend, i.e., there's no action taken by the -- backend. propUnsupported :: TestM () -propUnsupported = withSystemTempDirectory "mls" $ \tmp -> do - MessagingSetup {..} <- aliceInvitesBobWithTmp tmp (1, LocalUser) def {createConv = CreateConv} - aliceKP <- liftIO $ do - d <- BS.readFile (tmp pClientQid creator) - either (\e -> assertFailure ("could not parse key package: " <> T.unpack e)) pure $ - decodeMLS' d - let alicePublicKey = bcSignatureKey $ kpCredential aliceKP - - -- "\0 " corresponds to 0020 in TLS encoding, which is the length of the - -- following public key - file <- - liftIO . BS.readFile $ - tmp pClientQid creator <> ".db" cs (B64U.encode $ "\0 " <> alicePublicKey) - let s = - file ^.. key "signature_private_key" . key "value" . _Array . traverse . _Integer - & fmap fromIntegral - & BS.pack - let (privKey, pubKey) = BS.splitAt 32 s - liftIO $ alicePublicKey @?= pubKey - let aliceRef = - kpRef - MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 - . KeyPackageData - . rmRaw - . kpTBS - $ aliceKP - let Just appAckMsg = +propUnsupported = do + users@[_alice, bob] <- createAndConnectUsers (replicate 2 Nothing) + runMLSTest $ do + [alice1, bob1] <- traverse createMLSClient users + void $ uploadNewKeyPackage bob1 + (gid, _) <- setupMLSGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit + + mems <- currentGroupFile alice1 >>= liftIO . readGroupState + (_, ref) <- assertJust $ find ((== alice1) . fst) mems + (priv, pub) <- clientKeyPair alice1 + msg <- + assertJust $ maybeCryptoError $ mkAppAckProposalMessage - groupId - (Epoch 0) - aliceRef + gid + (Epoch 1) + ref [] - <$> C.secretKey privKey - <*> C.publicKey pubKey - msgSerialised = - LBS.toStrict . runPut . serialiseMLS $ appAckMsg + <$> Ed25519.secretKey priv + <*> Ed25519.publicKey pub + let msgData = LBS.toStrict (runPut (serialiseMLS msg)) + + -- we cannot use sendAndConsumeMessage here, because openmls does not yet + -- support AppAck proposals + postMessage (ciUser alice1) msgData !!! const 201 === statusCode + +testBackendRemoveProposalLocalConvLocalUser :: TestM () +testBackendRemoveProposalLocalConvLocalUser = do + [alice, bob] <- createAndConnectUsers (replicate 2 Nothing) + runMLSTest $ do + [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] + traverse_ uploadNewKeyPackage [bob1, bob2] + (_, qcnv) <- setupMLSGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit + + bobClients <- getClientsFromGroupState alice1 bob + mlsBracket [alice1] $ \wss -> void $ do + liftTest $ deleteUser (qUnqualified bob) !!! const 200 === statusCode + -- remove bob clients from the test state + State.modify $ \mls -> + mls + { mlsMembers = Set.difference (mlsMembers mls) (Set.fromList [bob1, bob2]) + } + + for bobClients $ \(_, ref) -> do + [msg] <- WS.assertMatchN (5 # Second) wss $ \n -> + wsAssertBackendRemoveProposal bob qcnv ref n + consumeMessage1 alice1 msg + + -- alice commits the external proposals + events <- createPendingProposalCommit alice1 >>= sendAndConsumeCommit + liftIO $ events @?= [] + +testBackendRemoveProposalLocalConvRemoteUser :: TestM () +testBackendRemoveProposalLocalConvRemoteUser = do + [alice, bob] <- createAndConnectUsers [Nothing, Just "bob.example.com"] + runMLSTest $ do + [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] + (_, 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 EmptyResponse) + "mls-welcome" -> pure (Aeson.encode EmptyResponse) + "get-mls-clients" -> + pure + . Aeson.encode + . Set.fromList + . map (flip ClientInfo True . ciClient) + $ [bob1, bob2] + ms -> assertFailure ("unmocked endpoint called: " <> cs ms) + + void . withTempMockFederator' mock $ do + mlsBracket [alice1] $ \[wsA] -> do + void $ sendAndConsumeCommit commit + + bobClients <- getClientsFromGroupState alice1 bob + fedGalleyClient <- view tsFedGalleyClient + void $ + runFedClient + @"on-user-deleted-conversations" + fedGalleyClient + (qDomain bob) + ( UserDeletedConversationsNotification + { udcvUser = qUnqualified bob, + udcvConversations = unsafeRange [qUnqualified qcnv] + } + ) + + for_ bobClients $ \(_, ref) -> + WS.assertMatch (5 # WS.Second) wsA $ + wsAssertBackendRemoveProposal bob qcnv ref + +sendRemoteMLSWelcome :: TestM () +sendRemoteMLSWelcome = do + -- Alice is from the originating domain and Bob is local, i.e., on the receiving domain + [alice, bob] <- createAndConnectUsers [Just "alice.example.com", Nothing] + commit <- runMLSTest $ do + [alice1, bob1] <- traverse createMLSClient [alice, bob] + void $ setupFakeMLSGroup alice1 + void $ uploadNewKeyPackage bob1 + createAddCommit alice1 [bob] + + welcome <- assertJust (mpWelcome commit) + + fedGalleyClient <- view tsFedGalleyClient + cannon <- view tsCannon + + WS.bracketR cannon (qUnqualified bob) $ \wsB -> do + -- send welcome message + void $ + runFedClient @"mls-welcome" fedGalleyClient (qDomain alice) $ + MLSWelcomeRequest + (Base64ByteString welcome) + + -- check that the corresponding event is received + liftIO $ do + WS.assertMatch_ (5 # WS.Second) wsB $ + wsAssertMLSWelcome bob welcome + +sendRemoteMLSWelcomeKPNotFound :: TestM () +sendRemoteMLSWelcomeKPNotFound = do + [alice, bob] <- createAndConnectUsers [Just "alice.example.com", Nothing] + commit <- runMLSTest $ do + [alice1, bob1] <- traverse createMLSClient [alice, bob] + void $ setupFakeMLSGroup alice1 + kp <- generateKeyPackage bob1 >>= keyPackageFile bob1 . snd + createAddCommitWithKeyPackages alice1 [(bob1, kp)] + welcome <- assertJust (mpWelcome commit) + + fedGalleyClient <- view tsFedGalleyClient + cannon <- view tsCannon + WS.bracketR cannon (qUnqualified bob) $ \wsB -> do + -- send welcome message + void $ + runFedClient @"mls-welcome" fedGalleyClient (qDomain alice) $ + MLSWelcomeRequest + (Base64ByteString welcome) + + liftIO $ do + -- check that no event is received + WS.assertNoEvent (1 # Second) [wsB] + +testBackendRemoveProposalLocalConvLocalLeaverCreator :: TestM () +testBackendRemoveProposalLocalConvLocalLeaverCreator = do + [alice, bob] <- createAndConnectUsers (replicate 2 Nothing) + + runMLSTest $ do + [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] + traverse_ uploadNewKeyPackage [bob1, bob2] + (_, qcnv) <- setupMLSGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit + + aliceClients <- getClientsFromGroupState alice1 alice + mlsBracket [alice1, bob1, bob2] $ \wss -> void $ do + liftTest $ + deleteMemberQualified (qUnqualified alice) alice qcnv + !!! const 200 === statusCode + -- remove alice's client from the test state + State.modify $ \mls -> + mls + { mlsMembers = Set.difference (mlsMembers mls) (Set.fromList [alice1]) + } + + for_ aliceClients $ \(_, ref) -> do + -- only bob's clients should receive the external proposals + msgs <- WS.assertMatchN (5 # Second) (drop 1 wss) $ \n -> + wsAssertBackendRemoveProposal alice qcnv ref n + traverse_ (uncurry consumeMessage1) (zip [bob1, bob2] msgs) + + -- but everyone should receive leave events + WS.assertMatchN_ (5 # WS.Second) wss $ + wsAssertMembersLeave qcnv alice [alice] + + -- check that no more events are sent, so in particular alice does not + -- receive any MLS messages + WS.assertNoEvent (1 # WS.Second) wss + + -- bob commits the external proposals + events <- createPendingProposalCommit bob1 >>= sendAndConsumeCommit + liftIO $ events @?= [] + +testBackendRemoveProposalLocalConvLocalLeaverCommitter :: TestM () +testBackendRemoveProposalLocalConvLocalLeaverCommitter = do + [alice, bob, charlie] <- createAndConnectUsers (replicate 3 Nothing) + + runMLSTest $ do + [alice1, bob1, bob2, charlie1] <- traverse createMLSClient [alice, bob, bob, charlie] + traverse_ uploadNewKeyPackage [bob1, bob2, charlie1] + (_, qcnv) <- setupMLSGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommit + + -- promote bob + putOtherMemberQualified (ciUser alice1) bob (OtherMemberUpdate (Just roleNameWireAdmin)) qcnv + !!! const 200 === statusCode + + void $ createAddCommit bob1 [charlie] >>= sendAndConsumeCommit + + bobClients <- getClientsFromGroupState alice1 bob + mlsBracket [alice1, charlie1, bob1, bob2] $ \wss -> void $ do + liftTest $ + deleteMemberQualified (qUnqualified bob) bob qcnv + !!! const 200 === statusCode + -- remove bob clients from the test state + State.modify $ \mls -> + mls + { mlsMembers = Set.difference (mlsMembers mls) (Set.fromList [bob1, bob2]) + } + + for_ bobClients $ \(_, ref) -> do + -- only alice and charlie should receive the external proposals + msgs <- WS.assertMatchN (5 # Second) (take 2 wss) $ \n -> + wsAssertBackendRemoveProposal bob qcnv ref n + traverse_ (uncurry consumeMessage1) (zip [alice1, charlie1] msgs) + + -- but everyone should receive leave events + WS.assertMatchN_ (5 # WS.Second) wss $ + wsAssertMembersLeave qcnv bob [bob] + + -- check that no more events are sent, so in particular bob does not + -- receive any MLS messages + WS.assertNoEvent (1 # WS.Second) wss + + -- alice commits the external proposals + events <- createPendingProposalCommit alice1 >>= sendAndConsumeCommit + liftIO $ events @?= [] + +testBackendRemoveProposalLocalConvRemoteLeaver :: TestM () +testBackendRemoveProposalLocalConvRemoteLeaver = do + [alice, bob] <- createAndConnectUsers [Nothing, Just "bob.example.com"] + + runMLSTest $ do + [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] + (_, 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 EmptyResponse) + "mls-welcome" -> pure (Aeson.encode EmptyResponse) + "get-mls-clients" -> + pure + . Aeson.encode + . Set.fromList + . map (flip ClientInfo True . ciClient) + $ [bob1, bob2] + ms -> assertFailure ("unmocked endpoint called: " <> cs ms) + + bobClients <- getClientsFromGroupState alice1 bob + void . withTempMockFederator' mock $ do + mlsBracket [alice1] $ \[wsA] -> void $ do + void $ sendAndConsumeCommit commit + fedGalleyClient <- view tsFedGalleyClient + void $ + runFedClient + @"update-conversation" + fedGalleyClient + (qDomain bob) + ConversationUpdateRequest + { curUser = qUnqualified bob, + curConvId = qUnqualified qcnv, + curAction = SomeConversationAction SConversationLeaveTag () + } + + for_ bobClients $ \(_, ref) -> + WS.assertMatch_ (5 # WS.Second) wsA $ + wsAssertBackendRemoveProposal bob qcnv ref + +testBackendRemoveProposalLocalConvLocalClient :: TestM () +testBackendRemoveProposalLocalConvLocalClient = do + [alice, bob, charlie] <- createAndConnectUsers (replicate 3 Nothing) + + runMLSTest $ do + [alice1, bob1, bob2, charlie1] <- traverse createMLSClient [alice, bob, bob, charlie] + traverse_ uploadNewKeyPackage [bob1, bob2, charlie1] + (_, qcnv) <- setupMLSGroup alice1 + void $ createAddCommit alice1 [bob, charlie] >>= sendAndConsumeCommit + Just (_, kpBob1) <- find (\(ci, _) -> ci == bob1) <$> getClientsFromGroupState alice1 bob + + mlsBracket [alice1, bob1] $ \[wsA, wsB] -> do + liftTest $ + deleteClient (ciUser bob1) (ciClient bob1) (Just defPassword) + !!! statusCode === const 200 + + State.modify $ \mls -> + mls + { mlsMembers = Set.difference (mlsMembers mls) (Set.fromList [bob1]) + } + + WS.assertMatch_ (5 # WS.Second) wsB $ + wsAssertClientRemoved (ciClient bob1) + + msg <- WS.assertMatch (5 # WS.Second) wsA $ \notification -> do + wsAssertBackendRemoveProposal bob qcnv kpBob1 notification + + for_ [alice1, bob2, charlie1] $ + flip consumeMessage1 msg + + mp <- createPendingProposalCommit charlie1 + events <- sendAndConsumeCommit mp + liftIO $ events @?= [] + +testBackendRemoveProposalLocalConvRemoteClient :: TestM () +testBackendRemoveProposalLocalConvRemoteClient = do + [alice, bob] <- createAndConnectUsers [Nothing, Just "faraway.example.com"] + + runMLSTest $ do + [alice1, bob1] <- traverse createMLSClient [alice, bob] + (_, 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 EmptyResponse) + "on-mls-message-sent" -> pure (Aeson.encode EmptyResponse) + "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 + + void . withTempMockFederator' mock $ do + mlsBracket [alice1] $ \[wsA] -> void $ do + void $ sendAndConsumeCommit commit + + fedGalleyClient <- view tsFedGalleyClient + void $ + runFedClient + @"on-client-removed" + fedGalleyClient + (ciDomain bob1) + (ClientRemovedRequest (ciUser bob1) (ciClient bob1) [qUnqualified qcnv]) + + WS.assertMatch_ (5 # WS.Second) wsA $ + \notification -> + void $ wsAssertBackendRemoveProposal bob qcnv bob1KP notification + +testGetGroupInfoOfLocalConv :: TestM () +testGetGroupInfoOfLocalConv = do + [alice, bob] <- createAndConnectUsers [Nothing, Nothing] + + runMLSTest $ do + [alice1, bob1] <- traverse createMLSClient [alice, bob] + traverse_ uploadNewKeyPackage [bob1] + (_, qcnv) <- setupMLSGroup alice1 + commit <- createAddCommit alice1 [bob] + + void $ sendAndConsumeCommitBundle commit + + -- check the group info matches + gs <- assertJust (mpPublicGroupState commit) + returnedGS <- + fmap responseBody $ + getGroupInfo (qUnqualified alice) qcnv + returnedGS + +testGetGroupInfoOfRemoteConv :: TestM () +testGetGroupInfoOfRemoteConv = do + let aliceDomain = Domain "faraway.example.com" + [alice, bob, charlie] <- createAndConnectUsers [Just (domainText aliceDomain), Nothing, Nothing] + + runMLSTest $ do + [alice1, bob1] <- traverse createMLSClient [alice, bob] + + void $ uploadNewKeyPackage bob1 + (groupId, qcnv) <- setupFakeMLSGroup alice1 + mp <- createAddCommit alice1 [bob] + traverse_ consumeWelcome (mpWelcome mp) + + receiveNewRemoteConv qcnv groupId + 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) + + (_, reqs) <- withTempMockFederator' mock $ do + res <- + fmap responseBody $ + getGroupInfo (qUnqualified bob) qcnv + pure (Aeson.encode EmptyResponse) + "on-conversation-updated" -> pure (Aeson.encode ()) + "mls-welcome" -> pure (Aeson.encode EmptyResponse) + "get-mls-clients" -> + pure + . Aeson.encode + . Set.fromList + . map (flip ClientInfo True) + $ [ciClient bob1] + ms -> assertFailure ("unmocked endpoint called: " <> cs ms) - postMessage (qUnqualified . pUserId $ creator) msgSerialised - !!! const 201 === statusCode + void . withTempMockFederator' mock $ do + void $ sendAndConsumeCommitBundle commit + + fedGalleyClient <- view tsFedGalleyClient + do + resp <- + runFedClient + @"query-group-info" + fedGalleyClient + (ciDomain bob1) + (GetGroupInfoRequest (qUnqualified qcnv) (qUnqualified bob)) + + liftIO $ case resp of + GetGroupInfoResponseError err -> assertFailure ("Unexpected error: " <> show err) + GetGroupInfoResponseState gs -> + fromBase64ByteString gs @=? groupState + + do + resp <- + runFedClient + @"query-group-info" + fedGalleyClient + (ciDomain bob1) + (GetGroupInfoRequest (qUnqualified qcnv) (qUnqualified charlie)) + + liftIO $ case resp of + GetGroupInfoResponseError err -> + err @?= ConvNotFound + GetGroupInfoResponseState _ -> + assertFailure "Unexpected success" diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index c4cca7bf2c..e71e592a6a 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE RecordWildCards #-} -- This file is part of the Wire Server implementation. @@ -22,513 +23,73 @@ module API.MLS.Util where import API.Util import Bilge import Bilge.Assert -import Control.Lens (preview, to, view) +import Control.Arrow ((&&&)) +import Control.Error.Util +import Control.Lens (preview, to, view, (^..)) import Control.Monad.Catch +import Control.Monad.State (StateT, evalStateT) import qualified Control.Monad.State as State +import Control.Monad.Trans.Maybe import Crypto.PubKey.Ed25519 +import Data.Aeson.Lens import qualified Data.ByteArray as BA import qualified Data.ByteString as BS +import qualified Data.ByteString.Base64.URL as B64U import Data.ByteString.Conversion -import Data.Default import Data.Domain +import Data.Hex import Data.Id -import Data.Json.Util -import Data.List.NonEmpty (NonEmpty (..)) -import qualified Data.List.NonEmpty as NonEmpty +import Data.Json.Util hiding ((#)) import qualified Data.Map as Map import Data.Qualified import qualified Data.Set as Set import qualified Data.Text as T +import qualified Data.Text.Encoding as T +import Data.Time.Clock (getCurrentTime) +import Galley.Keys +import Galley.Options import Imports +import System.Directory (getSymbolicLinkTarget) import System.FilePath import System.IO.Temp +import System.Posix hiding (createDirectory) import System.Process import Test.QuickCheck (arbitrary, generate) +import qualified Test.Tasty.Cannon as WS import Test.Tasty.HUnit import TestHelpers import TestSetup import Wire.API.Conversation +import Wire.API.Conversation.Action import Wire.API.Conversation.Protocol +import Wire.API.Conversation.Role (roleNameWireMember) import Wire.API.Event.Conversation +import Wire.API.Federation.API.Galley +import Wire.API.MLS.CipherSuite +import Wire.API.MLS.CommitBundle import Wire.API.MLS.Credential +import Wire.API.MLS.GroupInfoBundle import Wire.API.MLS.KeyPackage +import Wire.API.MLS.Keys import Wire.API.MLS.Message import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation import Wire.API.User.Client import Wire.API.User.Client.Prekey -data CreateClients = CreateWithoutKey | CreateWithKey | DontCreateClients - deriving (Eq) - -data CreateConv = CreateConv | CreateProteusConv | DontCreateConv - deriving (Eq) - -data UserOrigin = LocalUser | RemoteUser Domain - -createNewConv :: ClientId -> CreateConv -> Maybe NewConv -createNewConv c CreateConv = Just (defNewMLSConv c) -createNewConv _ CreateProteusConv = Just defNewProteusConv -createNewConv _ DontCreateConv = Nothing - -data SetupOptions = SetupOptions - { createClients :: CreateClients, - creatorOrigin :: UserOrigin, - createConv :: CreateConv, - makeConnections :: Bool, - numCreatorClients :: Int - } - -instance Default SetupOptions where - def = - SetupOptions - { createClients = CreateWithKey, - creatorOrigin = LocalUser, - createConv = DontCreateConv, - makeConnections = True, - numCreatorClients = 1 - } - -data MessagingSetup = MessagingSetup - { creator :: Participant, - users :: [Participant], - conversation :: Qualified ConvId, - groupId :: GroupId, - welcome :: ByteString, - commit :: ByteString - } - deriving (Show) - -data AddKeyPackage = AddKeyPackage - { mapKeyPackage :: Bool, - setPublicKey :: Bool - } - deriving (Show) - -instance Default AddKeyPackage where - def = - AddKeyPackage - { mapKeyPackage = True, - setPublicKey = True - } - -cli :: String -> FilePath -> [String] -> CreateProcess -cli store tmp args = - proc "mls-test-cli" $ - ["--store", tmp (store <> ".db")] <> args - -data Participant = Participant - { pUserId :: Qualified UserId, - pClientIds :: NonEmpty ClientId - } - deriving (Show) - -userClientQid :: Qualified UserId -> ClientId -> String -userClientQid usr c = - show (qUnqualified usr) +cid2Str :: ClientIdentity -> String +cid2Str cid = + show (ciUser cid) <> ":" - <> T.unpack (client c) + <> T.unpack (client . ciClient $ cid) <> "@" - <> T.unpack (domainText (qDomain usr)) - -pClients :: Participant -> NonEmpty (String, ClientId) -pClients p = - pClientIds p <&> \c -> - (userClientQid (pUserId p) c, c) - -pClientQid :: Participant -> String -pClientQid p = userClientQid (pUserId p) (NonEmpty.head (pClientIds p)) - -pClientId :: Participant -> ClientId -pClientId = NonEmpty.head . pClientIds - -setupUserClient :: - HasCallStack => - FilePath -> - CreateClients -> - -- | Whether to claim/map the key package - Bool -> - Qualified UserId -> - State.StateT [LastPrekey] TestM ClientId -setupUserClient tmp doCreateClients mapKeyPackage usr = do - localDomain <- lift viewFederationDomain - lpk <- takeLastPrekey - lift $ do - -- create client if requested - c <- case doCreateClients of - DontCreateClients -> liftIO $ generate arbitrary - _ -> randomClient (qUnqualified usr) lpk - - let qcid = userClientQid usr c - - -- generate key package - void . liftIO $ spawn (cli qcid tmp ["init", qcid]) Nothing - kp <- - liftIO $ - decodeMLSError - =<< spawn (cli qcid tmp ["key-package", "create"]) Nothing - liftIO $ BS.writeFile (tmp qcid) (rmRaw kp) - - -- Set Bob's private key and upload key package if required. If a client - -- does not have to be created and it is remote, pretend to have claimed its - -- key package. - case doCreateClients of - CreateWithKey -> addKeyPackage def {mapKeyPackage = mapKeyPackage} usr c kp - DontCreateClients | localDomain /= qDomain usr -> do - brig <- view tsBrig - let bundle = - KeyPackageBundle $ - Set.singleton $ - KeyPackageBundleEntry - { kpbeUser = usr, - kpbeClient = c, - kpbeRef = fromJust $ kpRef' kp, - kpbeKeyPackage = KeyPackageData $ rmRaw kp - } - when mapKeyPackage $ mapRemoteKeyPackageRef brig bundle - DontCreateClients -> pure () - CreateWithoutKey -> pure () - - pure c - -setupParticipant :: - HasCallStack => - FilePath -> - CreateClients -> - Int -> - Qualified UserId -> - State.StateT [LastPrekey] TestM Participant -setupParticipant tmp doCreateClients numClients usr = - Participant usr . NonEmpty.fromList - <$> replicateM numClients (setupUserClient tmp doCreateClients True usr) + <> T.unpack (domainText (ciDomain cid)) -setupParticipants :: - HasCallStack => - FilePath -> - SetupOptions -> - -- | A list of pairs, where each pair represents the number of clients for a - -- participant other than the group creator and whether the participant is - -- local or remote. - [(Int, UserOrigin)] -> - State.StateT [LastPrekey] TestM (Participant, [Participant]) -setupParticipants tmp SetupOptions {..} ns = do - creator <- do - u <- lift $ createUserOrId creatorOrigin - let createCreatorClients = createClientsForUR creatorOrigin createClients - c0 <- setupUserClient tmp createCreatorClients False u - cs <- replicateM (numCreatorClients - 1) (setupUserClient tmp createCreatorClients True u) - pure (Participant u (c0 :| cs)) - others <- for ns $ \(n, ur) -> do - qusr <- lift (createUserOrId ur) - participant <- setupParticipant tmp (createClientsForUR ur createClients) n qusr - pure (participant, ur) - lift . when makeConnections $ do - for_ others $ \(o, ur) -> case (creatorOrigin, ur) of - (LocalUser, LocalUser) -> - connectUsers (qUnqualified (pUserId creator)) (pure ((qUnqualified . pUserId) o)) - (LocalUser, RemoteUser _) -> - connectWithRemoteUser - (qUnqualified . pUserId $ creator) - (pUserId o) - (RemoteUser _, LocalUser) -> - connectWithRemoteUser - (qUnqualified . pUserId $ o) - (pUserId creator) - (RemoteUser _, RemoteUser _) -> - liftIO $ - assertFailure "Trying to have both the creator and a group participant remote" - pure (creator, fst <$> others) - where - createUserOrId :: UserOrigin -> TestM (Qualified UserId) - createUserOrId = \case - LocalUser -> randomQualifiedUser - RemoteUser d -> randomQualifiedId d - - createClientsForUR LocalUser cc = cc - createClientsForUR (RemoteUser _) _ = DontCreateClients - -withLastPrekeys :: Monad m => State.StateT [LastPrekey] m a -> m a -withLastPrekeys m = State.evalStateT m someLastPrekeys - -setupGroup :: - HasCallStack => - FilePath -> - CreateConv -> - Participant -> - String -> - TestM (GroupId, Qualified ConvId) -setupGroup tmp createConv creator name = do - (mGroupId, conversation) <- case createNewConv (pClientId creator) createConv of - Nothing -> pure (Nothing, error "No conversation created") - Just nc -> do - conv <- - responseJsonError =<< postConvQualified (qUnqualified (pUserId creator)) nc - pure gid - -- generate a random group id - Nothing -> liftIO $ fmap (GroupId . BS.pack) (replicateM 32 (generate arbitrary)) - - groupJSON <- - liftIO $ - spawn - ( cli - (pClientQid creator) - tmp - ["group", "create", T.unpack (toBase64Text (unGroupId groupId))] - ) - Nothing - liftIO $ BS.writeFile (tmp name) groupJSON - - pure (groupId, conversation) - -setupCommit :: - (HasCallStack, Foldable f) => - String -> - Participant -> - String -> - String -> - f (String, ClientId) -> - IO (ByteString, ByteString) -setupCommit tmp admin groupName newGroupName clients = - (,) - <$> spawn - ( cli - (pClientQid admin) - tmp - $ [ "member", - "add", - "--group", - tmp groupName, - "--welcome-out", - tmp "welcome", - "--group-out", - tmp newGroupName - ] - <> map ((tmp ) . fst) (toList clients) - ) - Nothing - <*> BS.readFile (tmp "welcome") - -setupRemoveCommit :: - (HasCallStack, Foldable f) => - String -> - Participant -> - String -> - String -> - f (String, ClientId) -> - IO (ByteString, Maybe ByteString) -setupRemoveCommit tmp admin groupName newGroupName clients = do - let welcomeFile = tmp "welcome" - commit <- - spawn - ( cli - (pClientQid admin) - tmp - $ [ "member", - "remove", - "--group", - tmp groupName, - "--group-out", - tmp newGroupName, - "--welcome-out", - welcomeFile - ] - <> map ((tmp ) . fst) (toList clients) - ) - Nothing - welcome <- - doesFileExist welcomeFile >>= \case - False -> pure Nothing - True -> Just <$> BS.readFile welcomeFile - pure (commit, welcome) - -mergeWelcome :: - (HasCallStack) => - String -> - String -> - String -> - String -> - String -> - IO () -mergeWelcome tmp clientQid groupIn groupOut welcomeIn = - void $ - spawn - ( cli - clientQid - tmp - [ groupIn, - "from-welcome", - "--group-out", - tmp groupOut, - tmp welcomeIn - ] - ) - Nothing - -bareAddProposal :: - HasCallStack => - String -> - Participant -> - Participant -> - String -> - String -> - IO ByteString -bareAddProposal tmp creator participantToAdd groupIn groupOut = - spawn - ( cli - (pClientQid creator) - tmp - $ [ "proposal", - "--group-in", - tmp groupIn, - "--group-out", - tmp groupOut, - "add", - tmp pClientQid participantToAdd - ] - ) - Nothing - -pendingProposalsCommit :: - HasCallStack => - String -> - Participant -> - String -> - IO (ByteString, Maybe ByteString) -pendingProposalsCommit tmp creator groupName = do - let welcomeFile = tmp "welcome" - commit <- - spawn - ( cli - (pClientQid creator) - tmp - $ [ "commit", - "--group", - tmp groupName, - "--welcome-out", - welcomeFile - ] - ) - Nothing - welcome <- - doesFileExist welcomeFile >>= \case - False -> pure Nothing - True -> Just <$> BS.readFile welcomeFile - pure (commit, welcome) - -createExternalProposal :: - HasCallStack => - String -> - String -> - String -> - String -> - IO ByteString -createExternalProposal tmp creatorClientQid groupIn groupOut = do - spawn - ( cli - creatorClientQid - tmp - $ [ "external-proposal", - "--group-in", - tmp groupIn, - "--group-out", - tmp groupOut, - "add" - ] - ) - Nothing - -createMessage :: - HasCallStack => - String -> - Participant -> - String -> - String -> - IO ByteString -createMessage tmp sender groupName msgText = - spawn (cli (pClientQid sender) tmp ["message", "--group", tmp groupName, msgText]) Nothing - -takeLastPrekey :: MonadFail m => State.StateT [LastPrekey] m LastPrekey -takeLastPrekey = do - (lpk : lpks) <- State.get - State.put lpks - pure lpk - --- | Setup: Alice creates a group and invites Bob that is local or remote to --- Alice depending on the passed in creator origin. Return welcome and commit --- message. -aliceInvitesBob :: HasCallStack => (Int, UserOrigin) -> SetupOptions -> TestM MessagingSetup -aliceInvitesBob bobConf opts = withSystemTempDirectory "mls" $ \tmp -> - aliceInvitesBobWithTmp tmp bobConf opts - -aliceInvitesBobWithTmp :: - HasCallStack => - FilePath -> - (Int, UserOrigin) -> - SetupOptions -> - TestM MessagingSetup -aliceInvitesBobWithTmp tmp bobConf opts@SetupOptions {..} = do - (alice, [bob]) <- withLastPrekeys $ setupParticipants tmp opts [bobConf] - -- create a group - (groupId, conversation) <- setupGroup tmp createConv alice "group" - - -- add clients to it and get welcome message - (commit, welcome) <- - liftIO $ - setupCommit tmp alice "group" "group" $ - NonEmpty.tail (pClients alice) <> toList (pClients bob) - - pure $ - MessagingSetup - { creator = alice, - users = [bob], - .. - } - -addKeyPackage :: - HasCallStack => - AddKeyPackage -> - Qualified UserId -> - ClientId -> - RawMLS KeyPackage -> - TestM () -addKeyPackage AddKeyPackage {..} u c kp = do - brig <- view tsBrig - - when setPublicKey $ do - -- set public key - let update = defUpdateClient {updateClientMLSPublicKeys = Map.singleton Ed25519 (bcSignatureKey (kpCredential (rmValue kp)))} - put - ( brig - . paths ["clients", toByteString' c] - . zUser (qUnqualified u) - . json update - ) - !!! const 200 === statusCode - - -- upload key package - post - ( brig - . paths ["mls", "key-packages", "self", toByteString' c] - . zUser (qUnqualified u) - . json (KeyPackageUpload [kp]) - ) - !!! const 201 === statusCode - - when mapKeyPackage $ - -- claim key package (technically, some other user should claim them, but it doesn't really make a difference) - post - ( brig - . paths ["mls", "key-packages", "claim", toByteString' (qDomain u), toByteString' (qUnqualified u)] - . zUser (qUnqualified u) - ) - !!! const 200 === statusCode - -mapRemoteKeyPackageRef :: (MonadIO m, MonadHttp m, MonadCatch m) => (Request -> Request) -> KeyPackageBundle -> m () +mapRemoteKeyPackageRef :: + (MonadIO m, MonadHttp m, MonadCatch m) => + (Request -> Request) -> + KeyPackageBundle -> + m () mapRemoteKeyPackageRef brig bundle = void $ put @@ -538,20 +99,6 @@ mapRemoteKeyPackageRef brig bundle = ) !!! const 204 === statusCode -claimKeyPackage :: (MonadIO m, MonadHttp m) => (Request -> Request) -> UserId -> Qualified UserId -> m ResponseLBS -claimKeyPackage brig claimant target = - post - ( brig - . paths ["mls", "key-packages", "claim", toByteString' (qDomain target), toByteString' (qUnqualified target)] - . zUser claimant - ) - -postCommit :: HasCallStack => MessagingSetup -> TestM [Event] -postCommit MessagingSetup {..} = - fmap mmssEvents . responseJsonError - =<< postMessage (qUnqualified (pUserId creator)) commit - + UserId -> + ByteString -> + m ResponseLBS +postCommitBundle sender bundle = do + galley <- viewGalley + post + ( galley . paths ["mls", "commit-bundles"] + . zUser sender + . zConn "conn" + . content "message/mls" + . bytes bundle + ) + postWelcome :: (MonadIO m, MonadHttp m, HasGalley m, HasCallStack) => UserId -> ByteString -> m ResponseLBS postWelcome uid welcome = do galley <- viewGalley @@ -605,4 +173,781 @@ mkAppAckProposalMessage gid epoch ref mrs priv pub = do tbsMsgPayload = ProposalMessage (mkAppAckProposal mrs) } sig = BA.convert $ sign priv pub (rmRaw tbs) - in (Message tbs (MessageExtraFields sig Nothing Nothing)) + in Message tbs (MessageExtraFields sig Nothing Nothing) + +saveRemovalKey :: FilePath -> TestM () +saveRemovalKey fp = do + keys <- fromJust <$> view (tsGConf . optSettings . setMlsPrivateKeyPaths) + keysByPurpose <- liftIO $ loadAllMLSKeys keys + let (_, pub) = fromJust (mlsKeyPair_ed25519 (keysByPurpose RemovalPurpose)) + liftIO $ BS.writeFile fp (BA.convert pub) + +data MLSState = MLSState + { mlsBaseDir :: FilePath, + -- | for creating clients + mlsUnusedPrekeys :: [LastPrekey], + mlsMembers :: Set ClientIdentity, + -- | users expected to receive a welcome message after the next commit + mlsNewMembers :: Set ClientIdentity, + mlsGroupId :: Maybe GroupId, + mlsEpoch :: Word64 + } + +newtype MLSTest a = MLSTest {unMLSTest :: StateT MLSState TestM a} + deriving newtype + ( Functor, + Applicative, + Monad, + MonadThrow, + MonadHttp, + MonadIO, + MonadCatch, + MonadFail, + MonadMask, + State.MonadState MLSState, + MonadReader TestSetup + ) + +instance HasGalley MLSTest where + viewGalley = MLSTest $ lift viewGalley + viewGalleyOpts = MLSTest $ lift viewGalleyOpts + +instance HasBrig MLSTest where + viewBrig = MLSTest $ lift viewBrig + +instance HasSettingsOverrides MLSTest where + withSettingsOverrides f (MLSTest action) = MLSTest $ + State.StateT $ \s -> + withSettingsOverrides f (State.runStateT action s) + +liftTest :: TestM a -> MLSTest a +liftTest = MLSTest . lift + +runMLSTest :: MLSTest a -> TestM a +runMLSTest (MLSTest m) = + withSystemTempDirectory "mls" $ \tmp -> do + saveRemovalKey (tmp "removal.key") + evalStateT + m + MLSState + { mlsBaseDir = tmp, + mlsUnusedPrekeys = someLastPrekeys, + mlsMembers = mempty, + mlsNewMembers = mempty, + mlsGroupId = Nothing, + mlsEpoch = 0 + } + +data MessagePackage = MessagePackage + { mpSender :: ClientIdentity, + mpMessage :: ByteString, + mpWelcome :: Maybe ByteString, + mpPublicGroupState :: Maybe ByteString + } + +takeLastPrekeyNG :: HasCallStack => MLSTest LastPrekey +takeLastPrekeyNG = do + s <- State.get + case mlsUnusedPrekeys s of + (pk : pks) -> do + State.modify (\s' -> s' {mlsUnusedPrekeys = pks}) + pure pk + [] -> error "no prekeys left" + +mlscli :: HasCallStack => ClientIdentity -> [String] -> Maybe ByteString -> MLSTest ByteString +mlscli qcid args mbstdin = do + bd <- State.gets mlsBaseDir + let cdir = bd cid2Str qcid + liftIO $ spawn (proc "mls-test-cli" (["--store", cdir "store"] <> args)) mbstdin + +createWireClient :: HasCallStack => Qualified UserId -> MLSTest ClientIdentity +createWireClient qusr = do + lpk <- takeLastPrekeyNG + clientId <- liftTest $ randomClient (qUnqualified qusr) lpk + pure $ mkClientIdentity qusr clientId + +initMLSClient :: HasCallStack => ClientIdentity -> MLSTest () +initMLSClient cid = do + bd <- State.gets mlsBaseDir + createDirectory $ bd cid2Str cid + void $ mlscli cid ["init", cid2Str cid] Nothing + +createLocalMLSClient :: Local UserId -> MLSTest ClientIdentity +createLocalMLSClient (qUntagged -> qusr) = do + qcid <- createWireClient qusr + initMLSClient qcid + + -- set public key + pkey <- mlscli qcid ["public-key"] Nothing + brig <- viewBrig + let update = defUpdateClient {updateClientMLSPublicKeys = Map.singleton Ed25519 pkey} + put + ( brig + . paths ["clients", toByteString' . ciClient $ qcid] + . zUser (ciUser qcid) + . json update + ) + !!! const 200 === statusCode + pure qcid + +-- | Create new mls client and register with backend. If the user is remote, +-- this only creates a fake client (see 'createFakeMLSClient'). +createMLSClient :: HasCallStack => Qualified UserId -> MLSTest ClientIdentity +createMLSClient qusr = do + loc <- liftTest $ qualifyLocal () + foldQualified loc createLocalMLSClient (createFakeMLSClient . qUntagged) qusr + +-- | Like 'createMLSClient', but do not actually register client with backend. +createFakeMLSClient :: HasCallStack => Qualified UserId -> MLSTest ClientIdentity +createFakeMLSClient qusr = do + c <- liftIO $ generate arbitrary + let cid = mkClientIdentity qusr c + initMLSClient cid + pure cid + +-- | create and upload to backend +uploadNewKeyPackage :: HasCallStack => ClientIdentity -> MLSTest KeyPackageRef +uploadNewKeyPackage qcid = do + (kp, _) <- generateKeyPackage qcid + + -- upload key package + brig <- viewBrig + post + ( brig + . paths ["mls", "key-packages", "self", toByteString' . ciClient $ qcid] + . zUser (ciUser qcid) + . json (KeyPackageUpload [kp]) + ) + !!! const 201 === statusCode + pure $ fromJust (kpRef' kp) + +generateKeyPackage :: HasCallStack => ClientIdentity -> MLSTest (RawMLS KeyPackage, KeyPackageRef) +generateKeyPackage qcid = do + kp <- liftIO . decodeMLSError =<< mlscli qcid ["key-package", "create"] Nothing + let ref = fromJust (kpRef' kp) + fp <- keyPackageFile qcid ref + liftIO $ BS.writeFile fp (rmRaw kp) + pure (kp, ref) + +groupFileLink :: HasCallStack => ClientIdentity -> MLSTest FilePath +groupFileLink qcid = State.gets $ \mls -> + mlsBaseDir mls cid2Str qcid "group.latest" + +currentGroupFile :: HasCallStack => ClientIdentity -> MLSTest FilePath +currentGroupFile = liftIO . getSymbolicLinkTarget <=< groupFileLink + +parseGroupFileName :: FilePath -> IO (FilePath, Int) +parseGroupFileName fp = do + let base = takeFileName fp + (prefix, version) <- case break (== '.') base of + (p, '.' : v) -> pure (p, v) + _ -> assertFailure "invalid group file name" + n <- case reads version of + [(v, "")] -> pure (v :: Int) + _ -> assertFailure "could not parse group file version" + pure $ (prefix, n) + +-- sets symlink and creates empty file +nextGroupFile :: HasCallStack => ClientIdentity -> MLSTest FilePath +nextGroupFile qcid = do + bd <- State.gets mlsBaseDir + link <- groupFileLink qcid + exists <- doesFileExist link + base' <- + liftIO $ + if exists + then -- group file exists, bump version and update link + do + (prefix, n) <- parseGroupFileName =<< getSymbolicLinkTarget link + removeFile link + pure $ prefix <> "." <> show (n + 1) + else -- group file does not exist yet, point link to version 0 + pure "group.0" + + let groupFile = bd cid2Str qcid base' + createFileLink groupFile link + pure groupFile + +rollBackClient :: HasCallStack => ClientIdentity -> MLSTest ByteString +rollBackClient cid = do + link <- groupFileLink cid + groupFile <- liftIO $ getSymbolicLinkTarget link + (prefix, n) <- + liftIO $ parseGroupFileName groupFile + when (n == 0) $ do + liftIO . assertFailure $ "Cannot roll back client " <> cid2Str cid + state <- liftIO $ BS.readFile groupFile + removeFile groupFile + removeFile link + bd <- State.gets mlsBaseDir + let newGroupFile = bd cid2Str cid (prefix <> "." <> show (n - 1)) + createFileLink newGroupFile link + pure state + +setGroupState :: HasCallStack => ClientIdentity -> ByteString -> MLSTest () +setGroupState cid state = do + fp <- nextGroupFile cid + liftIO $ BS.writeFile fp state + +-- | Create conversation and corresponding group. +setupMLSGroup :: HasCallStack => ClientIdentity -> MLSTest (GroupId, Qualified ConvId) +setupMLSGroup creator = do + ownDomain <- liftTest viewFederationDomain + liftIO $ assertEqual "creator is not local" (ciDomain creator) ownDomain + conv <- + responseJsonError + =<< liftTest + ( postConvQualified + (ciUser creator) + (defNewMLSConv (ciClient creator)) + ) + GroupId -> MLSTest () +createGroup cid gid = do + State.gets mlsGroupId >>= \case + Just _ -> liftIO $ assertFailure "only one group can be created" + Nothing -> pure () + + groupJSON <- mlscli cid ["group", "create", T.unpack (toBase64Text (unGroupId gid))] Nothing + g <- nextGroupFile cid + liftIO $ BS.writeFile g groupJSON + State.modify $ \s -> + s + { mlsGroupId = Just gid, + mlsMembers = Set.singleton cid + } + +-- | Create a local group only without a conversation. This simulates creating +-- an MLS conversation on a remote backend. +setupFakeMLSGroup :: ClientIdentity -> MLSTest (GroupId, Qualified ConvId) +setupFakeMLSGroup creator = do + groupId <- + liftIO $ + fmap (GroupId . BS.pack) (replicateM 32 (generate arbitrary)) + createGroup creator groupId + qcnv <- randomQualifiedId (ciDomain creator) + pure (groupId, qcnv) + +keyPackageFile :: HasCallStack => ClientIdentity -> KeyPackageRef -> MLSTest FilePath +keyPackageFile qcid ref = + State.gets $ \mls -> + mlsBaseDir mls cid2Str qcid + T.unpack (T.decodeUtf8 (hex (unKeyPackageRef ref))) + +claimLocalKeyPackages :: HasCallStack => ClientIdentity -> Local UserId -> MLSTest KeyPackageBundle +claimLocalKeyPackages qcid lusr = do + brig <- viewBrig + responseJsonError + =<< post + ( brig + . paths ["mls", "key-packages", "claim", toByteString' (tDomain lusr), toByteString' (tUnqualified lusr)] + . zUser (ciUser qcid) + ) + Qualified UserId -> MLSTest [ClientIdentity] +getUserClients qusr = do + bd <- State.gets mlsBaseDir + files <- getDirectoryContents bd + let toClient f = do + cid <- hush . decodeMLS' . T.encodeUtf8 . T.pack $ f + guard (cidQualifiedUser cid == qusr) + pure cid + pure . catMaybes . map toClient $ files + +-- | Generate one key package for each client of a remote user +claimRemoteKeyPackages :: HasCallStack => Remote UserId -> MLSTest KeyPackageBundle +claimRemoteKeyPackages (qUntagged -> qusr) = do + brig <- viewBrig + clients <- getUserClients qusr + bundle <- fmap (KeyPackageBundle . Set.fromList) $ + for clients $ \cid -> do + (kp, ref) <- generateKeyPackage cid + pure $ + KeyPackageBundleEntry + { kpbeUser = qusr, + kpbeClient = ciClient cid, + kpbeRef = ref, + kpbeKeyPackage = KeyPackageData (rmRaw kp) + } + mapRemoteKeyPackageRef brig bundle + pure bundle + +-- | Claim key package for a local user, or generate and map key packages for remote ones. +claimKeyPackages :: + HasCallStack => + ClientIdentity -> + Qualified UserId -> + MLSTest KeyPackageBundle +claimKeyPackages cid qusr = do + loc <- liftTest $ qualifyLocal () + foldQualified loc (claimLocalKeyPackages cid) claimRemoteKeyPackages qusr + +bundleKeyPackages :: KeyPackageBundle -> MLSTest [(ClientIdentity, FilePath)] +bundleKeyPackages bundle = do + let bundleEntries = kpbEntries bundle + entryIdentity be = mkClientIdentity (kpbeUser be) (kpbeClient be) + for (toList bundleEntries) $ \be -> do + let d = kpData . kpbeKeyPackage $ be + qcid = entryIdentity be + fn <- keyPackageFile qcid (kpbeRef be) + liftIO $ BS.writeFile fn d + pure (qcid, fn) + +-- | Claim keypackages and create a commit/welcome pair on a given client. +-- Note that this alters the state of the group immediately. If we want to test +-- a scenario where the commit is rejected by the backend, we can restore the +-- group to the previous state by using an older version of the group file. +createAddCommit :: HasCallStack => ClientIdentity -> [Qualified UserId] -> MLSTest MessagePackage +createAddCommit cid users = do + kps <- concat <$> traverse (bundleKeyPackages <=< claimKeyPackages cid) users + createAddCommitWithKeyPackages cid kps + +createAddProposals :: HasCallStack => ClientIdentity -> [Qualified UserId] -> MLSTest [MessagePackage] +createAddProposals cid users = do + kps <- concat <$> traverse (bundleKeyPackages <=< claimKeyPackages cid) users + traverse (createAddProposalWithKeyPackage cid) kps + +-- | Create an application message. +createApplicationMessage :: + HasCallStack => + ClientIdentity -> + String -> + MLSTest MessagePackage +createApplicationMessage cid messageContent = do + groupFile <- currentGroupFile cid + message <- + mlscli + cid + ["message", "--group", groupFile, messageContent] + Nothing + + pure $ + MessagePackage + { mpSender = cid, + mpMessage = message, + mpWelcome = Nothing, + mpPublicGroupState = Nothing + } + +createAddCommitWithKeyPackages :: + ClientIdentity -> + [(ClientIdentity, FilePath)] -> + MLSTest MessagePackage +createAddCommitWithKeyPackages qcid clientsAndKeyPackages = do + bd <- State.gets mlsBaseDir + g <- currentGroupFile qcid + gNew <- nextGroupFile qcid + welcomeFile <- liftIO $ emptyTempFile bd "welcome" + pgsFile <- liftIO $ emptyTempFile bd "pgs" + commit <- + mlscli + qcid + ( [ "member", + "add", + "--group", + g, + "--welcome-out", + welcomeFile, + "--group-state-out", + pgsFile, + "--group-out", + gNew + ] + <> map snd clientsAndKeyPackages + ) + Nothing + + State.modify $ \mls -> + mls + { mlsNewMembers = Set.fromList (map fst clientsAndKeyPackages) + } + + welcome <- liftIO $ BS.readFile welcomeFile + pgs <- liftIO $ BS.readFile pgsFile + pure $ + MessagePackage + { mpSender = qcid, + mpMessage = commit, + mpWelcome = Just welcome, + mpPublicGroupState = Just pgs + } + +createAddProposalWithKeyPackage :: + ClientIdentity -> + (ClientIdentity, FilePath) -> + MLSTest MessagePackage +createAddProposalWithKeyPackage cid (_, kp) = do + g <- currentGroupFile cid + gNew <- nextGroupFile cid + prop <- + mlscli + cid + ["proposal", "--group-in", g, "--group-out", gNew, "add", kp] + Nothing + pure + MessagePackage + { mpSender = cid, + mpMessage = prop, + mpWelcome = Nothing, + mpPublicGroupState = Nothing + } + +createPendingProposalCommit :: HasCallStack => ClientIdentity -> MLSTest MessagePackage +createPendingProposalCommit qcid = do + bd <- State.gets mlsBaseDir + welcomeFile <- liftIO $ emptyTempFile bd "welcome" + pgsFile <- liftIO $ emptyTempFile bd "pgs" + g <- currentGroupFile qcid + gNew <- nextGroupFile qcid + commit <- + mlscli + qcid + [ "commit", + "--group", + g, + "--group-out", + gNew, + "--welcome-out", + welcomeFile, + "--group-state-out", + pgsFile + ] + Nothing + + welcome <- liftIO $ readWelcome welcomeFile + pgs <- liftIO $ BS.readFile pgsFile + pure + MessagePackage + { mpSender = qcid, + mpMessage = commit, + mpWelcome = welcome, + mpPublicGroupState = Just pgs + } + +readWelcome :: FilePath -> IO (Maybe ByteString) +readWelcome fp = runMaybeT $ do + liftIO (doesFileExist fp) >>= guard + stat <- liftIO $ getFileStatus fp + guard $ fileSize stat > 0 + liftIO $ BS.readFile fp + +createRemoveCommit :: HasCallStack => ClientIdentity -> [ClientIdentity] -> MLSTest MessagePackage +createRemoveCommit cid targets = do + bd <- State.gets mlsBaseDir + welcomeFile <- liftIO $ emptyTempFile bd "welcome" + pgsFile <- liftIO $ emptyTempFile bd "pgs" + g <- currentGroupFile cid + gNew <- nextGroupFile cid + + kprefByClient <- liftIO $ Map.fromList <$> readGroupState g + let fetchKeyPackage c = keyPackageFile c (kprefByClient Map.! c) + kps <- traverse fetchKeyPackage targets + + commit <- + mlscli + cid + ( [ "member", + "remove", + "--group", + g, + "--group-out", + gNew, + "--welcome-out", + welcomeFile, + "--group-state-out", + pgsFile + ] + <> kps + ) + Nothing + welcome <- liftIO $ readWelcome welcomeFile + pgs <- liftIO $ BS.readFile pgsFile + pure + MessagePackage + { mpSender = cid, + mpMessage = commit, + mpWelcome = welcome, + mpPublicGroupState = Just pgs + } + +createExternalAddProposal :: HasCallStack => ClientIdentity -> MLSTest MessagePackage +createExternalAddProposal joiner = do + groupId <- + State.gets mlsGroupId >>= \case + Nothing -> liftIO $ assertFailure "Creating add proposal for non-existing group" + Just g -> pure g + epoch <- State.gets mlsEpoch + proposal <- + mlscli + joiner + [ "proposal-external", + "--group-id", + T.unpack (toBase64Text (unGroupId groupId)), + "--epoch", + show epoch, + "add" + ] + Nothing + + State.modify $ \mls -> + mls + { mlsNewMembers = mlsNewMembers mls <> Set.singleton joiner + } + pure + MessagePackage + { mpSender = joiner, + mpMessage = proposal, + mpWelcome = Nothing, + mpPublicGroupState = Nothing + } + +consumeWelcome :: HasCallStack => ByteString -> MLSTest () +consumeWelcome welcome = do + qcids <- State.gets mlsNewMembers + for_ qcids $ \qcid -> do + link <- groupFileLink qcid + liftIO $ + doesFileExist link >>= \e -> + assertBool "Existing clients in a conversation should not consume commits" (not e) + groupFile <- nextGroupFile qcid + void $ + mlscli + qcid + [ "group", + "from-welcome", + "--group-out", + groupFile, + "-" + ] + (Just welcome) + +-- | Make all member clients consume a given message. +consumeMessage :: HasCallStack => MessagePackage -> MLSTest () +consumeMessage msg = do + mems <- State.gets mlsMembers + for_ (Set.delete (mpSender msg) mems) $ \cid -> + consumeMessage1 cid (mpMessage msg) + +consumeMessage1 :: HasCallStack => ClientIdentity -> ByteString -> MLSTest () +consumeMessage1 cid msg = do + bd <- State.gets mlsBaseDir + g <- currentGroupFile cid + gNew <- nextGroupFile cid + void $ + mlscli + cid + [ "consume", + "--group", + g, + "--group-out", + gNew, + "--signer-key", + bd "removal.key", + "-" + ] + (Just msg) + +-- | Send an MLS message and simulate clients receiving it. If the message is a +-- commit, the 'sendAndConsumeCommit' function should be used instead. +sendAndConsumeMessage :: HasCallStack => MessagePackage -> MLSTest [Event] +sendAndConsumeMessage mp = do + events <- + fmap mmssEvents . responseJsonError + =<< postMessage (ciUser (mpSender mp)) (mpMessage mp) + do + postWelcome (ciUser (mpSender mp)) welcome + !!! const 201 === statusCode + consumeWelcome welcome + + pure events + +-- | Send an MLS commit message, simulate clients receiving it, and update the +-- test state accordingly. +sendAndConsumeCommit :: + HasCallStack => + MessagePackage -> + MLSTest [Event] +sendAndConsumeCommit mp = do + events <- sendAndConsumeMessage mp + + -- increment epoch and add new clients + State.modify $ \mls -> + mls + { mlsEpoch = mlsEpoch mls + 1, + mlsMembers = mlsMembers mls <> mlsNewMembers mls, + mlsNewMembers = mempty + } + + pure events + +mkBundle :: MessagePackage -> Either Text CommitBundle +mkBundle mp = do + commitB <- decodeMLS' (mpMessage mp) + welcomeB <- traverse decodeMLS' (mpWelcome mp) + pgs <- note "public group state unavailable" (mpPublicGroupState mp) + pgsB <- decodeMLS' pgs + pure $ + CommitBundle commitB welcomeB $ + GroupInfoBundle UnencryptedGroupInfo TreeFull pgsB + +createBundle :: MonadIO m => MessagePackage -> m ByteString +createBundle mp = do + bundle <- + either (liftIO . assertFailure . T.unpack) pure $ + mkBundle mp + pure (encodeMLS' bundle) + +sendAndConsumeCommitBundle :: + HasCallStack => + MessagePackage -> + MLSTest [Event] +sendAndConsumeCommitBundle mp = do + bundle <- createBundle mp + events <- + fmap mmssEvents + . responseJsonError + =<< postCommitBundle (ciUser (mpSender mp)) bundle + + mls + { mlsEpoch = mlsEpoch mls + 1, + mlsMembers = mlsMembers mls <> mlsNewMembers mls, + mlsNewMembers = mempty + } + + pure events + +mlsBracket :: + HasCallStack => + [ClientIdentity] -> + ([WS.WebSocket] -> MLSTest a) -> + MLSTest a +mlsBracket clients k = do + c <- view tsCannon + WS.bracketAsClientRN c (map (ciUser &&& ciClient) clients) k + +readGroupState :: FilePath -> IO [(ClientIdentity, KeyPackageRef)] +readGroupState fp = do + j <- BS.readFile fp + pure $ do + node <- j ^.. key "group" . key "tree" . key "tree" . key "nodes" . _Array . traverse + leafNode <- node ^.. key "node" . key "LeafNode" + identity <- + either (const []) pure . decodeMLS' . BS.pack . map fromIntegral $ + leafNode ^.. key "key_package" . key "payload" . key "credential" . key "credential" . key "Basic" . key "identity" . key "vec" . _Array . traverse . _Integer + kpr <- (unhexM . T.encodeUtf8 =<<) $ leafNode ^.. key "key_package_ref" . _String + pure (identity, KeyPackageRef kpr) + +getClientsFromGroupState :: + ClientIdentity -> + Qualified UserId -> + MLSTest [(ClientIdentity, KeyPackageRef)] +getClientsFromGroupState cid u = do + groupFile <- currentGroupFile cid + groupState <- liftIO $ readGroupState groupFile + pure $ filter (\(cid', _) -> cidQualifiedUser cid' == u) groupState + +clientKeyPair :: ClientIdentity -> MLSTest (ByteString, ByteString) +clientKeyPair cid = do + bd <- State.gets mlsBaseDir + credential <- + liftIO . BS.readFile $ + bd cid2Str cid "store" T.unpack (T.decodeUtf8 (B64U.encode "self")) + let s = + credential ^.. key "signature_private_key" . key "value" . _Array . traverse . _Integer + & fmap fromIntegral + & BS.pack + pure $ BS.splitAt 32 s + +receiveNewRemoteConv :: + (MonadReader TestSetup m, MonadIO m) => + Qualified ConvId -> + GroupId -> + m () +receiveNewRemoteConv conv gid = do + client <- view tsFedGalleyClient + let nrc = + NewRemoteConversation (qUnqualified conv) $ + ProtocolMLS + ( ConversationMLSData + gid + (Epoch 1) + MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + ) + void $ + runFedClient + @"on-new-remote-conversation" + client + (qDomain conv) + nrc + +receiveOnConvUpdated :: + (MonadReader TestSetup m, MonadIO m) => + Qualified ConvId -> + Qualified UserId -> + Qualified UserId -> + m () +receiveOnConvUpdated conv origUser joiner = do + client <- view tsFedGalleyClient + now <- liftIO getCurrentTime + let cu = + ConversationUpdate + { cuTime = now, + cuOrigUserId = origUser, + cuConvId = qUnqualified conv, + cuAlreadyPresentUsers = [qUnqualified joiner], + cuAction = + SomeConversationAction + SConversationJoinTag + ConversationJoin + { cjUsers = pure joiner, + cjRole = roleNameWireMember + } + } + void $ + runFedClient + @"on-conversation-updated" + client + (qDomain conv) + cu + +getGroupInfo :: + ( HasCallStack, + MonadIO m, + MonadCatch m, + MonadThrow m, + MonadHttp m, + HasGalley m + ) => + UserId -> + Qualified ConvId -> + m ResponseLBS +getGroupInfo sender qcnv = do + galley <- viewGalley + get + ( galley + . paths + [ "conversations", + toByteString' (qDomain qcnv), + toByteString' (qUnqualified qcnv), + "groupinfo" + ] + . zUser sender + . zConn "conn" + ) diff --git a/services/galley/test/integration/API/Roles.hs b/services/galley/test/integration/API/Roles.hs index 1f55cfe02a..ec4a5dc80d 100644 --- a/services/galley/test/integration/API/Roles.hs +++ b/services/galley/test/integration/API/Roles.hs @@ -67,7 +67,7 @@ testAllConversationRoles = do connectUsers alice (list1 bob [chuck]) let role = roleNameWireAdmin c <- decodeConvId <$> postConvWithRole alice [bob] (Just "gossip") [] Nothing Nothing role - g <- view tsGalley + g <- viewGalley get ( g . paths ["conversations", toByteString' c, "roles"] diff --git a/services/galley/test/integration/API/Teams.hs b/services/galley/test/integration/API/Teams.hs index 489e3c3cab..353280ac05 100644 --- a/services/galley/test/integration/API/Teams.hs +++ b/services/galley/test/integration/API/Teams.hs @@ -33,7 +33,6 @@ import qualified Brig.Types.Intra as Brig import Control.Arrow ((>>>)) import Control.Lens hiding ((#), (.=)) import Control.Monad.Catch -import Control.Retry import Data.Aeson hiding (json) import Data.ByteString.Conversion import Data.ByteString.Lazy (fromStrict) @@ -75,8 +74,8 @@ import Test.Tasty import Test.Tasty.Cannon (TimeoutUnit (..), (#)) import qualified Test.Tasty.Cannon as WS import Test.Tasty.HUnit -import TestHelpers (test, viewFederationDomain) -import TestSetup (TestM, TestSetup, tsBrig, tsCannon, tsGConf, tsGalley) +import TestHelpers +import TestSetup import UnliftIO (mapConcurrently) import Wire.API.Conversation import Wire.API.Conversation.Protocol @@ -303,7 +302,7 @@ testListTeamMembersCsv numMembers = do addClient :: UserId -> Int -> TestM () addClient uid i = do - brig <- view tsBrig + brig <- viewBrig post (brig . paths ["i", "clients", toByteString' uid] . contentJson . json (newClient (someLastPrekeys !! i)) . queryItem "skip_reauth" "true") !!! const 201 === statusCode newClient :: PC.LastPrekey -> C.NewClient @@ -382,7 +381,7 @@ testEnableSSOPerTeam = do liftIO $ assertEqual msg enabledness statusValue let putSSOEnabledInternalCheckNotImplemented :: HasCallStack => TestM () putSSOEnabledInternalCheckNotImplemented = do - g <- view tsGalley + g <- viewGalley Wai.Error status label _ _ <- responseJsonUnsafe <$> put @@ -406,27 +405,27 @@ testEnableTeamSearchVisibilityPerTeam = do (tid, owner, member : _) <- Util.createBindingTeamWithMembers 2 let check :: String -> Public.FeatureStatus -> TestM () check msg enabledness = do - g <- view tsGalley + g <- viewGalley status :: Public.WithStatusNoLock Public.SearchVisibilityAvailableConfig <- responseJsonUnsafe <$> (Util.getTeamSearchVisibilityAvailableInternal g tid putSearchVisibility g owner tid SearchVisibilityNoNameOutsideTeam liftIO $ do assertEqual "bad status" status403 status assertEqual "bad label" "team-search-visibility-not-enabled" label let getSearchVisibilityCheck :: TeamSearchVisibility -> TestM () getSearchVisibilityCheck vis = do - g <- view tsGalley + g <- viewGalley getSearchVisibility g owner tid !!! do const 200 === statusCode const (Just (TeamSearchVisibilityView vis)) === responseJsonUnsafe Util.withCustomSearchFeature FeatureTeamSearchVisibilityAvailableByDefault $ do - g <- view tsGalley + g <- viewGalley check "Teams should start with Custom Search Visibility enabled" Public.FeatureStatusEnabled putSearchVisibility g owner tid SearchVisibilityNoNameOutsideTeam !!! const 204 === statusCode putSearchVisibility g owner tid SearchVisibilityStandard !!! const 204 === statusCode @@ -434,7 +433,7 @@ testEnableTeamSearchVisibilityPerTeam = do check "Teams should start with Custom Search Visibility disabled" Public.FeatureStatusDisabled putSearchVisibilityCheckNotAllowed - g <- view tsGalley + g <- viewGalley Util.putTeamSearchVisibilityAvailableInternal g tid Public.FeatureStatusEnabled -- Nothing was set, default value getSearchVisibilityCheck SearchVisibilityStandard @@ -491,8 +490,6 @@ testCreateOne2OneWithMembers (rolePermissions -> perms) = do -- | At the time of writing this test, the only event sent to this queue is 'MemberJoin'. testTeamQueue :: TestM () testTeamQueue = do - let eventually = recovering (limitRetries 3 <> exponentialBackoff 100000) [] . const - (owner, tid) <- createBindingTeam eventually $ do queue <- getTeamQueue owner Nothing Nothing False @@ -566,7 +563,7 @@ testAddTeamMemberInternal = do testRemoveBindingTeamMember :: Bool -> TestM () testRemoveBindingTeamMember ownerHasPassword = do localDomain <- viewFederationDomain - g <- view tsGalley + g <- viewGalley c <- view tsCannon -- Owner who creates the team must have an email, This is why we run all tests with a second -- owner @@ -689,7 +686,7 @@ testRemoveBindingTeamOwner = do where check :: HasCallStack => TeamId -> UserId -> UserId -> Maybe PlainTextPassword -> Maybe LText -> TestM () check tid deleter deletee pass maybeError = do - g <- view tsGalley + g <- viewGalley delete ( g . paths ["teams", toByteString' tid, "members", toByteString' deletee] @@ -913,7 +910,7 @@ testUpdateTeamConv _ convRole = do testDeleteBindingTeamSingleMember :: TestM () testDeleteBindingTeamSingleMember = do - g <- view tsGalley + g <- viewGalley c <- view tsCannon (owner, tid) <- Util.createBindingTeam other <- Util.addUserToTeam owner tid @@ -974,7 +971,7 @@ testDeleteBindingTeamSingleMember = do testDeleteBindingTeamNoMembers :: TestM () testDeleteBindingTeamNoMembers = do - g <- view tsGalley + g <- viewGalley (owner, tid) <- Util.createBindingTeam deleteUser owner !!! const 200 === statusCode ensureQueueEmpty @@ -985,8 +982,8 @@ testDeleteBindingTeamNoMembers = do testDeleteBindingTeamMoreThanOneMember :: TestM () testDeleteBindingTeamMoreThanOneMember = do - g <- view tsGalley - b <- view tsBrig + g <- viewGalley + b <- viewBrig c <- view tsCannon (alice, tid, members) <- Util.createBindingTeamWithNMembers 10 ensureQueueEmpty @@ -1014,7 +1011,7 @@ testDeleteBindingTeamMoreThanOneMember = do testDeleteTeamVerificationCodeSuccess :: TestM () testDeleteTeamVerificationCodeSuccess = do - g <- view tsGalley + g <- viewGalley (owner, tid) <- Util.createBindingTeam' let Just email = U.userEmail owner setFeatureLockStatus @Public.SndFactorPasswordChallengeConfig tid Public.LockStatusUnlocked @@ -1038,7 +1035,7 @@ testDeleteTeamVerificationCodeSuccess = do -- Test that team cannot be deleted with missing second factor email verification code when this feature is enabled testDeleteTeamVerificationCodeMissingCode :: TestM () testDeleteTeamVerificationCodeMissingCode = do - g <- view tsGalley + g <- viewGalley (owner, tid) <- Util.createBindingTeam' setFeatureLockStatus @Public.SndFactorPasswordChallengeConfig tid Public.LockStatusUnlocked setTeamSndFactorPasswordChallenge tid Public.FeatureStatusEnabled @@ -1063,7 +1060,7 @@ testDeleteTeamVerificationCodeMissingCode = do -- Test that team cannot be deleted with expired second factor email verification code when this feature is enabled testDeleteTeamVerificationCodeExpiredCode :: TestM () testDeleteTeamVerificationCodeExpiredCode = do - g <- view tsGalley + g <- viewGalley (owner, tid) <- Util.createBindingTeam' setFeatureLockStatus @Public.SndFactorPasswordChallengeConfig tid Public.LockStatusUnlocked setTeamSndFactorPasswordChallenge tid Public.FeatureStatusEnabled @@ -1091,7 +1088,7 @@ testDeleteTeamVerificationCodeExpiredCode = do -- Test that team cannot be deleted with wrong second factor email verification code when this feature is enabled testDeleteTeamVerificationCodeWrongCode :: TestM () testDeleteTeamVerificationCodeWrongCode = do - g <- view tsGalley + g <- viewGalley (owner, tid) <- Util.createBindingTeam' setFeatureLockStatus @Public.SndFactorPasswordChallengeConfig tid Public.LockStatusUnlocked setTeamSndFactorPasswordChallenge tid Public.FeatureStatusEnabled @@ -1114,24 +1111,24 @@ testDeleteTeamVerificationCodeWrongCode = do setFeatureLockStatus :: forall cfg. (Public.IsFeatureConfig cfg, KnownSymbol (Public.FeatureSymbol cfg)) => TeamId -> Public.LockStatus -> TestM () setFeatureLockStatus tid status = do - g <- view tsGalley + g <- viewGalley put (g . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg, toByteString' status]) !!! const 200 === statusCode generateVerificationCode :: Public.SendVerificationCode -> TestM () generateVerificationCode req = do - brig <- view tsBrig + brig <- viewBrig let js = RequestBodyLBS $ encode req post (brig . paths ["verification-code", "send"] . contentJson . body js) !!! const 200 === statusCode setTeamSndFactorPasswordChallenge :: TeamId -> Public.FeatureStatus -> TestM () setTeamSndFactorPasswordChallenge tid status = do - g <- view tsGalley + g <- viewGalley let js = RequestBodyLBS $ encode $ Public.WithStatusNoLock status Public.SndFactorPasswordChallengeConfig Public.FeatureTTLUnlimited put (g . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @Public.SndFactorPasswordChallengeConfig] . contentJson . body js) !!! const 200 === statusCode getVerificationCode :: UserId -> Public.VerificationAction -> TestM Code.Value getVerificationCode uid action = do - brig <- view tsBrig + brig <- viewBrig resp <- get (brig . paths ["i", "users", toByteString' uid, "verification-code", toByteString' action]) TestM () testDeleteBindingTeam ownerHasPassword = do - g <- view tsGalley + g <- viewGalley c <- view tsCannon (ownerWithPassword, tid) <- Util.createBindingTeam ownerMem <- @@ -1275,7 +1272,7 @@ testDeleteTeamConv = do testUpdateTeamIconValidation :: TestM () testUpdateTeamIconValidation = do - g <- view tsGalley + g <- viewGalley (tid, owner, _) <- Util.createBindingTeamWithMembers 2 let update payload expectedStatusCode = put @@ -1300,7 +1297,7 @@ testUpdateTeamIconValidation = do testUpdateTeam :: TestM () testUpdateTeam = do - g <- view tsGalley + g <- viewGalley c <- view tsCannon (tid, owner, [member]) <- Util.createBindingTeamWithMembers 2 @@ -1405,7 +1402,7 @@ testTeamAddRemoveMemberAboveThresholdNoEvents = do modifyUserProfileAndExpectEvent :: HasCallStack => Bool -> UserId -> [UserId] -> TestM () modifyUserProfileAndExpectEvent expect target listeners = do c <- view tsCannon - b <- view tsBrig + b <- viewBrig WS.bracketRN c listeners $ \wsListeners -> do -- Do something let u = U.UserUpdate (Just $ U.Name "name") Nothing Nothing Nothing @@ -1424,7 +1421,7 @@ testTeamAddRemoveMemberAboveThresholdNoEvents = do modifyTeamDataAndExpectEvent :: HasCallStack => Bool -> TeamId -> UserId -> TestM () modifyTeamDataAndExpectEvent expect tid origin = do c <- view tsCannon - g <- view tsGalley + g <- viewGalley let u = newTeamUpdateData & nameUpdate .~ (Just $ unsafeRange "bar") WS.bracketR c origin $ \wsOrigin -> do put @@ -1453,7 +1450,7 @@ testTeamAddRemoveMemberAboveThresholdNoEvents = do removeTeamMemberAndExpectEvent :: HasCallStack => Bool -> UserId -> TeamId -> UserId -> [UserId] -> TestM () removeTeamMemberAndExpectEvent expect owner tid victim others = do c <- view tsCannon - g <- view tsGalley + g <- viewGalley WS.bracketRN c (owner : victim : others) $ \(wsOwner : _wsVictim : wsOthers) -> do delete ( g @@ -1473,7 +1470,7 @@ testTeamAddRemoveMemberAboveThresholdNoEvents = do deleteTeam :: HasCallStack => TeamId -> UserId -> [UserId] -> [Qualified ConvId] -> UserId -> TestM () deleteTeam tid owner otherRealUsersInTeam teamCidsThatExternBelongsTo extern = do c <- view tsCannon - g <- view tsGalley + g <- viewGalley void . WS.bracketRN c (owner : extern : otherRealUsersInTeam) $ \(_wsOwner : wsExtern : _wsotherRealUsersInTeam) -> do delete ( g @@ -1500,7 +1497,7 @@ testBillingInLargeTeam = do (firstOwner, team) <- Util.createBindingTeam refreshIndex opts <- view tsGConf - galley <- view tsGalley + galley <- viewGalley let fanoutLimit = fromRange $ Galley.currentFanoutLimit opts allOwnersBeforeFanoutLimit <- foldM @@ -1537,7 +1534,7 @@ testBillingInLargeTeamWithoutIndexedBillingTeamMembers = do (firstOwner, team) <- Util.createBindingTeam refreshIndex opts <- view tsGConf - galley <- view tsGalley + galley <- viewGalley let withoutIndexedBillingTeamMembers = withSettingsOverrides (\o -> o & optSettings . setEnableIndexedBillingTeamMembers ?~ False) let fanoutLimit = fromRange $ Galley.currentFanoutLimit opts @@ -1569,7 +1566,7 @@ testBillingInLargeTeamWithoutIndexedBillingTeamMembers = do -- We cannot properly add the new owner with an invite as we don't have a way to -- override galley settings while making a call to brig withoutIndexedBillingTeamMembers $ do - g <- view tsGalley + g <- viewGalley post (g . paths ["i", "teams", toByteString' team, "members"] . memFanoutPlusTwo) !!! const 200 === statusCode assertQueue ("add " <> show (fanoutLimit + 2) <> "th billing member: " <> show ownerFanoutPlusTwo) $ @@ -1635,7 +1632,7 @@ testBillingInLargeTeamWithoutIndexedBillingTeamMembers = do -- Demotion by inferior roles is NOT allowed. testUpdateTeamMember :: TestM () testUpdateTeamMember = do - g <- view tsGalley + g <- viewGalley c <- view tsCannon (owner, tid) <- Util.createBindingTeam member <- Util.addUserToTeamWithRole (Just RoleAdmin) owner tid @@ -1701,7 +1698,7 @@ testUpdateTeamMember = do testUpdateTeamStatus :: TestM () testUpdateTeamStatus = do - g <- view tsGalley + g <- viewGalley (_, tid) <- Util.createBindingTeam -- Check for idempotency Util.changeTeamStatus tid TeamsIntra.Active @@ -1910,6 +1907,9 @@ postCryptoBroadcastMessage2 bcast = do -- Deleted charlie WS.bracketR2 c bob charlie $ \(wsB, wsE) -> do deleteClient charlie cc (Just defPassword) !!! const 200 === statusCode + liftIO $ + WS.assertMatch_ (5 # WS.Second) wsE $ + wsAssertClientRemoved cc let m4 = [(bob, bc, toBase64Text "ciphertext4"), (charlie, cc, toBase64Text "ciphertext4")] Util.postBroadcast (q alice) ac bcast {bMessage = m4} !!! do const 201 === statusCode diff --git a/services/galley/test/integration/API/Teams/Feature.hs b/services/galley/test/integration/API/Teams/Feature.hs index 0660b3f836..cf56f46ddd 100644 --- a/services/galley/test/integration/API/Teams/Feature.hs +++ b/services/galley/test/integration/API/Teams/Feature.hs @@ -18,15 +18,16 @@ module API.Teams.Feature (tests) where -import API.Util (HasGalley, getFeatureStatusMulti, withSettingsOverrides) -import qualified API.Util as Util -import API.Util.TeamFeature (patchFeatureStatusInternal) +import API.SQS (assertQueue, tActivate) +import API.Util +import API.Util.TeamFeature hiding (getFeatureConfig, setLockStatusInternal) import qualified API.Util.TeamFeature as Util import Bilge import Bilge.Assert import Brig.Types.Test.Arbitrary (Arbitrary (arbitrary)) import Cassandra as Cql -import Control.Lens (over, to, view) +import Control.Lens (over, to, view, (.~), (?~)) +import Control.Lens.Operators () import Control.Monad.Catch (MonadCatch) import Data.Aeson (FromJSON, ToJSON) import qualified Data.Aeson as Aeson @@ -40,7 +41,7 @@ import Data.Schema (ToSchema) import qualified Data.Set as Set import Data.Timeout (TimeoutUnit (Second), (#)) import GHC.TypeLits (KnownSymbol) -import Galley.Options (optSettings, setFeatureFlags) +import Galley.Options (optSettings, setExposeInvitationURLsTeamAllowlist, setFeatureFlags) import Galley.Types.Teams import Imports import Network.Wai.Utilities (label) @@ -49,14 +50,14 @@ import Test.QuickCheck (Gen, generate, suchThat) import Test.Tasty import qualified Test.Tasty.Cannon as WS import Test.Tasty.HUnit (assertFailure, (@?=)) -import TestHelpers (test) +import TestHelpers (eventually, test) import TestSetup import Wire.API.Conversation.Protocol (ProtocolTag (ProtocolMLSTag, ProtocolProteusTag)) import qualified Wire.API.Event.FeatureConfig as FeatureConfig import Wire.API.Internal.Notification (Notification) import Wire.API.MLS.CipherSuite import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti as Multi -import Wire.API.Team.Feature (FeatureStatus (..), FeatureTTL, FeatureTTL' (..), LockStatus (LockStatusUnlocked), MLSConfig (MLSConfig)) +import Wire.API.Team.Feature (ExposeInvitationURLsToTeamAdminConfig (..), FeatureStatus (..), FeatureTTL, FeatureTTL' (..), LockStatus (LockStatusUnlocked), MLSConfig (MLSConfig)) import qualified Wire.API.Team.Feature as Public tests :: IO TestSetup -> TestTree @@ -135,6 +136,12 @@ tests s = testPatch AssertLockStatusChange Public.FeatureStatusDisabled Public.SndFactorPasswordChallengeConfig, test s (unpack $ Public.featureNameBS @Public.SelfDeletingMessagesConfig) $ testPatch AssertLockStatusChange Public.FeatureStatusEnabled (Public.SelfDeletingMessagesConfig 0) + ], + testGroup + "ExposeInvitationURLsToTeamAdmin" + [ test s "can be set when TeamId is in allow list" testExposeInvitationURLsToTeamAdminTeamIdInAllowList, + test s "can not be set when allow list is empty" testExposeInvitationURLsToTeamAdminEmptyAllowList, + test s "server config takes precendece over team feature config" testExposeInvitationURLsToTeamAdminServerConfigTakesPrecedence ] ] @@ -217,10 +224,10 @@ testPatch' :: cfg -> TestM () testPatch' testLockStatusChange rndFeatureConfig defStatus defConfig = do - (_, tid) <- Util.createBindingTeam - Just original <- responseJsonMaybe <$> Util.getFeatureStatusInternal @cfg tid + (_, tid) <- createBindingTeam + Just original <- responseJsonMaybe <$> getFeatureStatusInternal @cfg tid patchFeatureStatusInternal tid rndFeatureConfig !!! statusCode === const 200 - Just actual <- responseJsonMaybe <$> Util.getFeatureStatusInternal @cfg tid + Just actual <- responseJsonMaybe <$> getFeatureStatusInternal @cfg tid liftIO $ if Public.wsLockStatus actual == Public.LockStatusLocked then do @@ -234,19 +241,19 @@ testPatch' testLockStatusChange rndFeatureConfig defStatus defConfig = do testSSO :: (TeamId -> Public.FeatureStatus -> TestM ()) -> TestM () testSSO setSSOFeature = do - (_owner, tid, member : _) <- Util.createBindingTeamWithNMembers 1 - nonMember <- Util.randomUser + (_owner, tid, member : _) <- createBindingTeamWithNMembers 1 + nonMember <- randomUser let getSSO :: HasCallStack => Public.FeatureStatus -> TestM () - getSSO = assertFlagNoConfig @Public.SSOConfig $ Util.getTeamFeatureFlag @Public.SSOConfig member tid + getSSO = assertFlagNoConfig @Public.SSOConfig $ getTeamFeatureFlag @Public.SSOConfig member tid getSSOFeatureConfig :: HasCallStack => Public.FeatureStatus -> TestM () getSSOFeatureConfig expectedStatus = do actual <- Util.getFeatureConfig @Public.SSOConfig member liftIO $ Public.wsStatus actual @?= expectedStatus getSSOInternal :: HasCallStack => Public.FeatureStatus -> TestM () - getSSOInternal = assertFlagNoConfig @Public.SSOConfig $ Util.getTeamFeatureFlagInternal @Public.SSOConfig tid + getSSOInternal = assertFlagNoConfig @Public.SSOConfig $ getTeamFeatureFlagInternal @Public.SSOConfig tid - assertFlagForbidden $ Util.getTeamFeatureFlag @Public.SSOConfig nonMember tid + assertFlagForbidden $ getTeamFeatureFlag @Public.SSOConfig nonMember tid featureSSO <- view (tsGConf . optSettings . setFeatureFlags . flagSSO) case featureSSO of @@ -270,20 +277,20 @@ testSSO setSSOFeature = do putSSOInternal :: HasCallStack => TeamId -> Public.FeatureStatus -> TestM () putSSOInternal tid = - void . Util.putTeamFeatureFlagInternal @Public.SSOConfig expect2xx tid + void . putTeamFeatureFlagInternal @Public.SSOConfig expect2xx tid . (\st -> Public.WithStatusNoLock st Public.SSOConfig Public.FeatureTTLUnlimited) patchSSOInternal :: HasCallStack => TeamId -> Public.FeatureStatus -> TestM () -patchSSOInternal tid status = void $ Util.patchFeatureStatusInternalWithMod @Public.SSOConfig expect2xx tid (Public.withStatus' (Just status) Nothing Nothing (Just Public.FeatureTTLUnlimited)) +patchSSOInternal tid status = void $ patchFeatureStatusInternalWithMod @Public.SSOConfig expect2xx tid (Public.withStatus' (Just status) Nothing Nothing (Just Public.FeatureTTLUnlimited)) testLegalHold :: ((Request -> Request) -> TeamId -> Public.FeatureStatus -> TestM ()) -> TestM () testLegalHold setLegalHoldInternal = do - (_owner, tid, member : _) <- Util.createBindingTeamWithNMembers 1 - nonMember <- Util.randomUser + (_owner, tid, member : _) <- createBindingTeamWithNMembers 1 + nonMember <- randomUser let getLegalHold :: HasCallStack => Public.FeatureStatus -> TestM () - getLegalHold = assertFlagNoConfig @Public.LegalholdConfig $ Util.getTeamFeatureFlag @Public.LegalholdConfig member tid + getLegalHold = assertFlagNoConfig @Public.LegalholdConfig $ getTeamFeatureFlag @Public.LegalholdConfig member tid getLegalHoldInternal :: HasCallStack => Public.FeatureStatus -> TestM () - getLegalHoldInternal = assertFlagNoConfig @Public.LegalholdConfig $ Util.getTeamFeatureFlagInternal @Public.LegalholdConfig tid + getLegalHoldInternal = assertFlagNoConfig @Public.LegalholdConfig $ getTeamFeatureFlagInternal @Public.LegalholdConfig tid getLegalHoldFeatureConfig expectedStatus = do actual <- Util.getFeatureConfig @Public.LegalholdConfig member liftIO $ Public.wsStatus actual @?= expectedStatus @@ -291,7 +298,7 @@ testLegalHold setLegalHoldInternal = do getLegalHold Public.FeatureStatusDisabled getLegalHoldInternal Public.FeatureStatusDisabled - assertFlagForbidden $ Util.getTeamFeatureFlag @Public.LegalholdConfig nonMember tid + assertFlagForbidden $ getTeamFeatureFlag @Public.LegalholdConfig nonMember tid -- FUTUREWORK: run two galleys, like below for custom search visibility. featureLegalHold <- view (tsGConf . optSettings . setFeatureFlags . flagLegalHold) @@ -318,25 +325,25 @@ testLegalHold setLegalHoldInternal = do putLegalHoldInternal :: HasCallStack => (Request -> Request) -> TeamId -> Public.FeatureStatus -> TestM () putLegalHoldInternal expectation tid = - void . Util.putTeamFeatureFlagInternal @Public.LegalholdConfig expectation tid + void . putTeamFeatureFlagInternal @Public.LegalholdConfig expectation tid . (\st -> Public.WithStatusNoLock st Public.LegalholdConfig Public.FeatureTTLUnlimited) patchLegalHoldInternal :: HasCallStack => (Request -> Request) -> TeamId -> Public.FeatureStatus -> TestM () -patchLegalHoldInternal expectation tid status = void $ Util.patchFeatureStatusInternalWithMod @Public.LegalholdConfig expectation tid (Public.withStatus' (Just status) Nothing Nothing (Just Public.FeatureTTLUnlimited)) +patchLegalHoldInternal expectation tid status = void $ patchFeatureStatusInternalWithMod @Public.LegalholdConfig expectation tid (Public.withStatus' (Just status) Nothing Nothing (Just Public.FeatureTTLUnlimited)) testSearchVisibility :: TestM () testSearchVisibility = do let getTeamSearchVisibility :: TeamId -> UserId -> Public.FeatureStatus -> TestM () getTeamSearchVisibility teamid uid expected = do - g <- view tsGalley - Util.getTeamSearchVisibilityAvailable g uid teamid !!! do + g <- viewGalley + getTeamSearchVisibilityAvailable g uid teamid !!! do statusCode === const 200 responseJsonEither === const (Right (Public.WithStatusNoLock expected Public.SearchVisibilityAvailableConfig Public.FeatureTTLUnlimited)) let getTeamSearchVisibilityInternal :: TeamId -> Public.FeatureStatus -> TestM () getTeamSearchVisibilityInternal teamid expected = do - g <- view tsGalley - Util.getTeamSearchVisibilityAvailableInternal g teamid !!! do + g <- viewGalley + getTeamSearchVisibilityAvailableInternal g teamid !!! do statusCode === const 200 responseJsonEither === const (Right (Public.WithStatusNoLock expected Public.SearchVisibilityAvailableConfig Public.FeatureTTLUnlimited)) @@ -347,15 +354,15 @@ testSearchVisibility = do let setTeamSearchVisibilityInternal :: TeamId -> Public.FeatureStatus -> TestM () setTeamSearchVisibilityInternal teamid val = do - g <- view tsGalley - Util.putTeamSearchVisibilityAvailableInternal g teamid val + g <- viewGalley + putTeamSearchVisibilityAvailableInternal g teamid val - (owner, tid, [member]) <- Util.createBindingTeamWithNMembers 1 - nonMember <- Util.randomUser + (owner, tid, [member]) <- createBindingTeamWithNMembers 1 + nonMember <- randomUser - assertFlagForbidden $ Util.getTeamFeatureFlag @Public.SearchVisibilityAvailableConfig nonMember tid + assertFlagForbidden $ getTeamFeatureFlag @Public.SearchVisibilityAvailableConfig nonMember tid - Util.withCustomSearchFeature FeatureTeamSearchVisibilityUnavailableByDefault $ do + withCustomSearchFeature FeatureTeamSearchVisibilityUnavailableByDefault $ do getTeamSearchVisibility tid owner Public.FeatureStatusDisabled getTeamSearchVisibilityInternal tid Public.FeatureStatusDisabled getTeamSearchVisibilityFeatureConfig member Public.FeatureStatusDisabled @@ -370,9 +377,9 @@ testSearchVisibility = do getTeamSearchVisibilityInternal tid Public.FeatureStatusDisabled getTeamSearchVisibilityFeatureConfig member Public.FeatureStatusDisabled - (owner2, tid2, team2member : _) <- Util.createBindingTeamWithNMembers 1 + (owner2, tid2, team2member : _) <- createBindingTeamWithNMembers 1 - Util.withCustomSearchFeature FeatureTeamSearchVisibilityAvailableByDefault $ do + withCustomSearchFeature FeatureTeamSearchVisibilityAvailableByDefault $ do getTeamSearchVisibility tid2 owner2 Public.FeatureStatusEnabled getTeamSearchVisibilityInternal tid2 Public.FeatureStatusEnabled getTeamSearchVisibilityFeatureConfig team2member Public.FeatureStatusEnabled @@ -395,7 +402,7 @@ getClassifiedDomains :: m () getClassifiedDomains member tid = assertFlagWithConfig @Public.ClassifiedDomainsConfig $ - Util.getTeamFeatureFlag @Public.ClassifiedDomainsConfig member tid + getTeamFeatureFlag @Public.ClassifiedDomainsConfig member tid getClassifiedDomainsInternal :: (HasCallStack, HasGalley m, MonadIO m, MonadHttp m, MonadCatch m) => @@ -404,11 +411,11 @@ getClassifiedDomainsInternal :: m () getClassifiedDomainsInternal tid = assertFlagWithConfig @Public.ClassifiedDomainsConfig $ - Util.getTeamFeatureFlagInternal @Public.ClassifiedDomainsConfig tid + getTeamFeatureFlagInternal @Public.ClassifiedDomainsConfig tid testClassifiedDomainsEnabled :: TestM () testClassifiedDomainsEnabled = do - (_owner, tid, member : _) <- Util.createBindingTeamWithNMembers 1 + (_owner, tid, member : _) <- createBindingTeamWithNMembers 1 let expected = Public.WithStatusNoLock Public.FeatureStatusEnabled (Public.ClassifiedDomainsConfig [Domain "example.com"]) Public.FeatureTTLUnlimited @@ -428,7 +435,7 @@ testClassifiedDomainsEnabled = do testClassifiedDomainsDisabled :: TestM () testClassifiedDomainsDisabled = do - (_owner, tid, member : _) <- Util.createBindingTeamWithNMembers 1 + (_owner, tid, member : _) <- createBindingTeamWithNMembers 1 let expected = Public.WithStatusNoLock Public.FeatureStatusDisabled (Public.ClassifiedDomainsConfig []) Public.FeatureTTLUnlimited @@ -442,7 +449,7 @@ testClassifiedDomainsDisabled = do liftIO $ Public.wsStatus result @?= Public.wssStatus expected' liftIO $ Public.wsConfig result @?= Public.wssConfig expected' - let classifiedDomainsDisabled = \opts -> + let classifiedDomainsDisabled opts = opts & over (optSettings . setFeatureFlags . flagClassifiedDomains) @@ -483,26 +490,26 @@ testSimpleFlagTTLOverride :: FeatureTTL -> TestM () testSimpleFlagTTLOverride defaultValue ttl ttlAfter = do - (_owner, tid, member : _) <- Util.createBindingTeamWithNMembers 1 - nonMember <- Util.randomUser + (_owner, tid, member : _) <- createBindingTeamWithNMembers 1 + nonMember <- randomUser let getFlag :: HasCallStack => Public.FeatureStatus -> TestM () - getFlag expected = - flip (assertFlagNoConfig @cfg) expected $ Util.getTeamFeatureFlag @cfg member tid + getFlag expected = eventually $ do + flip (assertFlagNoConfig @cfg) expected $ getTeamFeatureFlag @cfg member tid getFeatureConfig :: HasCallStack => Public.FeatureStatus -> FeatureTTL -> TestM () - getFeatureConfig expectedStatus expectedTtl = do + getFeatureConfig expectedStatus expectedTtl = eventually $ do actual <- Util.getFeatureConfig @cfg member liftIO $ Public.wsStatus actual @?= expectedStatus liftIO $ Public.wsTTL actual @?= expectedTtl getFlagInternal :: HasCallStack => Public.FeatureStatus -> TestM () - getFlagInternal expected = - flip (assertFlagNoConfig @cfg) expected $ Util.getTeamFeatureFlagInternal @cfg tid + getFlagInternal expected = eventually $ do + flip (assertFlagNoConfig @cfg) expected $ getTeamFeatureFlagInternal @cfg tid setFlagInternal :: Public.FeatureStatus -> FeatureTTL -> TestM () setFlagInternal statusValue ttl' = - void $ Util.putTeamFeatureFlagInternalTTL @cfg expect2xx tid (Public.WithStatusNoLock statusValue (Public.trivialConfig @cfg) ttl') + void $ putTeamFeatureFlagInternalTTL @cfg expect2xx tid (Public.WithStatusNoLock statusValue (Public.trivialConfig @cfg) ttl') select :: PrepQuery R (Identity TeamId) (Identity (Maybe FeatureTTL)) select = fromString "select ttl(conference_calling) from team_features where team_id = ?" @@ -529,7 +536,7 @@ testSimpleFlagTTLOverride defaultValue ttl ttlAfter = do toMicros secs = fromIntegral secs * 1000000 - assertFlagForbidden $ Util.getTeamFeatureFlag @cfg nonMember tid + assertFlagForbidden $ getTeamFeatureFlag @cfg nonMember tid let otherValue = case defaultValue of Public.FeatureStatusDisabled -> Public.FeatureStatusEnabled @@ -610,12 +617,12 @@ testSimpleFlagTTL :: FeatureTTL -> TestM () testSimpleFlagTTL defaultValue ttl = do - (_owner, tid, member : _) <- Util.createBindingTeamWithNMembers 1 - nonMember <- Util.randomUser + (_owner, tid, member : _) <- createBindingTeamWithNMembers 1 + nonMember <- randomUser let getFlag :: HasCallStack => Public.FeatureStatus -> TestM () getFlag expected = - flip (assertFlagNoConfig @cfg) expected $ Util.getTeamFeatureFlag @cfg member tid + flip (assertFlagNoConfig @cfg) expected $ getTeamFeatureFlag @cfg member tid getFeatureConfig :: HasCallStack => Public.FeatureStatus -> TestM () getFeatureConfig expected = do @@ -624,11 +631,11 @@ testSimpleFlagTTL defaultValue ttl = do getFlagInternal :: HasCallStack => Public.FeatureStatus -> TestM () getFlagInternal expected = - flip (assertFlagNoConfig @cfg) expected $ Util.getTeamFeatureFlagInternal @cfg tid + flip (assertFlagNoConfig @cfg) expected $ getTeamFeatureFlagInternal @cfg tid setFlagInternal :: Public.FeatureStatus -> FeatureTTL -> TestM () setFlagInternal statusValue ttl' = - void $ Util.putTeamFeatureFlagInternalTTL @cfg expect2xx tid (Public.WithStatusNoLock statusValue (Public.trivialConfig @cfg) ttl') + void $ putTeamFeatureFlagInternalTTL @cfg expect2xx tid (Public.WithStatusNoLock statusValue (Public.trivialConfig @cfg) ttl') select :: PrepQuery R (Identity TeamId) (Identity (Maybe FeatureTTL)) select = fromString "select ttl(conference_calling) from team_features where team_id = ?" @@ -653,7 +660,7 @@ testSimpleFlagTTL defaultValue ttl = do Just (FeatureTTLSeconds i) -> i <= upper unless check $ error ("expected ttl <= " <> show upper <> ", got " <> show storedTTL) - assertFlagForbidden $ Util.getTeamFeatureFlag @cfg nonMember tid + assertFlagForbidden $ getTeamFeatureFlag @cfg nonMember tid let otherValue = case defaultValue of Public.FeatureStatusDisabled -> Public.FeatureStatusEnabled @@ -708,13 +715,13 @@ testSimpleFlagWithLockStatus :: Public.LockStatus -> TestM () testSimpleFlagWithLockStatus defaultStatus defaultLockStatus = do - galley <- view tsGalley - (owner, tid, member : _) <- Util.createBindingTeamWithNMembers 1 - nonMember <- Util.randomUser + galley <- viewGalley + (owner, tid, member : _) <- createBindingTeamWithNMembers 1 + nonMember <- randomUser let getFlag :: HasCallStack => Public.FeatureStatus -> Public.LockStatus -> TestM () getFlag expectedStatus expectedLockStatus = do - let flag = Util.getTeamFeatureFlag @cfg member tid + let flag = getTeamFeatureFlag @cfg member tid assertFlagNoConfigWithLockStatus @cfg flag expectedStatus expectedLockStatus getFeatureConfig :: HasCallStack => Public.FeatureStatus -> Public.LockStatus -> TestM () @@ -725,7 +732,7 @@ testSimpleFlagWithLockStatus defaultStatus defaultLockStatus = do getFlagInternal :: HasCallStack => Public.FeatureStatus -> Public.LockStatus -> TestM () getFlagInternal expectedStatus expectedLockStatus = do - let flag = Util.getTeamFeatureFlagInternal @cfg tid + let flag = getTeamFeatureFlagInternal @cfg tid assertFlagNoConfigWithLockStatus @cfg flag expectedStatus expectedLockStatus getFlags expectedStatus expectedLockStatus = do @@ -735,12 +742,12 @@ testSimpleFlagWithLockStatus defaultStatus defaultLockStatus = do setFlagWithGalley :: Public.FeatureStatus -> TestM () setFlagWithGalley statusValue = - Util.putTeamFeatureFlagWithGalley @cfg galley owner tid (Public.WithStatusNoLock statusValue (Public.trivialConfig @cfg) Public.FeatureTTLUnlimited) + putTeamFeatureFlagWithGalley @cfg galley owner tid (Public.WithStatusNoLock statusValue (Public.trivialConfig @cfg) Public.FeatureTTLUnlimited) !!! statusCode === const 200 assertSetStatusForbidden :: Public.FeatureStatus -> TestM () assertSetStatusForbidden statusValue = - Util.putTeamFeatureFlagWithGalley @cfg galley owner tid (Public.WithStatusNoLock statusValue (Public.trivialConfig @cfg) Public.FeatureTTLUnlimited) + putTeamFeatureFlagWithGalley @cfg galley owner tid (Public.WithStatusNoLock statusValue (Public.trivialConfig @cfg) Public.FeatureTTLUnlimited) !!! statusCode === const 409 setLockStatus :: Public.LockStatus -> TestM () @@ -748,7 +755,7 @@ testSimpleFlagWithLockStatus defaultStatus defaultLockStatus = do Util.setLockStatusInternal @cfg galley tid lockStatus !!! statusCode === const 200 - assertFlagForbidden $ Util.getTeamFeatureFlag @cfg nonMember tid + assertFlagForbidden $ getTeamFeatureFlag @cfg nonMember tid let otherStatus = case defaultStatus of Public.FeatureStatusDisabled -> Public.FeatureStatusEnabled @@ -813,19 +820,19 @@ testSelfDeletingMessages = do (Public.SelfDeletingMessagesConfig tout) Public.FeatureTTLUnlimited - personalUser <- Util.randomUser + personalUser <- randomUser do result <- Util.getFeatureConfig @Public.SelfDeletingMessagesConfig personalUser liftIO $ result @?= settingWithLockStatus FeatureStatusEnabled 0 defLockStatus -- team users - galley <- view tsGalley - (owner, tid, []) <- Util.createBindingTeamWithNMembers 0 + galley <- viewGalley + (owner, tid, []) <- createBindingTeamWithNMembers 0 let checkSet :: FeatureStatus -> Int32 -> Int -> TestM () checkSet stat tout expectedStatusCode = do - Util.putTeamFeatureFlagInternal @Public.SelfDeletingMessagesConfig + putTeamFeatureFlagInternal @Public.SelfDeletingMessagesConfig galley tid (settingWithoutLockStatus stat tout) @@ -836,8 +843,8 @@ testSelfDeletingMessages = do checkGet stat tout lockStatus = do let expected = settingWithLockStatus stat tout lockStatus forM_ - [ Util.getTeamFeatureFlagInternal @Public.SelfDeletingMessagesConfig tid, - Util.getTeamFeatureFlagWithGalley @Public.SelfDeletingMessagesConfig galley owner tid + [ getTeamFeatureFlagInternal @Public.SelfDeletingMessagesConfig tid, + getTeamFeatureFlagWithGalley @Public.SelfDeletingMessagesConfig galley owner tid ] (!!! responseJsonEither === const (Right expected)) result <- Util.getFeatureConfig @Public.SelfDeletingMessagesConfig owner @@ -886,18 +893,18 @@ testSelfDeletingMessages = do testGuestLinksInternal :: TestM () testGuestLinksInternal = do - galley <- view tsGalley + galley <- viewGalley testGuestLinks - (const $ Util.getTeamFeatureFlagInternal @Public.GuestLinksConfig) - (const $ Util.putTeamFeatureFlagInternal @Public.GuestLinksConfig galley) + (const $ getTeamFeatureFlagInternal @Public.GuestLinksConfig) + (const $ putTeamFeatureFlagInternal @Public.GuestLinksConfig galley) (Util.setLockStatusInternal @Public.GuestLinksConfig galley) testGuestLinksPublic :: TestM () testGuestLinksPublic = do - galley <- view tsGalley + galley <- viewGalley testGuestLinks - (Util.getTeamFeatureFlagWithGalley @Public.GuestLinksConfig galley) - (Util.putTeamFeatureFlagWithGalley @Public.GuestLinksConfig galley) + (getTeamFeatureFlagWithGalley @Public.GuestLinksConfig galley) + (putTeamFeatureFlagWithGalley @Public.GuestLinksConfig galley) (Util.setLockStatusInternal @Public.GuestLinksConfig galley) testGuestLinks :: @@ -906,7 +913,7 @@ testGuestLinks :: (TeamId -> Public.LockStatus -> TestM ResponseLBS) -> TestM () testGuestLinks getStatus putStatus setLockStatusInternal = do - (owner, tid, []) <- Util.createBindingTeamWithNMembers 0 + (owner, tid, []) <- createBindingTeamWithNMembers 0 let checkGet :: HasCallStack => Public.FeatureStatus -> Public.LockStatus -> TestM () checkGet status lock = getStatus owner tid !!! do @@ -951,28 +958,28 @@ testAllFeatures = do . to Public.wsLockStatus ) - (_owner, tid, member : _) <- Util.createBindingTeamWithNMembers 1 - Util.getAllTeamFeatures member tid !!! do + (_owner, tid, member : _) <- createBindingTeamWithNMembers 1 + getAllTeamFeatures member tid !!! do statusCode === const 200 responseJsonMaybe === const (Just (expected FeatureStatusEnabled defLockStatus {- determined by default in galley -})) -- This block catches potential errors in the logic that reverts to default if there is a disinction made between -- 1. there is no row for a team_id in galley.team_features -- 2. there is a row for team_id in galley.team_features but the feature has a no entry (null value) - galley <- view tsGalley + galley <- viewGalley -- this sets the guest links config to its default value thereby creating a row for the team in galley.team_features - Util.putTeamFeatureFlagInternal @Public.GuestLinksConfig galley tid (Public.WithStatusNoLock FeatureStatusEnabled Public.GuestLinksConfig Public.FeatureTTLUnlimited) + putTeamFeatureFlagInternal @Public.GuestLinksConfig galley tid (Public.WithStatusNoLock FeatureStatusEnabled Public.GuestLinksConfig Public.FeatureTTLUnlimited) !!! statusCode === const 200 - Util.getAllTeamFeatures member tid !!! do + getAllTeamFeatures member tid !!! do statusCode === const 200 responseJsonMaybe === const (Just (expected FeatureStatusEnabled defLockStatus {- determined by default in galley -})) - Util.getAllTeamFeaturesPersonal member !!! do + getAllTeamFeaturesPersonal member !!! do statusCode === const 200 responseJsonMaybe === const (Just (expected FeatureStatusEnabled defLockStatus {- determined by default in galley -})) - randomPersonalUser <- Util.randomUser - Util.getAllTeamFeaturesPersonal randomPersonalUser !!! do + randomPersonalUser <- randomUser + getAllTeamFeaturesPersonal randomPersonalUser !!! do statusCode === const 200 responseJsonMaybe === const (Just (expected FeatureStatusEnabled defLockStatus {- determined by 'getAfcConferenceCallingDefNew' in brig -})) where @@ -991,16 +998,17 @@ testAllFeatures = do Public.afcGuestLink = Public.withStatus FeatureStatusEnabled Public.LockStatusUnlocked Public.GuestLinksConfig Public.FeatureTTLUnlimited, Public.afcSndFactorPasswordChallenge = Public.withStatus FeatureStatusDisabled Public.LockStatusLocked Public.SndFactorPasswordChallengeConfig Public.FeatureTTLUnlimited, Public.afcMLS = Public.withStatus FeatureStatusDisabled Public.LockStatusUnlocked (Public.MLSConfig [] ProtocolProteusTag [MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519] MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519) Public.FeatureTTLUnlimited, - Public.afcSearchVisibilityInboundConfig = Public.withStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.SearchVisibilityInboundConfig Public.FeatureTTLUnlimited + Public.afcSearchVisibilityInboundConfig = Public.withStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.SearchVisibilityInboundConfig Public.FeatureTTLUnlimited, + Public.afcExposeInvitationURLsToTeamAdmin = Public.withStatus FeatureStatusDisabled Public.LockStatusLocked Public.ExposeInvitationURLsToTeamAdminConfig Public.FeatureTTLUnlimited } testFeatureConfigConsistency :: TestM () testFeatureConfigConsistency = do - (_owner, tid, member : _) <- Util.createBindingTeamWithNMembers 1 + (_owner, tid, member : _) <- createBindingTeamWithNMembers 1 - allFeaturesRes <- Util.getAllFeatureConfigs member >>= parseObjectKeys + allFeaturesRes <- getAllFeatureConfigs member >>= parseObjectKeys - allTeamFeaturesRes <- Util.getAllTeamFeatures member tid >>= parseObjectKeys + allTeamFeaturesRes <- getAllTeamFeatures member tid >>= parseObjectKeys unless (allTeamFeaturesRes `Set.isSubsetOf` allFeaturesRes) $ liftIO $ expectationFailure (show allTeamFeaturesRes <> " is not a subset of " <> show allFeaturesRes) @@ -1017,15 +1025,15 @@ testFeatureConfigConsistency = do testSearchVisibilityInbound :: TestM () testSearchVisibilityInbound = do let defaultValue = FeatureStatusDisabled - (_owner, tid, _) <- Util.createBindingTeamWithNMembers 1 + (_owner, tid, _) <- createBindingTeamWithNMembers 1 let getFlagInternal :: HasCallStack => Public.FeatureStatus -> TestM () getFlagInternal expected = - flip (assertFlagNoConfig @Public.SearchVisibilityInboundConfig) expected $ Util.getTeamFeatureFlagInternal @Public.SearchVisibilityInboundConfig tid + flip (assertFlagNoConfig @Public.SearchVisibilityInboundConfig) expected $ getTeamFeatureFlagInternal @Public.SearchVisibilityInboundConfig tid setFlagInternal :: Public.FeatureStatus -> TestM () setFlagInternal statusValue = - void $ Util.putTeamFeatureFlagInternal @Public.SearchVisibilityInboundConfig expect2xx tid (Public.WithStatusNoLock statusValue Public.SearchVisibilityInboundConfig Public.FeatureTTLUnlimited) + void $ putTeamFeatureFlagInternal @Public.SearchVisibilityInboundConfig expect2xx tid (Public.WithStatusNoLock statusValue Public.SearchVisibilityInboundConfig Public.FeatureTTLUnlimited) let otherValue = case defaultValue of Public.FeatureStatusDisabled -> Public.FeatureStatusEnabled @@ -1038,12 +1046,12 @@ testSearchVisibilityInbound = do testFeatureNoConfigMultiSearchVisibilityInbound :: TestM () testFeatureNoConfigMultiSearchVisibilityInbound = do - (_owner1, team1, _) <- Util.createBindingTeamWithNMembers 0 - (_owner2, team2, _) <- Util.createBindingTeamWithNMembers 0 + (_owner1, team1, _) <- createBindingTeamWithNMembers 0 + (_owner2, team2, _) <- createBindingTeamWithNMembers 0 let setFlagInternal :: TeamId -> Public.FeatureStatus -> TestM () setFlagInternal tid statusValue = - void $ Util.putTeamFeatureFlagInternal @Public.SearchVisibilityInboundConfig expect2xx tid (Public.WithStatusNoLock statusValue Public.SearchVisibilityInboundConfig Public.FeatureTTLUnlimited) + void $ putTeamFeatureFlagInternal @Public.SearchVisibilityInboundConfig expect2xx tid (Public.WithStatusNoLock statusValue Public.SearchVisibilityInboundConfig Public.FeatureTTLUnlimited) setFlagInternal team2 Public.FeatureStatusEnabled @@ -1056,26 +1064,26 @@ testFeatureNoConfigMultiSearchVisibilityInbound = do liftIO $ do length teamsStatuses @?= 2 - Multi.TeamStatus _ team1Status <- Util.assertOne (filter ((== team1) . Multi.team) teamsStatuses) + Multi.TeamStatus _ team1Status <- assertOne (filter ((== team1) . Multi.team) teamsStatuses) team1Status @?= Public.FeatureStatusDisabled - Multi.TeamStatus _ team2Status <- Util.assertOne (filter ((== team2) . Multi.team) teamsStatuses) + Multi.TeamStatus _ team2Status <- assertOne (filter ((== team2) . Multi.team) teamsStatuses) team2Status @?= Public.FeatureStatusEnabled testMLS :: TestM () testMLS = do - (owner, tid, member : _) <- Util.createBindingTeamWithNMembers 1 + (owner, tid, member : _) <- createBindingTeamWithNMembers 1 - galley <- view tsGalley + galley <- viewGalley cannon <- view tsCannon let getForTeam :: HasCallStack => Public.WithStatusNoLock MLSConfig -> TestM () getForTeam expected = - flip assertFlagWithConfig expected $ Util.getTeamFeatureFlag @MLSConfig member tid + flip assertFlagWithConfig expected $ getTeamFeatureFlag @MLSConfig member tid getForTeamInternal :: HasCallStack => Public.WithStatusNoLock MLSConfig -> TestM () getForTeamInternal expected = - flip assertFlagWithConfig expected $ Util.getTeamFeatureFlagInternal @Public.MLSConfig tid + flip assertFlagWithConfig expected $ getTeamFeatureFlagInternal @Public.MLSConfig tid getForUser :: HasCallStack => Public.WithStatusNoLock MLSConfig -> TestM () getForUser expected = do @@ -1091,12 +1099,12 @@ testMLS = do setForTeam :: HasCallStack => Public.WithStatusNoLock MLSConfig -> TestM () setForTeam wsnl = - Util.putTeamFeatureFlagWithGalley @MLSConfig galley owner tid wsnl + putTeamFeatureFlagWithGalley @MLSConfig galley owner tid wsnl !!! statusCode === const 200 setForTeamInternal :: HasCallStack => Public.WithStatusNoLock MLSConfig -> TestM () setForTeamInternal wsnl = - void $ Util.putTeamFeatureFlagInternal @Public.MLSConfig expect2xx tid wsnl + void $ putTeamFeatureFlagInternal @Public.MLSConfig expect2xx tid wsnl let cipherSuite = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 let defaultConfig = @@ -1131,6 +1139,73 @@ testMLS = do wsAssertFeatureConfigUpdate @MLSConfig config3 LockStatusUnlocked getViaEndpoints config3 +testExposeInvitationURLsToTeamAdminTeamIdInAllowList :: TestM () +testExposeInvitationURLsToTeamAdminTeamIdInAllowList = do + owner <- randomUser + tid <- createBindingTeamInternal "foo" owner + assertQueue "create team" tActivate + void $ + withSettingsOverrides (\opts -> opts & optSettings . setExposeInvitationURLsTeamAllowlist ?~ [tid]) $ do + g <- viewGalley + assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusDisabled Public.LockStatusUnlocked + let enabled = Public.WithStatusNoLock Public.FeatureStatusEnabled ExposeInvitationURLsToTeamAdminConfig Public.FeatureTTLUnlimited + void $ + putTeamFeatureFlagWithGalley @ExposeInvitationURLsToTeamAdminConfig g owner tid enabled !!! do + const 200 === statusCode + assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusEnabled Public.LockStatusUnlocked + +testExposeInvitationURLsToTeamAdminEmptyAllowList :: TestM () +testExposeInvitationURLsToTeamAdminEmptyAllowList = do + owner <- randomUser + tid <- createBindingTeamInternal "foo" owner + assertQueue "create team" tActivate + void $ + withSettingsOverrides (\opts -> opts & optSettings . setExposeInvitationURLsTeamAllowlist .~ Nothing) $ do + g <- viewGalley + assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusDisabled Public.LockStatusLocked + let enabled = Public.WithStatusNoLock Public.FeatureStatusEnabled ExposeInvitationURLsToTeamAdminConfig Public.FeatureTTLUnlimited + void $ + putTeamFeatureFlagWithGalley @ExposeInvitationURLsToTeamAdminConfig g owner tid enabled !!! do + const 409 === statusCode + assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusDisabled Public.LockStatusLocked + +-- | Ensure that the server config takes precedence over a saved team config. +-- +-- In other words: When a team id is no longer in the +-- `setExposeInvitationURLsTeamAllowlist` the +-- `ExposeInvitationURLsToTeamAdminConfig` is always disabled (even tough it +-- might have been enabled before). +testExposeInvitationURLsToTeamAdminServerConfigTakesPrecedence :: TestM () +testExposeInvitationURLsToTeamAdminServerConfigTakesPrecedence = do + owner <- randomUser + tid <- createBindingTeamInternal "foo" owner + assertQueue "create team" tActivate + void $ + withSettingsOverrides (\opts -> opts & optSettings . setExposeInvitationURLsTeamAllowlist ?~ [tid]) $ do + g <- viewGalley + assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusDisabled Public.LockStatusUnlocked + let enabled = Public.WithStatusNoLock Public.FeatureStatusEnabled ExposeInvitationURLsToTeamAdminConfig Public.FeatureTTLUnlimited + void $ + putTeamFeatureFlagWithGalley @ExposeInvitationURLsToTeamAdminConfig g owner tid enabled !!! do + const 200 === statusCode + assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusEnabled Public.LockStatusUnlocked + void $ + withSettingsOverrides (\opts -> opts & optSettings . setExposeInvitationURLsTeamAllowlist .~ Nothing) $ do + g <- viewGalley + assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusDisabled Public.LockStatusLocked + let enabled = Public.WithStatusNoLock Public.FeatureStatusEnabled ExposeInvitationURLsToTeamAdminConfig Public.FeatureTTLUnlimited + void $ + putTeamFeatureFlagWithGalley @ExposeInvitationURLsToTeamAdminConfig g owner tid enabled !!! do + const 409 === statusCode + assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusDisabled Public.LockStatusLocked + +assertExposeInvitationURLsToTeamAdminConfigStatus :: UserId -> TeamId -> FeatureStatus -> LockStatus -> TestM () +assertExposeInvitationURLsToTeamAdminConfigStatus owner tid fStatus lStatus = do + g <- viewGalley + Util.getTeamFeatureFlagWithGalley @ExposeInvitationURLsToTeamAdminConfig g owner tid !!! do + const 200 === statusCode + const (Right (Public.withStatus fStatus lStatus Public.ExposeInvitationURLsToTeamAdminConfig Public.FeatureTTLUnlimited)) === responseJsonEither + assertFlagForbidden :: HasCallStack => TestM ResponseLBS -> TestM () assertFlagForbidden res = do res !!! do diff --git a/services/galley/test/integration/API/Teams/LegalHold.hs b/services/galley/test/integration/API/Teams/LegalHold.hs index 63ec84cfe7..d4dde55c62 100644 --- a/services/galley/test/integration/API/Teams/LegalHold.hs +++ b/services/galley/test/integration/API/Teams/LegalHold.hs @@ -36,7 +36,7 @@ import qualified Control.Concurrent.Async as Async import Control.Concurrent.Chan import Control.Concurrent.Timeout hiding (threadDelay) import Control.Exception (asyncExceptionFromException) -import Control.Lens +import Control.Lens hiding ((#)) import Control.Monad.Catch import Control.Retry (RetryPolicy, RetryStatus, exponentialBackoff, limitRetries, retrying) import qualified Data.Aeson as Aeson @@ -59,6 +59,7 @@ import qualified Data.Set as Set import Data.String.Conversions (LBS, cs) import Data.Text.Encoding (encodeUtf8) import qualified Data.Time.Clock as Time +import Data.Timeout import Galley.Cassandra.Client import Galley.Cassandra.LegalHold import qualified Galley.Cassandra.LegalHold as LegalHoldData @@ -149,10 +150,13 @@ testsPublic s = "teams listed" [ test s "happy flow" testInWhitelist, test s "handshake between LH device and user with old clients is blocked" testOldClientsBlockDeviceHandshake, - testGroup "no-consent" $ - flip fmap [(a, b, c, d) | a <- [minBound ..], b <- [minBound ..], c <- [minBound ..], d <- [minBound ..]] $ - \args@(a, b, c, d) -> - test s (show args) $ testNoConsentBlockOne2OneConv a b c d, + testGroup "no-consent" $ do + connectFirst <- ("connectFirst",) <$> [False, True] + teamPeer <- ("teamPeer",) <$> [False, True] + approveLH <- ("approveLH",) <$> [False, True] + testPendingConnection <- ("testPendingConnection",) <$> [False, True] + let name = intercalate ", " $ map (\(n, b) -> n <> "=" <> show b) [connectFirst, teamPeer, approveLH, testPendingConnection] + pure . test s name $ testNoConsentBlockOne2OneConv (snd connectFirst) (snd teamPeer) (snd approveLH) (snd testPendingConnection), testGroup "Legalhold is activated for user A in a group conversation" [ test s "All admins are consenting: all non-consenters get removed from conversation" (onlyIfLhWhitelisted (testNoConsentRemoveFromGroupConv LegalholderIsAdmin)), @@ -617,7 +621,7 @@ testCannotCreateLegalHoldDeviceOldAPI = do where tryout :: UserId -> TestM () tryout uid = do - brg <- view tsBrig + brg <- viewBrig let newClientBody = (newClient LegalHoldClientType (head someLastPrekeys)) { newClientPassword = Just defPassword @@ -667,7 +671,7 @@ testGetTeamMembersIncludesLHStatus = do testInWhitelist :: TestM () testInWhitelist = do - g <- view tsGalley + g <- viewGalley (owner, tid) <- createBindingTeam member <- randomUser addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing @@ -822,7 +826,7 @@ testNoConsentBlockOne2OneConv connectFirst teamPeer approveLH testPendingConnect regularClient <- randomClient legalholder (head someLastPrekeys) peer :: UserId <- if teamPeer then fst <$> createBindingTeam else randomUser - galley <- view tsGalley + galley <- viewGalley putLHWhitelistTeam tid !!! const 200 === statusCode @@ -908,6 +912,12 @@ testNoConsentBlockOne2OneConv connectFirst teamPeer approveLH testPendingConnect do doDisableLH + + when approveLH $ do + legalholderLHDevice <- assertJust mbLegalholderLHDevice + WS.assertMatch_ (5 # Second) legalholderWs $ + wsAssertClientRemoved legalholderLHDevice + assertConnections legalholder [ ConnectionStatus legalholder peer $ @@ -956,7 +966,7 @@ testNoConsentRemoveFromGroupConv whoIsAdmin = do qLegalHolder <- Qualified legalholder <$> viewFederationDomain (peer :: UserId, teamPeer) <- createBindingTeam qPeer <- Qualified peer <$> viewFederationDomain - galley <- view tsGalley + galley <- viewGalley let enableLHForLegalholder :: HasCallStack => TestM () enableLHForLegalholder = do @@ -1048,7 +1058,7 @@ testGroupConvInvitationHandlesLHConflicts inviteCase = do -- activate legalhold for legalholder do - galley <- view tsGalley + galley <- viewGalley requestLegalHoldDevice legalholder legalholder tid !!! testResponse 201 Nothing approveLegalHoldDevice (Just defPassword) legalholder legalholder tid !!! testResponse 200 Nothing UserLegalHoldStatusResponse userStatus _ _ <- getUserStatusTyped' galley legalholder tid @@ -1098,7 +1108,7 @@ testNoConsentCannotBeInvited = do -- activate legalhold for legalholder do - galley <- view tsGalley + galley <- viewGalley requestLegalHoldDevice legalholder legalholder tid !!! testResponse 201 Nothing approveLegalHoldDevice (Just defPassword) legalholder legalholder tid !!! testResponse 200 Nothing UserLegalHoldStatusResponse userStatus _ _ <- getUserStatusTyped' galley legalholder tid @@ -1137,7 +1147,7 @@ testCannotCreateGroupWithUsersInConflict = do -- activate legalhold for legalholder do - galley <- view tsGalley + galley <- viewGalley requestLegalHoldDevice legalholder legalholder tid !!! testResponse 201 Nothing approveLegalHoldDevice (Just defPassword) legalholder legalholder tid !!! testResponse 200 Nothing UserLegalHoldStatusResponse userStatus _ _ <- getUserStatusTyped' galley legalholder tid @@ -1257,7 +1267,7 @@ testBenchHack' :: HasCallStack => Int -> TestM (Int, Time.NominalDiffTime) testBenchHack' numPeers = do (legalholder :: UserId, tid) <- createBindingTeam peers :: [UserId] <- replicateM numPeers randomUser - galley <- view tsGalley + galley <- viewGalley let doEnableLH :: HasCallStack => TestM () doEnableLH = do @@ -1295,14 +1305,14 @@ testBenchHack' numPeers = do getEnabled :: HasCallStack => TeamId -> TestM ResponseLBS getEnabled tid = do - g <- view tsGalley + g <- viewGalley get $ g . paths ["i", "teams", toByteString' tid, "features", "legalhold"] renewToken :: HasCallStack => Text -> TestM () renewToken tok = do - b <- view tsBrig + b <- viewBrig void . post $ b . paths ["access"] @@ -1311,7 +1321,7 @@ renewToken tok = do _putEnabled :: HasCallStack => TeamId -> Public.FeatureStatus -> TestM () _putEnabled tid enabled = do - g <- view tsGalley + g <- viewGalley putEnabledM g tid enabled putEnabledM :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyR -> TeamId -> Public.FeatureStatus -> m () @@ -1319,7 +1329,7 @@ putEnabledM g tid enabled = void $ putEnabledM' g expect2xx tid enabled putEnabled' :: HasCallStack => (Bilge.Request -> Bilge.Request) -> TeamId -> Public.FeatureStatus -> TestM ResponseLBS putEnabled' extra tid enabled = do - g <- view tsGalley + g <- viewGalley putEnabledM' g extra tid enabled putEnabledM' :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyR -> (Bilge.Request -> Bilge.Request) -> TeamId -> Public.FeatureStatus -> m ResponseLBS @@ -1335,7 +1345,7 @@ postSettings uid tid new = -- Retry calls to this endpoint, on k8s it sometimes takes a while to establish a working -- connection. retrying policy only412 $ \_ -> do - g <- view tsGalley + g <- viewGalley post $ g . paths ["teams", toByteString' tid, "legalhold", "settings"] @@ -1354,7 +1364,7 @@ getSettingsTyped uid tid = responseJsonUnsafe <$> (getSettings uid tid UserId -> TeamId -> TestM ResponseLBS getSettings uid tid = do - g <- view tsGalley + g <- viewGalley get $ g . paths ["teams", toByteString' tid, "legalhold", "settings"] @@ -1364,7 +1374,7 @@ getSettings uid tid = do deleteSettings :: HasCallStack => Maybe PlainTextPassword -> UserId -> TeamId -> TestM ResponseLBS deleteSettings mPassword uid tid = do - g <- view tsGalley + g <- viewGalley delete $ g . paths ["teams", toByteString' tid, "legalhold", "settings"] @@ -1375,7 +1385,7 @@ deleteSettings mPassword uid tid = do getUserStatusTyped :: HasCallStack => UserId -> TeamId -> TestM UserLegalHoldStatusResponse getUserStatusTyped uid tid = do - g <- view tsGalley + g <- viewGalley getUserStatusTyped' g uid tid getUserStatusTyped' :: (HasCallStack, MonadHttp m, MonadIO m, MonadCatch m) => GalleyR -> UserId -> TeamId -> m UserLegalHoldStatusResponse @@ -1394,7 +1404,7 @@ getUserStatus' g uid tid = do approveLegalHoldDevice :: HasCallStack => Maybe PlainTextPassword -> UserId -> UserId -> TeamId -> TestM ResponseLBS approveLegalHoldDevice mPassword zusr uid tid = do - g <- view tsGalley + g <- viewGalley approveLegalHoldDevice' g mPassword zusr uid tid approveLegalHoldDevice' :: @@ -1422,7 +1432,7 @@ disableLegalHoldForUser :: UserId -> TestM ResponseLBS disableLegalHoldForUser mPassword tid zusr uid = do - g <- view tsGalley + g <- viewGalley disableLegalHoldForUser' g mPassword tid zusr uid disableLegalHoldForUser' :: @@ -1466,7 +1476,7 @@ assertZeroLegalHoldDevices uid = do requestLegalHoldDevice :: HasCallStack => UserId -> UserId -> TeamId -> TestM ResponseLBS requestLegalHoldDevice zusr uid tid = do - g <- view tsGalley + g <- viewGalley requestLegalHoldDevice' g zusr uid tid requestLegalHoldDevice' :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyR -> UserId -> UserId -> TeamId -> m ResponseLBS @@ -1804,7 +1814,7 @@ assertMatchChan c match = go [] getLHWhitelistedTeam :: HasCallStack => TeamId -> TestM ResponseLBS getLHWhitelistedTeam tid = do - galley <- view tsGalley + galley <- viewGalley getLHWhitelistedTeam' galley tid getLHWhitelistedTeam' :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyR -> TeamId -> m ResponseLBS @@ -1816,7 +1826,7 @@ getLHWhitelistedTeam' g tid = do putLHWhitelistTeam :: HasCallStack => TeamId -> TestM ResponseLBS putLHWhitelistTeam tid = do - galley <- view tsGalley + galley <- viewGalley putLHWhitelistTeam' galley tid putLHWhitelistTeam' :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyR -> TeamId -> m ResponseLBS @@ -1828,7 +1838,7 @@ putLHWhitelistTeam' g tid = do _deleteLHWhitelistTeam :: HasCallStack => TeamId -> TestM ResponseLBS _deleteLHWhitelistTeam tid = do - galley <- view tsGalley + galley <- viewGalley deleteLHWhitelistTeam' galley tid deleteLHWhitelistTeam' :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyR -> TeamId -> m ResponseLBS diff --git a/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs b/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs index f8e390259c..677895ad58 100644 --- a/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs +++ b/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs @@ -633,7 +633,7 @@ testCannotCreateLegalHoldDeviceOldAPI = do where tryout :: UserId -> TestM () tryout uid = do - brg <- view tsBrig + brg <- viewBrig let newClientBody = (newClient LegalHoldClientType (head someLastPrekeys)) { newClientPassword = Just defPassword @@ -841,14 +841,14 @@ testClaimKeys testcase = do getEnabled :: HasCallStack => TeamId -> TestM ResponseLBS getEnabled tid = do - g <- view tsGalley + g <- viewGalley get $ g . paths ["i", "teams", toByteString' tid, "features", "legalhold"] renewToken :: HasCallStack => Text -> TestM () renewToken tok = do - b <- view tsBrig + b <- viewBrig void . post $ b . paths ["access"] @@ -857,7 +857,7 @@ renewToken tok = do putEnabled :: HasCallStack => TeamId -> Public.FeatureStatus -> TestM () putEnabled tid enabled = do - g <- view tsGalley + g <- viewGalley putEnabledM g tid enabled putEnabledM :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyR -> TeamId -> Public.FeatureStatus -> m () @@ -865,7 +865,7 @@ putEnabledM g tid enabled = void $ putEnabledM' g expect2xx tid enabled putEnabled' :: HasCallStack => (Bilge.Request -> Bilge.Request) -> TeamId -> Public.FeatureStatus -> TestM ResponseLBS putEnabled' extra tid enabled = do - g <- view tsGalley + g <- viewGalley putEnabledM' g extra tid enabled putEnabledM' :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyR -> (Bilge.Request -> Bilge.Request) -> TeamId -> Public.FeatureStatus -> m ResponseLBS @@ -881,7 +881,7 @@ postSettings uid tid new = -- Retry calls to this endpoint, on k8s it sometimes takes a while to establish a working -- connection. retrying policy only412 $ \_ -> do - g <- view tsGalley + g <- viewGalley post $ g . paths ["teams", toByteString' tid, "legalhold", "settings"] @@ -900,7 +900,7 @@ getSettingsTyped uid tid = responseJsonUnsafe <$> (getSettings uid tid UserId -> TeamId -> TestM ResponseLBS getSettings uid tid = do - g <- view tsGalley + g <- viewGalley get $ g . paths ["teams", toByteString' tid, "legalhold", "settings"] @@ -910,7 +910,7 @@ getSettings uid tid = do deleteSettings :: HasCallStack => Maybe PlainTextPassword -> UserId -> TeamId -> TestM ResponseLBS deleteSettings mPassword uid tid = do - g <- view tsGalley + g <- viewGalley delete $ g . paths ["teams", toByteString' tid, "legalhold", "settings"] @@ -921,7 +921,7 @@ deleteSettings mPassword uid tid = do getUserStatusTyped :: HasCallStack => UserId -> TeamId -> TestM UserLegalHoldStatusResponse getUserStatusTyped uid tid = do - g <- view tsGalley + g <- viewGalley getUserStatusTyped' g uid tid getUserStatusTyped' :: (HasCallStack, MonadHttp m, MonadIO m, MonadCatch m) => GalleyR -> UserId -> TeamId -> m UserLegalHoldStatusResponse @@ -940,7 +940,7 @@ getUserStatus' g uid tid = do approveLegalHoldDevice :: HasCallStack => Maybe PlainTextPassword -> UserId -> UserId -> TeamId -> TestM ResponseLBS approveLegalHoldDevice mPassword zusr uid tid = do - g <- view tsGalley + g <- viewGalley approveLegalHoldDevice' g mPassword zusr uid tid approveLegalHoldDevice' :: @@ -968,7 +968,7 @@ disableLegalHoldForUser :: UserId -> TestM ResponseLBS disableLegalHoldForUser mPassword tid zusr uid = do - g <- view tsGalley + g <- viewGalley disableLegalHoldForUser' g mPassword tid zusr uid disableLegalHoldForUser' :: @@ -1012,7 +1012,7 @@ assertZeroLegalHoldDevices uid = do grantConsent :: HasCallStack => TeamId -> UserId -> TestM () grantConsent tid zusr = do - g <- view tsGalley + g <- viewGalley grantConsent' g tid zusr grantConsent' :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyR -> TeamId -> UserId -> m () @@ -1030,7 +1030,7 @@ grantConsent'' expectation g tid zusr = do requestLegalHoldDevice :: HasCallStack => UserId -> UserId -> TeamId -> TestM ResponseLBS requestLegalHoldDevice zusr uid tid = do - g <- view tsGalley + g <- viewGalley requestLegalHoldDevice' g zusr uid tid requestLegalHoldDevice' :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyR -> UserId -> UserId -> TeamId -> m ResponseLBS diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 4556a34a1b..5664206d25 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -26,7 +26,7 @@ import Brig.Types.Connection import Brig.Types.Intra (UserAccount (..)) import Control.Concurrent.Async import Control.Exception (throw) -import Control.Lens hiding (from, to, (#), (.=)) +import Control.Lens hiding (from, to, uncons, (#), (.=)) import Control.Monad.Catch (MonadCatch, MonadMask) import Control.Monad.Codensity (lowerCodensity) import Control.Monad.Except (ExceptT, runExceptT) @@ -34,6 +34,7 @@ import Control.Retry (constantDelay, exponentialBackoff, limitRetries, retrying) import Data.Aeson hiding (json) import Data.Aeson.Lens (key, _String) import qualified Data.ByteString as BS +import qualified Data.ByteString.Char8 as B8 import qualified Data.ByteString.Char8 as C import Data.ByteString.Conversion import qualified Data.ByteString.Lazy as Lazy @@ -54,7 +55,7 @@ import qualified Data.Map.Strict as Map import Data.Misc import qualified Data.ProtoLens as Protolens import Data.ProtocolBuffers (encodeMessage) -import Data.Qualified +import Data.Qualified hiding (isLocal) import Data.Range import Data.Serialize (runPut) import qualified Data.Set as Set @@ -79,13 +80,14 @@ import qualified Galley.Types.Teams as Team import Galley.Types.Teams.Intra import Galley.Types.UserList import Imports +import qualified Network.HTTP.Client as HTTP import Network.HTTP.Media.MediaType import qualified Network.HTTP.Types as HTTP -import Network.Wai (Application, defaultRequest) +import Network.Wai (defaultRequest) import qualified Network.Wai as Wai import qualified Network.Wai.Test as Wai import Network.Wai.Utilities.MockServer (withMockServer) -import Servant (Handler, HasServer, Server, ServerT, serve, (:<|>) (..)) +import Servant import System.Exit import System.Process import System.Random @@ -111,6 +113,9 @@ import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Domain (originDomainHeaderName) import Wire.API.Internal.Notification hiding (target) +import Wire.API.MLS.KeyPackage +import Wire.API.MLS.Message +import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation import Wire.API.Message import qualified Wire.API.Message.Proto as Proto @@ -134,19 +139,34 @@ import Wire.API.User.Client.Prekey ------------------------------------------------------------------------------- -- API Operations +addPrefix :: Request -> Request +addPrefix r = r {HTTP.path = "v" <> toHeader latestVersion <> "/" <> removeSlash (HTTP.path r)} + where + removeSlash s = case B8.uncons s of + Just ('/', s') -> s' + _ -> s + latestVersion :: Version + latestVersion = maxBound + -- | A class for monads with access to a Sem r instance class HasGalley m where viewGalley :: m GalleyR viewGalleyOpts :: m Opts.Opts instance HasGalley TestM where - viewGalley = view tsGalley + viewGalley = fmap (addPrefix .) (view tsUnversionedGalley) viewGalleyOpts = view tsGConf instance (HasGalley m, Monad m) => HasGalley (SessionT m) where viewGalley = lift viewGalley viewGalleyOpts = lift viewGalleyOpts +class HasBrig m where + viewBrig :: m BrigR + +instance HasBrig TestM where + viewBrig = fmap (addPrefix .) (view tsUnversionedBrig) + symmPermissions :: [Perm] -> Permissions symmPermissions p = let s = Set.fromList p in fromJust (newPermissions s s) @@ -186,7 +206,7 @@ createBindingTeamWithQualifiedMembers num = do getTeams :: UserId -> [(ByteString, Maybe ByteString)] -> TestM TeamList getTeams u queryItems = do - g <- view tsGalley + g <- viewGalley r <- get ( g @@ -221,7 +241,7 @@ createBindingTeamWithNMembersWithHandles withHandles n = do setHandle :: UserId -> TestM () setHandle uid = when withHandles $ do - b <- view tsBrig + b <- viewBrig randomHandle <- mkRandomHandle put ( b @@ -233,7 +253,7 @@ createBindingTeamWithNMembersWithHandles withHandles n = do changeTeamStatus :: HasCallStack => TeamId -> TeamStatus -> TestM () changeTeamStatus tid s = do - g <- view tsGalley + g <- viewGalley put ( g . paths ["i", "teams", toByteString' tid, "status"] . json (TeamStatusUpdate s Nothing) @@ -249,7 +269,7 @@ createBindingTeamInternal name owner = do createBindingTeamInternalNoActivate :: HasCallStack => Text -> UserId -> TestM TeamId createBindingTeamInternalNoActivate name owner = do - g <- view tsGalley + g <- viewGalley tid <- randomId let nt = BindingNewTeam $ newNewTeam (unsafeRange name) DefaultIcon _ <- @@ -260,7 +280,7 @@ createBindingTeamInternalNoActivate name owner = do createBindingTeamInternalWithCurrency :: HasCallStack => Text -> UserId -> Currency.Alpha -> TestM TeamId createBindingTeamInternalWithCurrency name owner cur = do - g <- view tsGalley + g <- viewGalley tid <- createBindingTeamInternalNoActivate name owner _ <- put (g . paths ["i", "teams", toByteString' tid, "status"] . json (TeamStatusUpdate Active $ Just cur)) @@ -269,39 +289,39 @@ createBindingTeamInternalWithCurrency name owner cur = do getTeamInternal :: HasCallStack => TeamId -> TestM TeamData getTeamInternal tid = do - g <- view tsGalley + g <- viewGalley r <- get (g . paths ["i/teams", toByteString' tid]) UserId -> TeamId -> TestM Team getTeam usr tid = do - g <- view tsGalley + g <- viewGalley r <- get (g . paths ["teams", toByteString' tid] . zUser usr) UserId -> TeamId -> TestM TeamMemberList getTeamMembers usr tid = do - g <- view tsGalley + g <- viewGalley r <- get (g . paths ["teams", toByteString' tid, "members"] . zUser usr) UserId -> TeamId -> TestM ResponseLBS getTeamMembersCsv usr tid = do - g <- view tsGalley + g <- viewGalley get (g . accept "text/csv" . paths ["teams", toByteString' tid, "members/csv"] . zUser usr) UserId -> TeamId -> Int -> TestM TeamMemberList getTeamMembersTruncated usr tid n = do - g <- view tsGalley + g <- viewGalley r <- get (g . paths ["teams", toByteString' tid, "members"] . zUser usr . queryItem "maxResults" (C.pack $ show n)) TeamId -> Int -> TestM TeamMemberList getTeamMembersInternalTruncated tid n = do - g <- view tsGalley + g <- viewGalley r <- get ( g @@ -314,7 +334,7 @@ getTeamMembersInternalTruncated tid n = do bulkGetTeamMembers :: HasCallStack => UserId -> TeamId -> [UserId] -> TestM TeamMemberList bulkGetTeamMembers usr tid uids = do - g <- view tsGalley + g <- viewGalley r <- post ( g @@ -328,7 +348,7 @@ bulkGetTeamMembers usr tid uids = do bulkGetTeamMembersTruncated :: HasCallStack => UserId -> TeamId -> [UserId] -> Int -> TestM ResponseLBS bulkGetTeamMembersTruncated usr tid uids trnc = do - g <- view tsGalley + g <- viewGalley post ( g . paths ["teams", toByteString' tid, "get-members-by-ids-using-post"] @@ -339,7 +359,7 @@ bulkGetTeamMembersTruncated usr tid uids trnc = do getTeamMember :: HasCallStack => UserId -> TeamId -> UserId -> TestM TeamMember getTeamMember getter tid gettee = do - g <- view tsGalley + g <- viewGalley getTeamMember' g getter tid gettee getTeamMember' :: (HasCallStack, MonadHttp m, MonadIO m, MonadCatch m) => GalleyR -> UserId -> TeamId -> UserId -> m TeamMember @@ -349,13 +369,13 @@ getTeamMember' g getter tid gettee = do getTeamMemberInternal :: HasCallStack => TeamId -> UserId -> TestM TeamMember getTeamMemberInternal tid mid = do - g <- view tsGalley + g <- viewGalley r <- get (g . paths ["i", "teams", toByteString' tid, "members", toByteString' mid]) UserId -> TeamId -> UserId -> Permissions -> Maybe (UserId, UTCTimeMillis) -> TestM () addTeamMember usr tid muid mperms mmbinv = do - g <- view tsGalley + g <- viewGalley let payload = json (mkNewTeamMember muid mperms mmbinv) post (g . paths ["teams", toByteString' tid, "members"] . zUser usr . zConn "conn" . payload) !!! const 200 === statusCode @@ -367,7 +387,7 @@ addTeamMemberInternal tid muid mperms mmbinv = addTeamMemberInternal' tid muid m -- | FUTUREWORK: do not use this, it's broken!! use 'addUserToTeam' instead! https://wearezeta.atlassian.net/browse/SQSERVICES-471 addTeamMemberInternal' :: HasCallStack => TeamId -> UserId -> Permissions -> Maybe (UserId, UTCTimeMillis) -> TestM ResponseLBS addTeamMemberInternal' tid muid mperms mmbinv = do - g <- view tsGalley + g <- viewGalley let payload = json (mkNewTeamMember muid mperms mmbinv) post (g . paths ["i", "teams", toByteString' tid, "members"] . payload) @@ -391,7 +411,7 @@ addUserToTeamWithRole role inviter tid = do addUserToTeamWithRole' :: HasCallStack => Maybe Role -> UserId -> TeamId -> TestM (Invitation, ResponseLBS) addUserToTeamWithRole' role inviter tid = do - brig <- view tsBrig + brig <- viewBrig inviteeEmail <- randomEmail let invite = InvitationRequest Nothing role Nothing inviteeEmail Nothing invResponse <- postInvitation tid inviter invite @@ -414,7 +434,7 @@ addUserToTeamWithSSO hasEmail tid = do makeOwner :: HasCallStack => UserId -> TeamMember -> TeamId -> TestM () makeOwner owner mem tid = do - galley <- view tsGalley + galley <- viewGalley let changeMember = mkNewTeamMember (mem ^. Team.userId) fullPermissions (mem ^. Team.invitation) put ( galley @@ -438,7 +458,7 @@ acceptInviteBody email code = postInvitation :: TeamId -> UserId -> InvitationRequest -> TestM ResponseLBS postInvitation t u i = do - brig <- view tsBrig + brig <- viewBrig post $ brig . paths ["teams", toByteString' t, "invitations"] @@ -454,7 +474,7 @@ zAuthAccess u conn = getInvitationCode :: HasCallStack => TeamId -> InvitationId -> TestM InvitationCode getInvitationCode t ref = do - brig <- view tsBrig + brig <- viewBrig let getm :: TestM (Maybe InvitationCode) getm = do @@ -480,7 +500,7 @@ getInvitationCode t ref = do -- it clearly shows the API that old(er) clients use. createTeamConvLegacy :: HasCallStack => UserId -> TeamId -> [UserId] -> Maybe Text -> TestM ConvId createTeamConvLegacy u tid us name = do - g <- view tsGalley + g <- viewGalley let tinfo = ConvTeamInfo tid let convPayload = object @@ -547,7 +567,7 @@ createTeamConvAccessRaw :: Maybe RoleName -> TestM ResponseLBS createTeamConvAccessRaw u tid us name acc role mtimer convRole = do - g <- view tsGalley + g <- viewGalley let tinfo = ConvTeamInfo tid let conv = NewConv us [] (name >>= checked) (fromMaybe (Set.fromList []) acc) role (Just tinfo) mtimer Nothing (fromMaybe roleNameWireAdmin convRole) ProtocolProteusTag Nothing @@ -603,7 +623,7 @@ createMLSTeamConv lusr c tid users name access role timer convRole = do updateTeamConv :: UserId -> ConvId -> ConversationRename -> TestM ResponseLBS updateTeamConv zusr convid upd = do - g <- view tsGalley + g <- viewGalley put ( g . paths ["/conversations", toByteString' convid] @@ -615,7 +635,7 @@ updateTeamConv zusr convid upd = do createOne2OneTeamConv :: UserId -> UserId -> Maybe Text -> TeamId -> TestM ResponseLBS createOne2OneTeamConv u1 u2 n tid = do - g <- view tsGalley + g <- viewGalley let conv = NewConv [u2] [] (n >>= checked) mempty Nothing (Just $ ConvTeamInfo tid) Nothing Nothing roleNameWireAdmin ProtocolProteusTag Nothing post $ g . path "/conversations/one2one" . zUser u1 . zConn "conn" . zType "access" . json conv @@ -671,7 +691,7 @@ postConvWithRemoteUsers u n = postTeamConv :: TeamId -> UserId -> [UserId] -> Maybe Text -> [Access] -> Maybe (Set AccessRoleV2) -> Maybe Milliseconds -> TestM ResponseLBS postTeamConv tid u us name a r mtimer = do - g <- view tsGalley + g <- viewGalley let conv = NewConv us [] (name >>= checked) (Set.fromList a) r (Just (ConvTeamInfo tid)) mtimer Nothing roleNameWireAdmin ProtocolProteusTag Nothing post $ g . path "/conversations" . zUser u . zConn "conn" . zType "access" . json conv @@ -708,25 +728,25 @@ postConvWithRole u members name access arole timer role = postConvWithReceipt :: UserId -> [UserId] -> Maybe Text -> [Access] -> Maybe (Set AccessRoleV2) -> Maybe Milliseconds -> ReceiptMode -> TestM ResponseLBS postConvWithReceipt u us name a r mtimer rcpt = do - g <- view tsGalley + g <- viewGalley let conv = NewConv us [] (name >>= checked) (Set.fromList a) r Nothing mtimer (Just rcpt) roleNameWireAdmin ProtocolProteusTag Nothing post $ g . path "/conversations" . zUser u . zConn "conn" . zType "access" . json conv postSelfConv :: UserId -> TestM ResponseLBS postSelfConv u = do - g <- view tsGalley + g <- viewGalley post $ g . path "/conversations/self" . zUser u . zConn "conn" . zType "access" postO2OConv :: UserId -> UserId -> Maybe Text -> TestM ResponseLBS postO2OConv u1 u2 n = do - g <- view tsGalley + g <- viewGalley let conv = NewConv [u2] [] (n >>= checked) mempty Nothing Nothing Nothing Nothing roleNameWireAdmin ProtocolProteusTag Nothing post $ g . path "/conversations/one2one" . zUser u1 . zConn "conn" . zType "access" . json conv postConnectConv :: UserId -> UserId -> Text -> Text -> Maybe Text -> TestM ResponseLBS postConnectConv a b name msg email = do qb <- Qualified b <$> viewFederationDomain - g <- view tsGalley + g <- viewGalley post $ g . path "/i/conversations/connect" @@ -737,7 +757,7 @@ postConnectConv a b name msg email = do putConvAccept :: UserId -> ConvId -> TestM ResponseLBS putConvAccept invited cid = do - g <- view tsGalley + g <- viewGalley put $ g . paths ["/i/conversations", C.pack $ show cid, "accept", "v2"] @@ -763,7 +783,7 @@ postOtrMessage' :: [(UserId, ClientId, Text)] -> TestM ResponseLBS postOtrMessage' reportMissing f u d c rec = do - g <- view tsGalley + g <- viewGalley post $ g . f @@ -903,7 +923,7 @@ postProtoOtrMessage = postProtoOtrMessage' Nothing id postProtoOtrMessage' :: Maybe [UserId] -> (Request -> Request) -> UserId -> ClientId -> ConvId -> OtrRecipients -> TestM ResponseLBS postProtoOtrMessage' reportMissing modif u d c rec = do - g <- view tsGalley + g <- viewGalley let m = runPut (encodeMessage $ mkOtrProtoMessage d rec reportMissing "ZXhhbXBsZQ==") in post $ g @@ -926,7 +946,7 @@ mkOtrProtoMessage sender rec reportMissing ad = getConvs :: UserId -> Maybe (Either [ConvId] ConvId) -> Maybe Int32 -> TestM ResponseLBS getConvs u r s = do - g <- view tsGalley + g <- viewGalley get $ g . path "/conversations" @@ -935,12 +955,16 @@ getConvs u r s = do . zType "access" . convRange r s -listConvs :: (MonadIO m, MonadHttp m, HasGalley m) => UserId -> ListConversations -> m ResponseLBS +listConvs :: + (MonadIO m, MonadHttp m, MonadReader TestSetup m) => + UserId -> + ListConversations -> + m ResponseLBS listConvs u req = do - g <- viewGalley + g <- view tsUnversionedGalley post $ g - . path "/conversations/list/v2" + . path "/v1/conversations/list/v2" . zUser u . zConn "conn" . zType "access" @@ -968,7 +992,7 @@ getConvQualified u (Qualified conv domain) = do getConvIds :: UserId -> Maybe (Either [ConvId] ConvId) -> Maybe Int32 -> TestM ResponseLBS getConvIds u r s = do - g <- view tsGalley + g <- viewGalley get $ g . path "/conversations/ids" @@ -979,7 +1003,7 @@ getConvIds u r s = do listConvIds :: UserId -> GetPaginatedConversationIds -> TestM ResponseLBS listConvIds u paginationOpts = do - g <- view tsGalley + g <- viewGalley post $ g . path "/conversations/list-ids" @@ -994,34 +1018,44 @@ listRemoteConvs remoteDomain uid = do pure $ filter (\qcnv -> qDomain qcnv == remoteDomain) allConvs postQualifiedMembers :: - (HasGalley m, MonadIO m, MonadHttp m) => + (MonadReader TestSetup m, MonadIO m, MonadHttp m) => UserId -> NonEmpty (Qualified UserId) -> ConvId -> m ResponseLBS postQualifiedMembers zusr invitees conv = do - g <- viewGalley + g <- view tsUnversionedGalley let invite = InviteQualified invitees roleNameWireAdmin post $ g - . paths ["conversations", toByteString' conv, "members", "v2"] + . paths ["v1", "conversations", toByteString' conv, "members", "v2"] . zUser zusr . zConn "conn" . zType "access" . json invite -postMembers :: UserId -> NonEmpty (Qualified UserId) -> Qualified ConvId -> TestM ResponseLBS +postMembers :: + (MonadIO m, MonadHttp m, HasGalley m) => + UserId -> + NonEmpty (Qualified UserId) -> + Qualified ConvId -> + m ResponseLBS postMembers u us c = postMembersWithRole u us c roleNameWireAdmin -postMembersWithRole :: UserId -> NonEmpty (Qualified UserId) -> Qualified ConvId -> RoleName -> TestM ResponseLBS +postMembersWithRole :: + (MonadIO m, MonadHttp m, HasGalley m) => + UserId -> + NonEmpty (Qualified UserId) -> + Qualified ConvId -> + RoleName -> + m ResponseLBS postMembersWithRole u us c r = do - g <- view tsGalley + g <- viewGalley let i = InviteQualified us r post $ g . paths - [ v2, - "conversations", + [ "conversations", toByteString' (qDomain c), toByteString' (qUnqualified c), "members" @@ -1030,8 +1064,6 @@ postMembersWithRole u us c r = do . zConn "conn" . zType "access" . json i - where - v2 = toByteString' (toLower <$> show V2) deleteMemberQualified :: (HasCallStack, MonadIO m, MonadHttp m, HasGalley m) => @@ -1057,7 +1089,7 @@ deleteMemberQualified u1 (Qualified u2 u2Domain) (Qualified conv convDomain) = d getSelfMember :: UserId -> ConvId -> TestM ResponseLBS getSelfMember u c = do - g <- view tsGalley + g <- viewGalley get $ g . paths ["conversations", toByteString' c, "self"] @@ -1067,7 +1099,7 @@ getSelfMember u c = do putMember :: UserId -> MemberUpdate -> Qualified ConvId -> TestM ResponseLBS putMember u m (Qualified c dom) = do - g <- view tsGalley + g <- viewGalley put $ g . paths ["conversations", toByteString' dom, toByteString' c, "self"] @@ -1102,7 +1134,7 @@ putOtherMemberQualified from to m c = do putOtherMember :: UserId -> UserId -> OtherMemberUpdate -> ConvId -> TestM ResponseLBS putOtherMember from to m c = do - g <- view tsGalley + g <- viewGalley put $ g . paths ["conversations", toByteString' c, "members", toByteString' to] @@ -1136,7 +1168,7 @@ putQualifiedConversationName u c n = do putConversationName :: UserId -> ConvId -> Text -> TestM ResponseLBS putConversationName u c n = do - g <- view tsGalley + g <- viewGalley let update = ConversationRename n put ( g @@ -1167,7 +1199,7 @@ putQualifiedReceiptMode u (Qualified c dom) r = do putReceiptMode :: UserId -> ConvId -> ReceiptMode -> TestM ResponseLBS putReceiptMode u c r = do - g <- view tsGalley + g <- viewGalley let update = ConversationReceiptModeUpdate r put ( g @@ -1180,7 +1212,7 @@ putReceiptMode u c r = do getJoinCodeConv :: UserId -> Code.Key -> Code.Value -> TestM ResponseLBS getJoinCodeConv u k v = do - g <- view tsGalley + g <- viewGalley get $ g . paths ["/conversations", "join"] @@ -1190,7 +1222,7 @@ getJoinCodeConv u k v = do postJoinConv :: UserId -> ConvId -> TestM ResponseLBS postJoinConv u c = do - g <- view tsGalley + g <- viewGalley post $ g . paths ["/conversations", toByteString' c, "join"] @@ -1200,7 +1232,7 @@ postJoinConv u c = do postJoinCodeConv :: UserId -> ConversationCode -> TestM ResponseLBS postJoinCodeConv u j = do - g <- view tsGalley + g <- viewGalley post $ g . paths ["/conversations", "join"] @@ -1211,7 +1243,7 @@ postJoinCodeConv u j = do putAccessUpdate :: UserId -> ConvId -> ConversationAccessData -> TestM ResponseLBS putAccessUpdate u c acc = do - g <- view tsGalley + g <- viewGalley put $ g . paths ["/conversations", toByteString' c, "access"] @@ -1260,7 +1292,7 @@ putMessageTimerUpdateQualified u c acc = do putMessageTimerUpdate :: UserId -> ConvId -> ConversationMessageTimerUpdate -> TestM ResponseLBS putMessageTimerUpdate u c acc = do - g <- view tsGalley + g <- viewGalley put $ g . paths ["/conversations", toByteString' c, "message-timer"] @@ -1271,7 +1303,7 @@ putMessageTimerUpdate u c acc = do postConvCode :: UserId -> ConvId -> TestM ResponseLBS postConvCode u c = do - g <- view tsGalley + g <- viewGalley post $ g . paths ["/conversations", toByteString' c, "code"] @@ -1281,7 +1313,7 @@ postConvCode u c = do postConvCodeCheck :: ConversationCode -> TestM ResponseLBS postConvCodeCheck code = do - g <- view tsGalley + g <- viewGalley post $ g . path "/conversations/code-check" @@ -1289,7 +1321,7 @@ postConvCodeCheck code = do getConvCode :: UserId -> ConvId -> TestM ResponseLBS getConvCode u c = do - g <- view tsGalley + g <- viewGalley get $ g . paths ["/conversations", toByteString' c, "code"] @@ -1299,7 +1331,7 @@ getConvCode u c = do deleteConvCode :: UserId -> ConvId -> TestM ResponseLBS deleteConvCode u c = do - g <- view tsGalley + g <- viewGalley delete $ g . paths ["/conversations", toByteString' c, "code"] @@ -1341,7 +1373,7 @@ getTeamQueue zusr msince msize onlyLast = getTeamQueue' :: HasCallStack => UserId -> Maybe NotificationId -> Maybe Int -> Bool -> TestM ResponseLBS getTeamQueue' zusr msince msize onlyLast = do - g <- view tsGalley + g <- viewGalley get ( g . path "/teams/notifications" . zUser zusr @@ -1378,7 +1410,7 @@ registerRemoteConv convId originUser name othMembers = do getFeatureStatusMulti :: forall cfg. (IsFeatureConfig cfg, KnownSymbol (FeatureSymbol cfg)) => Multi.TeamFeatureNoConfigMultiRequest -> TestM ResponseLBS getFeatureStatusMulti req = do - g <- view tsGalley + g <- viewGalley post ( g . paths ["i", "features-multi-teams", featureNameBS @cfg] . json req @@ -1571,6 +1603,18 @@ wsAssertMLSMessage conv u message n = do ntfTransient n @?= False assertMLSMessageEvent conv u message e +wsAssertClientRemoved :: + HasCallStack => + ClientId -> + Notification -> + IO () +wsAssertClientRemoved cid n = do + let j = Object $ List1.head (ntfPayload n) + let etype = j ^? key "type" . _String + let eclient = j ^? key "client" . key "id" . _String + etype @?= Just "user.client-remove" + fmap ClientId eclient @?= Just cid + assertMLSMessageEvent :: HasCallStack => Qualified ConvId -> @@ -1690,15 +1734,15 @@ assertRemoveUpdate req qconvId remover alreadyPresentUsers victim = liftIO $ do sort (cuAlreadyPresentUsers cu) @?= sort alreadyPresentUsers cuAction cu @?= SomeConversationAction (sing @'ConversationRemoveMembersTag) (pure victim) -assertLeaveUpdate :: (MonadIO m, HasCallStack) => FederatedRequest -> Qualified ConvId -> Qualified UserId -> [UserId] -> Qualified UserId -> m () -assertLeaveUpdate req qconvId remover alreadyPresentUsers victim = liftIO $ do +assertLeaveUpdate :: (MonadIO m, HasCallStack) => FederatedRequest -> Qualified ConvId -> Qualified UserId -> [UserId] -> m () +assertLeaveUpdate req qconvId remover alreadyPresentUsers = liftIO $ do frRPC req @?= "on-conversation-updated" frOriginDomain req @?= qDomain qconvId let Just cu = decode (frBody req) cuOrigUserId cu @?= remover cuConvId cu @?= qUnqualified qconvId sort (cuAlreadyPresentUsers cu) @?= sort alreadyPresentUsers - cuAction cu @?= SomeConversationAction (sing @'ConversationLeaveTag) (pure victim) + cuAction cu @?= SomeConversationAction (sing @'ConversationLeaveTag) () ------------------------------------------------------------------------------- -- Helpers @@ -1793,13 +1837,13 @@ connectUsersWith :: connectUsersWith fn u = mapM connectTo where connectTo v = do - b <- view tsBrig + b <- view tsUnversionedBrig r1 <- post ( b . zUser u . zConn "conn" - . path "/connections" + . paths ["v1", "connections"] . json (ConnectionRequest v (unsafeRange "chat")) . fn ) @@ -1808,20 +1852,20 @@ connectUsersWith fn u = mapM connectTo ( b . zUser v . zConn "conn" - . paths ["connections", toByteString' u] + . paths ["v1", "connections", toByteString' u] . json (ConnectionUpdate Accepted) . fn ) pure (r1, r2) connectWithRemoteUser :: - (MonadReader TestSetup m, MonadIO m, MonadHttp m, MonadCatch m, HasCallStack) => + (HasBrig m, MonadIO m, MonadHttp m, MonadCatch m, HasCallStack) => UserId -> Qualified UserId -> m () connectWithRemoteUser self other = do let req = CreateConnectionForTest self other - b <- view tsBrig + b <- viewBrig put ( b . zUser self @@ -1836,10 +1880,10 @@ connectWithRemoteUser self other = do -- | A copy of 'postConnection' from Brig integration tests. postConnection :: UserId -> UserId -> TestM ResponseLBS postConnection from to = do - brig <- view tsBrig + brig <- view tsUnversionedBrig post $ brig - . path "/connections" + . paths ["v1", "connections"] . contentJson . body payload . zUser from @@ -1851,10 +1895,10 @@ postConnection from to = do postConnectionQualified :: UserId -> Qualified UserId -> TestM ResponseLBS postConnectionQualified from (Qualified toUser toDomain) = do - brig <- view tsBrig + brig <- viewBrig post $ brig - . paths ["/connections", toByteString' toDomain, toByteString' toUser] + . paths ["connections", toByteString' toDomain, toByteString' toUser] . contentJson . zUser from . zConn "conn" @@ -1862,10 +1906,10 @@ postConnectionQualified from (Qualified toUser toDomain) = do -- | A copy of 'putConnection' from Brig integration tests. putConnection :: UserId -> UserId -> Relation -> TestM ResponseLBS putConnection from to r = do - brig <- view tsBrig + brig <- view tsUnversionedBrig put $ brig - . paths ["/connections", toByteString' to] + . paths ["v1", "connections", toByteString' to] . contentJson . body payload . zUser from @@ -1883,10 +1927,10 @@ putConnectionQualified fromQualified to r = do "The qualified user's domain is not local" localDomain qualifiedDomain - brig <- view tsBrig + brig <- view tsUnversionedBrig put $ brig - . paths ["/connections", toByteString' to] + . paths ["v1", "connections", toByteString' to] . contentJson . body payload . zUser from @@ -1897,7 +1941,7 @@ putConnectionQualified fromQualified to r = do -- | A copy of `assertConnections from Brig integration tests. assertConnections :: HasCallStack => UserId -> [ConnectionStatus] -> TestM () assertConnections u cstat = do - brig <- view tsBrig + brig <- view tsUnversionedBrig resp <- listConnections brig u show cstat <> " is not a subset of " <> show cstat' where status c = ConnectionStatus (ucFrom c) (qUnqualified $ ucTo c) (ucStatus c) - listConnections brig usr = get $ brig . path "connections" . zUser usr + listConnections brig usr = get $ brig . paths ["v1", "connections"] . zUser usr randomUsers :: Int -> TestM [UserId] randomUsers n = replicateM n randomUser @@ -1938,7 +1982,7 @@ randomUser'' isCreator hasPassword hasEmail = selfUser <$> randomUserProfile' is randomUserProfile' :: HasCallStack => Bool -> Bool -> Bool -> TestM SelfProfile randomUserProfile' isCreator hasPassword hasEmail = do - b <- view tsBrig + b <- viewBrig e <- liftIO randomEmail let p = object $ @@ -1950,7 +1994,7 @@ randomUserProfile' isCreator hasPassword hasEmail = do ephemeralUser :: HasCallStack => TestM UserId ephemeralUser = do - b <- view tsBrig + b <- viewBrig name <- UUID.toText <$> liftIO nextRandom let p = object ["name" .= name] r <- post (b . path "/register" . json p) UserId -> LastPrekey -> Maybe (Set Client.ClientCapability) -> TestM ClientId randomClientWithCaps uid lk caps = do - b <- view tsBrig + b <- viewBrig resp <- post ( b @@ -1989,18 +2033,18 @@ ensureDeletedState check from u = do getDeletedState :: HasCallStack => UserId -> UserId -> TestM (Maybe Bool) getDeletedState from u = do - b <- view tsBrig + b <- view tsUnversionedBrig fmap profileDeleted . responseJsonMaybe <$> get ( b - . paths ["users", toByteString' u] + . paths ["v1", "users", toByteString' u] . zUser from . zConn "conn" ) getClients :: UserId -> TestM ResponseLBS getClients u = do - b <- view tsBrig + b <- viewBrig get $ b . paths ["clients"] @@ -2009,7 +2053,7 @@ getClients u = do getInternalClientsFull :: UserSet -> TestM UserClientsFull getInternalClientsFull userSet = do - b <- view tsBrig + b <- viewBrig res <- post $ b @@ -2027,7 +2071,7 @@ ensureClientCaps uid cid caps = do -- TODO: Refactor, as used also in brig deleteClient :: UserId -> ClientId -> Maybe PlainTextPassword -> TestM ResponseLBS deleteClient u c pw = do - b <- view tsBrig + b <- viewBrig delete $ b . paths ["clients", toByteString' c] @@ -2045,7 +2089,7 @@ deleteClient u c pw = do -- TODO: Refactor, as used also in brig isUserDeleted :: HasCallStack => UserId -> TestM Bool isUserDeleted u = do - b <- view tsBrig + b <- viewBrig r <- get (b . paths ["i", "users", toByteString' u, "status"]) ConvId -> TestM Bool isMember usr cnv = do - g <- view tsGalley + g <- viewGalley res <- get $ g @@ -2276,12 +2320,12 @@ mkProteusConv cnvId creator selfRole otherMembers = -- | ES is only refreshed occasionally; we don't want to wait for that in tests. refreshIndex :: TestM () refreshIndex = do - brig <- view tsBrig + brig <- viewBrig post (brig . path "/i/index/refresh") !!! const 200 === statusCode postSSOUser :: Text -> Bool -> UserSSOId -> TeamId -> TestM ResponseLBS postSSOUser name hasEmail ssoid teamid = do - brig <- view tsBrig + brig <- viewBrig email <- randomEmail let o = object $ @@ -2297,20 +2341,23 @@ postSSOUser name hasEmail ssoid teamid = do defCookieLabel :: CookieLabel defCookieLabel = CookieLabel "auth" -withSettingsOverrides :: (Opts.Opts -> Opts.Opts) -> TestM a -> TestM a -withSettingsOverrides f action = do - ts :: TestSetup <- ask - let opts = f (ts ^. tsGConf) - liftIO . lowerCodensity $ do - (galleyApp, _env) <- Run.mkApp opts - port' <- withMockServer galleyApp - liftIO $ - runReaderT - (runTestM action) - ( ts - & tsGalley .~ Bilge.host "127.0.0.1" . Bilge.port port' - & tsFedGalleyClient .~ FedClient (ts ^. tsManager) (Endpoint "127.0.0.1" port') - ) +class HasSettingsOverrides m where + withSettingsOverrides :: (Opts.Opts -> Opts.Opts) -> m a -> m a + +instance HasSettingsOverrides TestM where + withSettingsOverrides f action = do + ts :: TestSetup <- ask + let opts = f (ts ^. tsGConf) + liftIO . lowerCodensity $ do + (galleyApp, _env) <- Run.mkApp opts + port' <- withMockServer galleyApp + liftIO $ + runReaderT + (runTestM action) + ( ts + & tsUnversionedGalley .~ Bilge.host "127.0.0.1" . Bilge.port port' + & tsFedGalleyClient .~ FedClient (ts ^. tsManager) (Endpoint "127.0.0.1" port') + ) waitForMemberDeletion :: UserId -> TeamId -> UserId -> TestM () waitForMemberDeletion zusr tid uid = do @@ -2320,7 +2367,7 @@ waitForMemberDeletion zusr tid uid = do assertFailure "Timed out waiting for member deletion" where loop = do - galley <- view tsGalley + galley <- viewGalley res <- get (galley . paths ["teams", toByteString' tid, "members", toByteString' uid] . zUser zusr) case statusCode res of 404 -> pure () @@ -2340,7 +2387,7 @@ deleteTeamMember g tid owner deletee = deleteTeam :: UserId -> TeamId -> TestM () deleteTeam owner tid = do - g <- view tsGalley + g <- viewGalley delete ( g . paths ["teams", toByteString' tid] @@ -2360,7 +2407,7 @@ getUsersByHandle = getUsersBy "handles" getUsersBy :: forall uidsOrHandles. (ToByteString uidsOrHandles) => ByteString -> [uidsOrHandles] -> TestM [User] getUsersBy keyName = chunkify $ \keys -> do - brig <- view tsBrig + brig <- viewBrig let users = BS.intercalate "," $ toByteString' <$> keys res <- get @@ -2374,8 +2421,8 @@ getUsersBy keyName = chunkify $ \keys -> do getUserProfile :: UserId -> UserId -> TestM UserProfile getUserProfile zusr uid = do - brig <- view tsBrig - res <- get (brig . zUser zusr . paths ["users", toByteString' uid]) + brig <- view tsUnversionedBrig + res <- get (brig . zUser zusr . paths ["v1", "users", toByteString' uid]) responseJsonError res upgradeClientToLH :: HasCallStack => UserId -> ClientId -> TestM () @@ -2384,7 +2431,7 @@ upgradeClientToLH zusr cid = putCapabilities :: HasCallStack => UserId -> ClientId -> [ClientCapability] -> TestM () putCapabilities zusr cid caps = do - brig <- view tsBrig + brig <- viewBrig void $ put ( brig @@ -2396,29 +2443,29 @@ putCapabilities zusr cid caps = do getUsersPrekeysClientUnqualified :: HasCallStack => UserId -> UserId -> ClientId -> TestM ResponseLBS getUsersPrekeysClientUnqualified zusr uid cid = do - brig <- view tsBrig + brig <- view tsUnversionedBrig get ( brig . zUser zusr - . paths ["users", toByteString' uid, "prekeys", toByteString' cid] + . paths ["v1", "users", toByteString' uid, "prekeys", toByteString' cid] ) getUsersPrekeyBundleUnqualified :: HasCallStack => UserId -> UserId -> TestM ResponseLBS getUsersPrekeyBundleUnqualified zusr uid = do - brig <- view tsBrig + brig <- view tsUnversionedBrig get ( brig . zUser zusr - . paths ["users", toByteString' uid, "prekeys"] + . paths ["v1", "users", toByteString' uid, "prekeys"] ) getMultiUserPrekeyBundleUnqualified :: HasCallStack => UserId -> UserClients -> TestM ResponseLBS getMultiUserPrekeyBundleUnqualified zusr userClients = do - brig <- view tsBrig + brig <- view tsUnversionedBrig post ( brig . zUser zusr - . paths ["users", "prekeys"] + . paths ["v1", "users", "prekeys"] . json userClients ) @@ -2455,9 +2502,10 @@ withTempMockFederator :: withTempMockFederator resp = withTempMockFederator' $ pure . encode . resp withTempMockFederator' :: + (MonadIO m, MonadMask m, HasSettingsOverrides m) => (FederatedRequest -> IO LByteString) -> - TestM b -> - TestM (b, [FederatedRequest]) + m b -> + m (b, [FederatedRequest]) withTempMockFederator' resp action = do Mock.withTempMockFederator [("Content-Type", "application/json")] @@ -2694,7 +2742,7 @@ assertJust Nothing = liftIO $ error "Expected Just, got Nothing" iUpsertOne2OneConversation :: UpsertOne2OneConversationRequest -> TestM ResponseLBS iUpsertOne2OneConversation req = do - galley <- view tsGalley + galley <- viewGalley post (galley . path "/i/conversations/one2one/upsert" . Bilge.json req) createOne2OneConvWithRemote :: HasCallStack => Local UserId -> Remote UserId -> TestM () @@ -2760,3 +2808,77 @@ wsAssertConvReceiptModeUpdate conv usr new n = do evtType e @?= ConvReceiptModeUpdate evtFrom e @?= usr evtData e @?= EdConvReceiptModeUpdate (ConversationReceiptModeUpdate new) + +wsAssertBackendRemoveProposal :: HasCallStack => Qualified UserId -> Qualified ConvId -> KeyPackageRef -> Notification -> IO ByteString +wsAssertBackendRemoveProposal fromUser convId kpref n = do + let e = List1.head (WS.unpackPayload n) + ntfTransient n @?= False + evtConv e @?= convId + evtType e @?= MLSMessageAdd + evtFrom e @?= fromUser + let bs = getMLSMessageData (evtData e) + let msg = fromRight (error "Failed to parse Message 'MLSPlaintext") $ decodeMLS' bs + let tbs = rmValue . msgTBS $ msg + tbsMsgSender tbs @?= PreconfiguredSender 0 + case tbsMsgPayload tbs of + ProposalMessage rp -> + case rmValue rp of + RemoveProposal kpRefRemove -> + kpRefRemove @?= kpref + otherProp -> assertFailure $ "Expected RemoveProposal but got " <> show otherProp + otherPayload -> assertFailure $ "Expected ProposalMessage but got " <> show otherPayload + pure bs + where + getMLSMessageData :: Conv.EventData -> ByteString + getMLSMessageData (EdMLSMessage bs) = bs + getMLSMessageData d = error ("Excepected EdMLSMessage, but got " <> show d) + +wsAssertAddProposal :: + HasCallStack => + Qualified UserId -> + Qualified ConvId -> + Notification -> + IO ByteString +wsAssertAddProposal fromUser convId n = do + let e = List1.head (WS.unpackPayload n) + ntfTransient n @?= False + evtConv e @?= convId + evtType e @?= MLSMessageAdd + evtFrom e @?= fromUser + let bs = getMLSMessageData (evtData e) + let msg = fromRight (error "Failed to parse Message 'MLSPlaintext") $ decodeMLS' bs + let tbs = rmValue . msgTBS $ msg + tbsMsgSender tbs @?= NewMemberSender + case tbsMsgPayload tbs of + ProposalMessage rp -> + case rmValue rp of + AddProposal _ -> pure () + otherProp -> + assertFailure $ + "Expected AddProposal but got " <> show otherProp + otherPayload -> + assertFailure $ + "Expected ProposalMessage but got " <> show otherPayload + pure bs + where + getMLSMessageData :: Conv.EventData -> ByteString + getMLSMessageData (EdMLSMessage bs) = bs + getMLSMessageData d = error ("Excepected EdMLSMessage, but got " <> show d) + +createAndConnectUsers :: [Maybe Text] -> TestM [Qualified UserId] +createAndConnectUsers domains = do + localDomain <- viewFederationDomain + users <- for domains $ maybe randomQualifiedUser (randomQualifiedId . Domain) + let userPairs = do + t <- tails users + (a, others) <- maybeToList (uncons t) + b <- others + pure (a, b) + for_ userPairs $ \(a, b) -> + case (qDomain a == localDomain, qDomain b == localDomain) of + (True, True) -> + connectUsers (qUnqualified a) (pure (qUnqualified b)) + (True, False) -> connectWithRemoteUser (qUnqualified a) b + (False, True) -> connectWithRemoteUser (qUnqualified b) a + (False, False) -> pure () + pure users diff --git a/services/galley/test/integration/API/Util/TeamFeature.hs b/services/galley/test/integration/API/Util/TeamFeature.hs index 5e62e7b77f..b56ee21c1b 100644 --- a/services/galley/test/integration/API/Util/TeamFeature.hs +++ b/services/galley/test/integration/API/Util/TeamFeature.hs @@ -20,7 +20,7 @@ module API.Util.TeamFeature where import API.Util (HasGalley (viewGalley), zUser) import qualified API.Util as Util import Bilge -import Control.Lens (view, (.~), (^?)) +import Control.Lens ((.~), (^?)) import Control.Monad.Catch (MonadThrow) import Data.Aeson (FromJSON, Result (Success), ToJSON, Value, fromJSON) import Data.Aeson.Lens @@ -129,7 +129,7 @@ getFeatureConfig uid = do getAllFeatureConfigs :: HasCallStack => UserId -> TestM ResponseLBS getAllFeatureConfigs uid = do - g <- view tsGalley + g <- viewGalley getAllFeatureConfigsWithGalley g uid getAllFeatureConfigsWithGalley :: (MonadIO m, MonadHttp m, HasCallStack) => (Request -> Request) -> UserId -> m ResponseLBS @@ -171,7 +171,7 @@ putTeamFeatureFlagInternalTTL :: Public.WithStatusNoLock cfg -> TestM ResponseLBS putTeamFeatureFlagInternalTTL reqmod tid status = do - g <- view tsGalley + g <- viewGalley putTeamFeatureFlagInternalWithGalleyAndMod @cfg g reqmod tid status putTeamFeatureFlagInternal :: @@ -186,7 +186,7 @@ putTeamFeatureFlagInternal :: Public.WithStatusNoLock cfg -> TestM ResponseLBS putTeamFeatureFlagInternal reqmod tid status = do - g <- view tsGalley + g <- viewGalley putTeamFeatureFlagInternalWithGalleyAndMod @cfg g reqmod tid status putTeamFeatureFlagInternalWithGalleyAndMod :: @@ -222,7 +222,7 @@ setLockStatusInternal :: Public.LockStatus -> TestM ResponseLBS setLockStatusInternal reqmod tid lockStatus = do - galley <- view tsGalley + galley <- viewGalley put $ galley . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg, toByteString' lockStatus] @@ -238,7 +238,7 @@ getFeatureStatusInternal :: TeamId -> TestM ResponseLBS getFeatureStatusInternal tid = do - galley <- view tsGalley + galley <- viewGalley get $ galley . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg] @@ -255,7 +255,7 @@ patchFeatureStatusInternal :: Public.WithStatusPatch cfg -> TestM ResponseLBS patchFeatureStatusInternal tid reqBody = do - galley <- view tsGalley + galley <- viewGalley patch $ galley . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg] @@ -274,7 +274,7 @@ patchFeatureStatusInternalWithMod :: Public.WithStatusPatch cfg -> TestM ResponseLBS patchFeatureStatusInternalWithMod reqmod tid reqBody = do - galley <- view tsGalley + galley <- viewGalley patch $ galley . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg] diff --git a/services/galley/test/integration/TestHelpers.hs b/services/galley/test/integration/TestHelpers.hs index 37c039d637..88b35c4e77 100644 --- a/services/galley/test/integration/TestHelpers.hs +++ b/services/galley/test/integration/TestHelpers.hs @@ -21,7 +21,10 @@ module TestHelpers where import API.SQS import Control.Lens (view) +import Control.Monad.Catch (MonadMask) +import Control.Retry import Data.Domain (Domain) +import Data.Qualified import qualified Galley.Aws as Aws import Galley.Options (optSettings, setFederationDomain) import Imports @@ -54,3 +57,11 @@ test s n h = testCase n runTest viewFederationDomain :: TestM Domain viewFederationDomain = view (tsGConf . optSettings . setFederationDomain) + +qualifyLocal :: a -> TestM (Local a) +qualifyLocal x = do + domain <- viewFederationDomain + pure $ toLocalUnsafe domain x + +eventually :: (MonadIO m, MonadMask m) => m a -> m a +eventually = recovering (limitRetries 3 <> exponentialBackoff 100000) [] . const diff --git a/services/galley/test/integration/TestSetup.hs b/services/galley/test/integration/TestSetup.hs index 9dcae5c314..e01fc52b14 100644 --- a/services/galley/test/integration/TestSetup.hs +++ b/services/galley/test/integration/TestSetup.hs @@ -25,8 +25,8 @@ module TestSetup tsGConf, tsIConf, tsManager, - tsGalley, - tsBrig, + tsUnversionedGalley, + tsUnversionedBrig, tsCannon, tsAwsEnv, tsMaxConvSize, @@ -112,8 +112,8 @@ data TestSetup = TestSetup { _tsGConf :: Opts, _tsIConf :: IntegrationConfig, _tsManager :: Manager, - _tsGalley :: GalleyR, - _tsBrig :: BrigR, + _tsUnversionedGalley :: GalleyR, + _tsUnversionedBrig :: BrigR, _tsCannon :: CannonR, _tsAwsEnv :: Maybe Aws.Env, _tsMaxConvSize :: Word16, diff --git a/services/galley/test/unit/Test/Galley/Mapping.hs b/services/galley/test/unit/Test/Galley/Mapping.hs index da4ca972fc..7ad96398c1 100644 --- a/services/galley/test/unit/Test/Galley/Mapping.hs +++ b/services/galley/test/unit/Test/Galley/Mapping.hs @@ -107,10 +107,9 @@ genLocalMember = <*> pure defMemberStatus <*> pure Nothing <*> arbitrary - <*> arbitrary genRemoteMember :: Gen RemoteMember -genRemoteMember = RemoteMember <$> arbitrary <*> pure roleNameWireMember <*> arbitrary +genRemoteMember = RemoteMember <$> arbitrary <*> pure roleNameWireMember genConversation :: Gen Data.Conversation genConversation = diff --git a/services/gundeck/gundeck.cabal b/services/gundeck/gundeck.cabal index 7ee97b7d79..ccb7d9d00e 100644 --- a/services/gundeck/gundeck.cabal +++ b/services/gundeck/gundeck.cabal @@ -338,6 +338,7 @@ executable gundeck-schema V6 V7 V8 + V9 hs-source-dirs: schema/src default-extensions: diff --git a/services/gundeck/schema/src/Main.hs b/services/gundeck/schema/src/Main.hs index 86d75348e8..e8b72ef8d8 100644 --- a/services/gundeck/schema/src/Main.hs +++ b/services/gundeck/schema/src/Main.hs @@ -30,6 +30,7 @@ import qualified V5 import qualified V6 import qualified V7 import qualified V8 +import qualified V9 main :: IO () main = do @@ -45,7 +46,8 @@ main = do V5.migration, V6.migration, V7.migration, - V8.migration + V8.migration, + V9.migration ] `finally` Log.close l where diff --git a/services/gundeck/schema/src/V9.hs b/services/gundeck/schema/src/V9.hs new file mode 100644 index 0000000000..2583384eff --- /dev/null +++ b/services/gundeck/schema/src/V9.hs @@ -0,0 +1,33 @@ +-- 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 V9 + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = Migration 9 "Remove deprecated tables" $ do + -- all data in notifications is written with a TTL, therefore should be a good fit for TWCS. + -- TTL is 28 days (see https://github.com/wireapp/wire-server/blob/434f7a874ce5e3f7e3e57aa98afb6441d4a53169/charts/gundeck/templates/configmap.yaml#L51) + -- so 28 windows of 1 day each fits well with the suggestion of 20-30 windows. + -- https://cassandra.apache.org/doc/latest/cassandra/operating/compaction/twcs.html + schema' [r| ALTER TABLE notifications WITH compaction = {'class': 'TimeWindowCompactionStrategy', 'compaction_window_unit': 'DAYS', 'compaction_window_size': 1}; |] diff --git a/services/gundeck/src/Gundeck/Env.hs b/services/gundeck/src/Gundeck/Env.hs index 96cb36f211..e841205dea 100644 --- a/services/gundeck/src/Gundeck/Env.hs +++ b/services/gundeck/src/Gundeck/Env.hs @@ -71,8 +71,8 @@ createEnv m o = do c <- maybe (C.initialContactsPlain (o ^. optCassandra . casEndpoint . epHost)) - (C.initialContactsDisco "cassandra_gundeck") - (unpack <$> o ^. optDiscoUrl) + (C.initialContactsDisco "cassandra_gundeck" . unpack) + (o ^. optDiscoUrl) n <- newManager tlsManagerSettings diff --git a/services/gundeck/src/Gundeck/Instances.hs b/services/gundeck/src/Gundeck/Instances.hs index 9b2087b5b7..f06fb8e2a7 100644 --- a/services/gundeck/src/Gundeck/Instances.hs +++ b/services/gundeck/src/Gundeck/Instances.hs @@ -63,7 +63,7 @@ instance Cql ConnId where instance Cql EndpointArn where ctype = Tagged TextColumn toCql = CqlText . toText - fromCql (CqlText txt) = either Left pure (fromText txt) + fromCql (CqlText txt) = fromText txt fromCql _ = Left "EndpointArn: Text expected" instance Cql Token where diff --git a/services/gundeck/src/Gundeck/Push/Native/Serialise.hs b/services/gundeck/src/Gundeck/Push/Native/Serialise.hs index 0077594710..8e070222f5 100644 --- a/services/gundeck/src/Gundeck/Push/Native/Serialise.hs +++ b/services/gundeck/src/Gundeck/Push/Native/Serialise.hs @@ -33,24 +33,17 @@ import Gundeck.Types import Imports serialise :: HasCallStack => NativePush -> UserId -> Transport -> IO (Either Failure LT.Text) -serialise m uid transport = do - let rs = prepare m uid - case rs of - Left failure -> pure $! Left $! failure - Right (v, prio) -> case renderText transport prio v of - Nothing -> pure $ Left PayloadTooLarge - Just txt -> pure $ Right txt - -prepare :: NativePush -> UserId -> Either Failure (Value, Priority) -prepare m uid = case m of - NativePush nid prio _aps -> - let o = - object - [ "type" .= ("notice" :: Text), - "data" .= object ["id" .= nid], - "user" .= uid - ] - in Right (o, prio) +serialise (NativePush nid prio _aps) uid transport = do + case renderText transport prio o of + Nothing -> pure $ Left PayloadTooLarge + Just txt -> pure $ Right txt + where + o = + object + [ "type" .= ("notice" :: Text), + "data" .= object ["id" .= nid], + "user" .= uid + ] -- | Assemble a final SNS JSON string for transmission. renderText :: Transport -> Priority -> Value -> Maybe LT.Text diff --git a/services/gundeck/src/Gundeck/Run.hs b/services/gundeck/src/Gundeck/Run.hs index f25fcc47c4..3a5819ba6b 100644 --- a/services/gundeck/src/Gundeck/Run.hs +++ b/services/gundeck/src/Gundeck/Run.hs @@ -62,7 +62,7 @@ run o = do lst <- Async.async $ Aws.execute (e ^. awsEnv) (Aws.listen throttleMillis (runDirect e . onEvent)) wtbs <- forM (e ^. threadBudgetState) $ \tbs -> Async.async $ runDirect e $ watchThreadBudgetState m tbs 10 wCollectAuth <- Async.async (collectAuthMetrics m (Aws._awsEnv (Env._awsEnv e))) - runSettingsWithShutdown s (middleware e $ mkApp e) 5 `finally` do + runSettingsWithShutdown s (middleware e $ mkApp e) Nothing `finally` do Log.info l $ Log.msg (Log.val "Shutting down ...") shutdown (e ^. cstate) Async.cancel lst diff --git a/services/proxy/src/Proxy/Run.hs b/services/proxy/src/Proxy/Run.hs index b58d93c61e..69b209b0bb 100644 --- a/services/proxy/src/Proxy/Run.hs +++ b/services/proxy/src/Proxy/Run.hs @@ -25,7 +25,6 @@ import Control.Monad.Catch import Data.Metrics.Middleware hiding (path) import Data.Metrics.Middleware.Prometheus (waiPrometheusMiddleware) import Imports hiding (head) -import Network.Wai.Handler.Warp (runSettings) import Network.Wai.Utilities.Server hiding (serverPort) import Proxy.API (sitemap) import Proxy.Env @@ -44,4 +43,4 @@ run o = do versionMiddleware . waiPrometheusMiddleware (sitemap e) . catchErrors (e ^. applog) [Right m] - runSettings s (middleware app) `finally` destroyEnv e + runSettingsWithShutdown s (middleware app) Nothing `finally` destroyEnv e diff --git a/services/spar/spar.cabal b/services/spar/spar.cabal index 9cb59ca37e..655f2b3401 100644 --- a/services/spar/spar.cabal +++ b/services/spar/spar.cabal @@ -748,6 +748,7 @@ test-suite spec Test.Spar.DataSpec Test.Spar.Intra.BrigSpec Test.Spar.Roundtrip.ByteString + Test.Spar.Scim.UserSpec Test.Spar.ScimSpec Test.Spar.Sem.DefaultSsoCodeSpec Test.Spar.Sem.IdPRawMetadataStoreSpec diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index a0d02df3c7..0a81b08c3a 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -443,8 +443,8 @@ idpDelete mbzusr idpid (fromMaybe False -> purge) = withDebugLog "idpDelete" (co when (mUserTeam == Just team) $ do if purge then do - BrigAccess.delete uid SAMLUserStore.delete uid uref + void $ BrigAccess.deleteUser uid else do throwSparSem SparIdPHasBoundUsers when (Cas.hasMore page) $ @@ -519,7 +519,7 @@ idpCreateXML zusr raw idpmeta mReplaces (fromMaybe defWireIdPAPIVersion -> apive teamid <- Brig.getZUsrCheckPerm zusr CreateUpdateDeleteIdp GalleyAccess.assertSSOEnabled teamid assertNoScimOrNoIdP teamid - handle <- maybe (IdPConfigStore.newHandle teamid) pure (IdPHandle . fromRange <$> mHandle) + handle <- maybe (IdPConfigStore.newHandle teamid) (pure . IdPHandle . fromRange) mHandle idp <- validateNewIdP apiversion idpmeta teamid mReplaces handle IdPRawMetadataStore.store (idp ^. SAML.idpId) raw IdPConfigStore.insertConfig idp diff --git a/services/spar/src/Spar/Intra/Brig.hs b/services/spar/src/Spar/Intra/Brig.hs index 5d23d79655..2a2f088ef7 100644 --- a/services/spar/src/Spar/Intra/Brig.hs +++ b/services/spar/src/Spar/Intra/Brig.hs @@ -29,7 +29,7 @@ module Spar.Intra.Brig setBrigUserRichInfo, setBrigUserLocale, checkHandleAvailable, - deleteBrigUser, + deleteBrigUserInternal, createBrigUserSAML, createBrigUserNoSAML, updateEmail, @@ -329,15 +329,19 @@ checkHandleAvailable hnd = do | otherwise -> rethrow "brig" resp --- | Call brig to delete a user -deleteBrigUser :: (HasCallStack, MonadSparToBrig m, MonadIO m) => UserId -> m () -deleteBrigUser buid = do - resp :: ResponseLBS <- +-- | Call brig to delete a user. +-- If the user wasn't deleted completely before, another deletion attempt will be made. +deleteBrigUserInternal :: (HasCallStack, MonadSparToBrig m, MonadIO m) => UserId -> m DeleteUserResult +deleteBrigUserInternal buid = do + resp <- call $ method DELETE . paths ["/i/users", toByteString' buid] - unless (statusCode resp == 202) $ - rethrow "brig" resp + case statusCode resp of + 200 -> pure AccountAlreadyDeleted + 202 -> pure AccountDeleted + 404 -> pure NoUser + _ -> rethrow "brig" resp -- | Verify user's password (needed for certain powerful operations). ensureReAuthorised :: diff --git a/services/spar/src/Spar/Run.hs b/services/spar/src/Spar/Run.hs index 1bf505a9a2..80e7013291 100644 --- a/services/spar/src/Spar/Run.hs +++ b/services/spar/src/Spar/Run.hs @@ -66,8 +66,8 @@ initCassandra opts lgr = do connectString <- maybe (Cas.initialContactsPlain (cassOpts ^. casEndpoint . epHost)) - (Cas.initialContactsDisco "cassandra_spar") - (cs <$> Types.discoUrl opts) + (Cas.initialContactsDisco "cassandra_spar" . cs) + (Types.discoUrl opts) cas <- Cas.init $ Cas.defSettings @@ -98,7 +98,7 @@ runServer sparCtxOpts = do (wrappedApp, ctxOpts) <- mkApp sparCtxOpts let logger = sparCtxLogger ctxOpts Log.info logger . Log.msg $ "Listening on " <> shost <> ":" <> show sport - WU.runSettingsWithShutdown settings wrappedApp 5 + WU.runSettingsWithShutdown settings wrappedApp Nothing mkApp :: Opts -> IO (Application, Env) mkApp sparCtxOpts = do diff --git a/services/spar/src/Spar/Scim/User.hs b/services/spar/src/Spar/Scim/User.hs index 637f28ac99..4b9c97b7ac 100644 --- a/services/spar/src/Spar/Scim/User.hs +++ b/services/spar/src/Spar/Scim/User.hs @@ -38,10 +38,12 @@ module Spar.Scim.User toScimStoredUser', mkValidExternalId, scimFindUserByEmail, + deleteScimUser, ) where import Brig.Types.Intra (AccountStatus, UserAccount (accountStatus, accountUser)) +import Brig.Types.User (HavePendingInvitations (..)) import qualified Control.Applicative as Applicative (empty) import Control.Lens (view, (^.)) import Control.Monad.Error.Class (MonadError) @@ -698,11 +700,20 @@ deleteScimUser tokeninfo@ScimTokenInfo {stiTeam, stiIdP} uid = ) (const id) $ do - mbBrigUser <- lift (Brig.getBrigUser Brig.WithPendingInvitations uid) - case mbBrigUser of + -- `getBrigUser` does not include deleted users. This is fine: these + -- ("tombstones") would not have the needed values (`userIdentity = + -- Nothing`) to delete a user in spar. I.e. `SAML.UserRef` and `Email` + -- cannot be figured out when a `User` has status `Deleted`. + mbBrigUser <- lift $ Brig.getBrigUser WithPendingInvitations uid + deletionStatus <- case mbBrigUser of Nothing -> - -- double-deletion gets you a 404. - throwError $ Scim.notFound "user" (idToText uid) + -- Ensure there's no left-over of this user in brig. This is safe + -- because the user has either been deleted (tombstone) or does not + -- exist. Asserting the correct team id here is not needed (and would + -- be hard as the check relies on the data of `mbBrigUser`): The worst + -- thing that could happen is that foreign users cleanup particially + -- deleted users. + lift $ BrigAccess.deleteUser uid Just brigUser -> do -- FUTUREWORK: currently it's impossible to delete the last available team owner via SCIM -- (because that owner won't be managed by SCIM in the first place), but if it ever becomes @@ -712,21 +723,47 @@ deleteScimUser tokeninfo@ScimTokenInfo {stiTeam, stiIdP} uid = throwError $ Scim.notFound "user" (idToText uid) - mIdpConfig <- mapM (lift . IdPConfigStore.getConfig) stiIdP - - case Brig.veidFromBrigUser brigUser ((^. SAML.idpMetadata . SAML.edIssuer) <$> mIdpConfig) of - Left _ -> pure () - Right veid -> - lift $ - ST.runValidExternalIdBoth - (>>) - (SAMLUserStore.delete uid) - (ScimExternalIdStore.delete stiTeam) - veid - - lift $ ScimUserTimesStore.delete uid - lift $ BrigAccess.delete uid + -- This deletion needs data from the non-deleted User in brig. So, + -- execute it first, then delete the user in brig. Unfortunately, this + -- dependency prevents us from cleaning up the spar fragments of users + -- that have been deleted in brig. Deleting scim-managed users in brig + -- (via the TM app) is blocked, though, so there is no legal way to enter + -- that situation. + deleteUserInSpar brigUser + lift $ BrigAccess.deleteUser uid + case deletionStatus of + NoUser -> + throwError $ + Scim.notFound "user" (idToText uid) + AccountAlreadyDeleted -> + throwError $ + Scim.notFound "user" (idToText uid) + AccountDeleted -> pure () + where + deleteUserInSpar :: + Members + '[ IdPConfigStore, + SAMLUserStore, + ScimExternalIdStore, + ScimUserTimesStore + ] + r => + User -> + Scim.ScimHandler (Sem r) () + deleteUserInSpar brigUser = do + mIdpConfig <- mapM (lift . IdPConfigStore.getConfig) stiIdP + + case Brig.veidFromBrigUser brigUser ((^. SAML.idpMetadata . SAML.edIssuer) <$> mIdpConfig) of + Left _ -> pure () + Right veid -> + lift $ + ST.runValidExternalIdBoth + (>>) + (SAMLUserStore.delete uid) + (ScimExternalIdStore.delete stiTeam) + veid + lift $ ScimUserTimesStore.delete uid ---------------------------------------------------------------------------- -- Utilities diff --git a/services/spar/src/Spar/Sem/BrigAccess.hs b/services/spar/src/Spar/Sem/BrigAccess.hs index b756f4f262..0e35976d5a 100644 --- a/services/spar/src/Spar/Sem/BrigAccess.hs +++ b/services/spar/src/Spar/Sem/BrigAccess.hs @@ -33,7 +33,7 @@ module Spar.Sem.BrigAccess setLocale, getRichInfo, checkHandleAvailable, - delete, + deleteUser, ensureReAuthorised, ssoLogin, getStatus, @@ -53,7 +53,7 @@ import Imports import Polysemy import qualified SAML2.WebSSO as SAML import Web.Cookie -import Wire.API.User (VerificationAction) +import Wire.API.User (DeleteUserResult, VerificationAction) import Wire.API.User.Identity import Wire.API.User.Profile import Wire.API.User.RichInfo as RichInfo @@ -74,7 +74,7 @@ data BrigAccess m a where SetLocale :: UserId -> Maybe Locale -> BrigAccess m () GetRichInfo :: UserId -> BrigAccess m RichInfo CheckHandleAvailable :: Handle -> BrigAccess m Bool - Delete :: UserId -> BrigAccess m () + DeleteUser :: UserId -> BrigAccess m DeleteUserResult EnsureReAuthorised :: Maybe UserId -> Maybe PlainTextPassword -> Maybe Code.Value -> Maybe VerificationAction -> BrigAccess m () SsoLogin :: UserId -> BrigAccess m SetCookie GetStatus :: UserId -> BrigAccess m AccountStatus diff --git a/services/spar/src/Spar/Sem/BrigAccess/Http.hs b/services/spar/src/Spar/Sem/BrigAccess/Http.hs index 122bdc3496..cbcafe8ad3 100644 --- a/services/spar/src/Spar/Sem/BrigAccess/Http.hs +++ b/services/spar/src/Spar/Sem/BrigAccess/Http.hs @@ -54,7 +54,7 @@ brigAccessToHttp mgr req = SetLocale itlu l -> Intra.setBrigUserLocale itlu l GetRichInfo itlu -> Intra.getBrigUserRichInfo itlu CheckHandleAvailable h -> Intra.checkHandleAvailable h - Delete itlu -> Intra.deleteBrigUser itlu + DeleteUser itlu -> Intra.deleteBrigUserInternal itlu EnsureReAuthorised mitlu mp mc ma -> Intra.ensureReAuthorised mitlu mp mc ma SsoLogin itlu -> Intra.ssoLogin itlu GetStatus itlu -> Intra.getStatus itlu diff --git a/services/spar/src/Spar/Sem/IdPConfigStore/Mem.hs b/services/spar/src/Spar/Sem/IdPConfigStore/Mem.hs index da125f84f4..2c3a5ad0b0 100644 --- a/services/spar/src/Spar/Sem/IdPConfigStore/Mem.hs +++ b/services/spar/src/Spar/Sem/IdPConfigStore/Mem.hs @@ -46,7 +46,9 @@ idPToMem = evState . evEff evEff = reinterpret @_ @(State TypedState) $ \case InsertConfig iw -> modify' (insertConfig iw) - NewHandle _tid -> pure $ IdPHandle "IdP 1" --todo(leif): generate a new handle + NewHandle _tid -> + -- Same handle for all IdPs is good enough, for now + pure $ IdPHandle "IdP 1" GetConfig i -> gets (getConfig i) GetIdPByIssuerV1Maybe issuer -> diff --git a/services/spar/src/Spar/Sem/SAMLUserStore/Mem.hs b/services/spar/src/Spar/Sem/SAMLUserStore/Mem.hs index 8e66f0e732..131ae26681 100644 --- a/services/spar/src/Spar/Sem/SAMLUserStore/Mem.hs +++ b/services/spar/src/Spar/Sem/SAMLUserStore/Mem.hs @@ -19,6 +19,7 @@ module Spar.Sem.SAMLUserStore.Mem ( samlUserStoreToMem, + UserRefOrd, ) where @@ -33,7 +34,7 @@ import qualified SAML2.WebSSO as SAML import Spar.Sem.SAMLUserStore newtype UserRefOrd = UserRefOrd {unUserRefOrd :: SAML.UserRef} - deriving (Eq) + deriving (Eq, Show) instance Ord UserRefOrd where compare (UserRefOrd (SAML.UserRef is ni)) (UserRefOrd (SAML.UserRef is' ni')) = diff --git a/services/spar/test-integration/Test/Spar/Intra/BrigSpec.hs b/services/spar/test-integration/Test/Spar/Intra/BrigSpec.hs index b400a32e92..822a4ee99b 100644 --- a/services/spar/test-integration/Test/Spar/Intra/BrigSpec.hs +++ b/services/spar/test-integration/Test/Spar/Intra/BrigSpec.hs @@ -21,13 +21,15 @@ module Test.Spar.Intra.BrigSpec where import Control.Lens ((^.)) -import Data.Id (Id (Id)) +import Data.Id (Id (Id), UserId) import qualified Data.UUID as UUID import Imports hiding (head) import qualified Spar.Intra.BrigApp as Intra +import qualified Spar.Sem.BrigAccess as BrigAccess +import Test.QuickCheck import Util import qualified Web.Scim.Schema.User as Scim.User -import Wire.API.User (fromEmail) +import Wire.API.User (DeleteUserResult (..), fromEmail) spec :: SpecWith TestEnv spec = do @@ -37,6 +39,12 @@ spec = do it "if a user gets deleted on spar, it will be deleted on brig as well." $ do pendingWith "or deactivated? we should decide what we want here." + describe "deleteBrigUserInternal" $ do + it "does not throw for non-existing users" $ do + uid :: UserId <- liftIO $ generate arbitrary + r <- runSpar $ BrigAccess.deleteUser uid + liftIO $ r `shouldBe` NoUser + describe "getBrigUser" $ do it "return Nothing if n/a" $ do musr <- runSpar $ Intra.getBrigUser Intra.WithPendingInvitations (Id . fromJust $ UUID.fromText "29546d9e-ed5b-11ea-8228-c324b1ea1030") diff --git a/services/spar/test-integration/Util/Scim.hs b/services/spar/test-integration/Util/Scim.hs index 93a2eaa33b..08f72bb750 100644 --- a/services/spar/test-integration/Util/Scim.hs +++ b/services/spar/test-integration/Util/Scim.hs @@ -409,7 +409,7 @@ patchUser_ auth muid patchop spar_ = . acceptScim ) --- | Update a user. +-- | Delete a user. deleteUser_ :: -- | Authentication Maybe ScimToken -> diff --git a/services/spar/test/Test/Spar/Scim/UserSpec.hs b/services/spar/test/Test/Spar/Scim/UserSpec.hs new file mode 100644 index 0000000000..93918199e5 --- /dev/null +++ b/services/spar/test/Test/Spar/Scim/UserSpec.hs @@ -0,0 +1,189 @@ +module Test.Spar.Scim.UserSpec where + +import Arbitrary () +import Brig.Types.Intra +import Brig.Types.User +import Control.Monad.Except (runExceptT) +import Data.Handle (parseHandle) +import Data.Id +import qualified Data.Json.Util +import Imports +import Polysemy +import Polysemy.TinyLog +import Spar.Scim.User (deleteScimUser) +import Spar.Sem.BrigAccess +import Spar.Sem.IdPConfigStore +import Spar.Sem.IdPConfigStore.Mem (TypedState, idPToMem) +import Spar.Sem.SAMLUserStore +import Spar.Sem.SAMLUserStore.Mem (UserRefOrd, samlUserStoreToMem) +import qualified Spar.Sem.ScimExternalIdStore as ScimExternalIdStore +import Spar.Sem.ScimExternalIdStore.Mem (scimExternalIdStoreToMem) +import Spar.Sem.ScimUserTimesStore +import Spar.Sem.ScimUserTimesStore.Mem (scimUserTimesStoreToMem) +import System.Logger (Msg) +import Test.Hspec +import Test.QuickCheck +import Web.Scim.Schema.Error +import Wire.API.User +import qualified Wire.API.User.Identity +import Wire.API.User.Scim +import Wire.Sem.Logger.TinyLog (discardTinyLogs) + +spec :: Spec +spec = describe "deleteScimUser" $ do + it "returns no error when the account was deleted for the first time (or partially)" $ do + tokenInfo <- generate arbitrary + acc <- someActiveUser tokenInfo + r <- + interpretWithBrigAccessMock + (mockBrigForActiveUser acc AccountDeleted) + (deleteUserAndAssertDeletionInSpar acc tokenInfo) + handlerResult r `shouldBe` Right () + it "returns an error when the account was deleted before" $ do + tokenInfo <- generate arbitrary + acc <- someActiveUser tokenInfo + r <- + interpretWithBrigAccessMock + (mockBrigForActiveUser acc AccountAlreadyDeleted) + (deleteUserAndAssertDeletionInSpar acc tokenInfo) + handlerResult r `shouldBe` Left (notFound "user" ((idToText . userId . accountUser) acc)) + it "returns an error when there never was an account" $ do + uid <- generate arbitrary + tokenInfo <- generate arbitrary + r <- + interpretWithBrigAccessMock + mockBrigForNonExistendUser + (runExceptT $ deleteScimUser tokenInfo uid) + handlerResult r `shouldBe` Left (notFound "user" (idToText uid)) + it "returns no error when there was a partially deleted account" $ do + uid <- generate arbitrary + tokenInfo <- generate arbitrary + r <- + interpretWithBrigAccessMock + mockBrigForPartiallyDeletedUser + (runExceptT $ deleteScimUser tokenInfo uid) + handlerResult r `shouldBe` Right () + +deleteUserAndAssertDeletionInSpar :: + forall (r :: EffectRow). + Members + '[ Logger (Msg -> Msg), + BrigAccess, + ScimExternalIdStore.ScimExternalIdStore, + ScimUserTimesStore, + SAMLUserStore, + IdPConfigStore, + Embed IO + ] + r => + UserAccount -> + ScimTokenInfo -> + Sem r (Either ScimError ()) +deleteUserAndAssertDeletionInSpar acc tokenInfo = do + let tid = stiTeam tokenInfo + email = (fromJust . emailIdentity . fromJust . userIdentity . accountUser) acc + uid = (userId . accountUser) acc + ScimExternalIdStore.insert tid email uid + r <- runExceptT $ deleteScimUser tokenInfo uid + lr <- ScimExternalIdStore.lookup tid email + liftIO $ lr `shouldBe` Nothing + pure r + +type EffsWithoutBrigAccess = + '[ IdPConfigStore, + SAMLUserStore, + ScimUserTimesStore, + ScimExternalIdStore.ScimExternalIdStore, + Logger (Msg -> Msg), + Embed IO, + Final IO + ] + +type Effs = BrigAccess ': EffsWithoutBrigAccess + +type InterpreterState = + ( Map (Data.Id.TeamId, Wire.API.User.Identity.Email) Data.Id.UserId, + ( Map Data.Id.UserId (Data.Json.Util.UTCTimeMillis, Data.Json.Util.UTCTimeMillis), + ( Map UserRefOrd UserId, + (Spar.Sem.IdPConfigStore.Mem.TypedState, Either ScimError ()) + ) + ) + ) + +handlerResult :: InterpreterState -> Either ScimError () +handlerResult = snd . snd . snd . snd + +interpretWithBrigAccessMock :: + ( Sem Effs (Either ScimError ()) -> + Sem EffsWithoutBrigAccess (Either ScimError ()) + ) -> + Sem Effs (Either ScimError ()) -> + IO InterpreterState +interpretWithBrigAccessMock mock = + runFinal + . embedToFinal @IO + . discardTinyLogs + . scimExternalIdStoreToMem + . scimUserTimesStoreToMem + . samlUserStoreToMem + . idPToMem + . mock + +mockBrigForNonExistendUser :: + forall (r :: EffectRow). + Members '[Embed IO] r => + Sem (BrigAccess ': r) (Either ScimError ()) -> + Sem r (Either ScimError ()) +mockBrigForNonExistendUser = interpret $ \case + (GetAccount WithPendingInvitations _) -> pure Nothing + (Spar.Sem.BrigAccess.DeleteUser _) -> pure NoUser + _ -> do + liftIO $ expectationFailure $ "Unexpected effect (call to brig)" + error "Throw error here to avoid implementation of all cases." + +mockBrigForPartiallyDeletedUser :: + forall (r :: EffectRow). + Members '[Embed IO] r => + Sem (BrigAccess ': r) (Either ScimError ()) -> + Sem r (Either ScimError ()) +mockBrigForPartiallyDeletedUser = interpret $ \case + (GetAccount WithPendingInvitations _) -> pure Nothing + (Spar.Sem.BrigAccess.DeleteUser _) -> pure AccountDeleted + _ -> do + liftIO $ expectationFailure $ "Unexpected effect (call to brig)" + error "Throw error here to avoid implementation of all cases." + +mockBrigForActiveUser :: + forall (r :: EffectRow). + Members '[Embed IO] r => + UserAccount -> + DeleteUserResult -> + Sem (BrigAccess ': r) (Either ScimError ()) -> + Sem r (Either ScimError ()) +mockBrigForActiveUser acc deletionResult = interpret $ \case + (GetAccount WithPendingInvitations uid) -> + if uid == (userId . accountUser) acc + then pure $ Just acc + else pure Nothing + (Spar.Sem.BrigAccess.DeleteUser _) -> pure deletionResult + _ -> do + liftIO $ expectationFailure $ "Unexpected effect (call to brig)" + error "Throw error here to avoid implementation of all cases." + +someActiveUser :: ScimTokenInfo -> IO UserAccount +someActiveUser tokenInfo = do + user <- generate arbitrary + pure $ + UserAccount + { accountStatus = Active, + accountUser = + user + { userDisplayName = Name "Some User", + userAccentId = defaultAccentId, + userPict = noPict, + userAssets = [], + userHandle = parseHandle "some-handle", + userIdentity = (Just . EmailIdentity . fromJust . parseEmail) "someone@wire.com", + userTeam = Just $ stiTeam tokenInfo + } + } diff --git a/tools/db/assets/src/Assets/Lib.hs b/tools/db/assets/src/Assets/Lib.hs index f150e90ed6..c4947b4d52 100644 --- a/tools/db/assets/src/Assets/Lib.hs +++ b/tools/db/assets/src/Assets/Lib.hs @@ -175,12 +175,12 @@ instance Show Result where <> "\nnum_invalid_assets: " <> show (length i) <> "\ninvalid_assets:\n" - <> concat (showRow <$> i) + <> concatMap showRow i where showRow (uid, Nothing) = " - user_id: " <> show uid <> "\n" showRow (uid, Just as) = " - user_id: " <> show uid <> "\n" <> showAssets as showAsset a = " key: " <> show (txtAssetKey a) <> "\n" - showAssets assets = concat $ showAsset <$> assets + showAssets assets = concatMap showAsset assets instance Semigroup Result where (<>) (Result n1 v1 i1) (Result n2 v2 i2) = diff --git a/tools/db/move-team/src/Work.hs b/tools/db/move-team/src/Work.hs index c5e69475e9..a10a9a13f8 100644 --- a/tools/db/move-team/src/Work.hs +++ b/tools/db/move-team/src/Work.hs @@ -101,7 +101,7 @@ runGalleyTeamMembers env@Env {..} = handleTeamMembers :: Env -> (Int32, [RowGalleyTeamMember]) -> IO [RowGalleyTeamMember] handleTeamMembers env@Env {..} (i, members) = do Log.info envLogger (Log.field "number of team members loaded: " (show (i * envPageSize))) - let uids = catMaybes $ fmap Id . view _2 <$> members + let uids = mapMaybe (fmap Id . view _2) members appendJsonLines (envTargetPath "brig.clients") (readBrigClients env uids) appendJsonLines (envTargetPath "brig.connection") (readBrigConnection env uids) @@ -131,7 +131,7 @@ runGalleyTeamConv env@Env {..} = handleTeamConv :: Env -> (Int32, [RowGalleyTeamConv]) -> IO [RowGalleyTeamConv] handleTeamConv env@Env {..} (i, convs) = do Log.info envLogger (Log.field "number of team convs loaded: " (show (i * envPageSize))) - let cids = catMaybes $ fmap Id . view _2 <$> convs + let cids = mapMaybe (fmap Id . view _2) convs appendJsonLines (envTargetPath "galley.conversation") (readGalleyConversation env cids) appendJsonLines (envTargetPath "galley.member") (readGalleyMember env cids) pure convs diff --git a/tools/hlint.sh b/tools/hlint.sh new file mode 100755 index 0000000000..35a7cc7dcc --- /dev/null +++ b/tools/hlint.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +usage() { echo "Usage: $0 -f [all, changeset] -m [check, inplace]" 1>&2; exit 1; } + +files='' +check=true + +while getopts ':f:m:' opt + do + case $opt in + f) f=${OPTARG} + if [ "$f" = "all" ]; then + files=$(find libs/ services/ -not -path "*/test/*" -name "*.hs") + echo "WARNING: not linting tests." + elif [ "$f" = "pr" ]; then + files=$(git diff --name-only origin/develop... | grep \.hs\$) + echo "WARNING: linting test files with changes. This may lead to some hard to fix warnings/errors, it is safe to ignore those!" + elif [ "$f" = "changeset" ]; then + files=$(git diff --name-only HEAD | grep \.hs\$) + echo "WARNING: linting test files with changes. This may lead to some hard to fix warnings/errors, it is safe to ignore those!" + else + usage + fi + ;; + m) m=${OPTARG} + if [ "$m" = "inplace" ]; then + check=false + elif [ "$m" = "check" ]; then + check=true + else + usage + fi + ;; + *) usage;; + esac +done + +if [ -z "${f}" ] || [ -z "${m}" ]; then + usage +fi + +count=$(echo "$files" | grep -c -v -e '^[[:space:]]*$') + +echo "Analysing $count file(s)…" + +for f in $files +do + echo "$f" + if [ $check = true ]; then + hlint --no-summary "$f" + else + hlint --refactor --refactor-options="--inplace" "$f" + fi +done diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index 0912ec7512..af93fae30e 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -49,7 +49,6 @@ import qualified Galley.Types.Teams.Intra as Team import Imports hiding (head) import Network.HTTP.Types import Network.Wai -import Network.Wai.Handler.Warp import qualified Network.Wai.Middleware.Gzip as GZip import Network.Wai.Predicate hiding (Error, reason, setStatus) import Network.Wai.Routing hiding (trace) @@ -78,7 +77,7 @@ start :: Opts -> IO () start o = do e <- newEnv o s <- Server.newSettings (server e) - runSettings s (pipeline e) + Server.runSettingsWithShutdown s (pipeline e) Nothing where server e = Server.defaultServer (unpack $ stern o ^. epHost) (stern o ^. epPort) (e ^. applog) (e ^. metrics) pipeline e = GZip.gzip GZip.def $ serve e