diff --git a/.envrc b/.envrc index 7fcbc7c74b..1d49d232ce 100644 --- a/.envrc +++ b/.envrc @@ -5,7 +5,11 @@ # or any of the `default.nix` files change. We do this by adding all these files # to the nix store and using the store paths as a cache key. -store_paths=$(find . -name default.nix | grep -v '^./nix' | grep -v '^./dist-newstyle' | xargs nix-store --add ./nix) +nix_files=$(find . -name '*.nix' | grep -v '^./dist-newstyle') +for nix_file in $nix_files; do + watch_file "$nix_file" +done +store_paths=$(echo "$nix_files" | xargs nix-store --add ./nix) layout_dir=$(direnv_layout_dir) env_dir=./.env diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c0b6fda77..e1b40c398d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,25 @@ on: branches: [master, develop] jobs: + treefmt: + name: Run treefmt + environment: cachix # for secrets + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - uses: cachix/install-nix-action@v14.1 + - uses: cachix/cachix-action@v10 + with: + name: wire-server + signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}' + authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' + - name: Install treefmt and nixpkgs-fmt (without pulling in all of dev-env) + run: nix-env -if nix/default.nix -iA pkgs.treefmt pkgs.nixpkgs-fmt pkgs.shellcheck + - name: Run treefmt + run: treefmt + build-docs: name: Build docs environment: cachix diff --git a/.gitmodules b/.gitmodules index cc9c3a9e10..ad996d503f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,3 @@ -[submodule "services/nginz/third_party/headers-more-nginx-module"] - path = services/nginz/third_party/headers-more-nginx-module - url = https://github.com/openresty/headers-more-nginx-module.git -[submodule "services/nginz/third_party/nginx-module-vts"] - path = services/nginz/third_party/nginx-module-vts - url = https://github.com/vozlt/nginx-module-vts.git [submodule "libs/wire-message-proto-lens/generic-message-proto"] path = libs/wire-message-proto-lens/generic-message-proto url = https://github.com/wireapp/generic-message-proto diff --git a/CHANGELOG.md b/CHANGELOG.md index f0b045c000..2e9a72d014 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,82 @@ +# [2022-11-30] (Chart Release 4.27.0) + +## Release notes + + +* This realease migrates data from `galley.member_client` to `galley.mls_group_member_client`. When upgrading wire-server no manual steps are required. (#2859) + + +## API changes + + +* Support MLS self-conversations via a new endpoint `GET /conversations/mls-self`. This removes the `PUT` counterpart introduced in #2730 (#2839) + +* List the MLS self-conversation automatically without needing to call `GET /conversations/mls-self` first (#2856) + + +## Features + + +* A team member's role can now be provisioned via SCIM (#2851, #2855) + + +## Bug fixes and other updates + + +* Avoid client deletion edge case condition which can lead to inconsistent data between brig and galley's clients tables. (#2830) + +* Do not list MLS self-conversation in client API v1 and v2 if it exists (#2872) + +* Prevention of storing unnecessary data in the database if adding a bot to a conversation fails. (#2870) + +* Fix bug in MLS user removal from conversation: the list of removed clients has to be compared with those in the conversation, not the list of *all* clients of that user (#2817) + +* For sftd/coturn/restund, fixed a bug in external ip address lookup, in case Kubernetes Node Name doesn't equal hostname. (#2837) + +* Requesting a new token with the client_id now works correctly when the old token is part of the request (#2860) + + +## Internal changes + + +* Add tests for invitation urls in team invitation responses. These depend on the settings of galley. (#2797) + +* Remove support for compiling local docker images with buildah. Nix is used to build docker images these days (#2822) + +* bump nginx-module-vts from v0.1.15 to v0.2.1 (#2827) + +* Nix-created docker images: add some debugging tools in the containers, and add 'make build-image-' for convenience (#2829) + +* Split galley API routes and handler definitions into several modules (#2820) + +* Default intraListing to true. This means that the list of clients, so far saved in both brig's and galley's databases, will still be written to both, but only read from brig's database. This avoids cases where these two tables go out of sync. Brig becomes the source of truth for clients. In the future, if this holds, code and data for galley's clients table can be removed. (#2847) + +* Build nginz and nginz_disco docker images using nix (#2796) + +* Bump nixpkgs to latest unstable. Stop using forked nixpkgs. (#2828) + +* Brig calling API is now migrated to servant (#2815) + +* Fixed flaky feature TTL integration test (#2823) + +* Brig teams API is now migrated to servant (#2824) + +* Backoffice Swagger 2.x docs is exposed on `/` and the old Swagger has been removed. Backoffice helm chart only runs stern without an extra nginx. (#2846) + +* Stern API endpoint `GET ejpd-info` has now the correct HTTP method (#2850) + +* External commits: add additional checks (#2852) + +* Golden tests for conversation and feature config event schemas (#2861) + +* Refactor and simplify MLS message handling logic (#2844) + +* Replay external backend proposals after forwarding external commits. + One column added to Galley's mls_proposal_refs. (#2842) + +* Use treefmt to ensure consistent formatting of .nix files, use for shellcheck too (#2831) + + # [2022-11-03] (Chart Release 4.26.0) ## Release notes @@ -63,8 +142,6 @@ * Convert brig's auth endpoints to servant (#2750) -* bump nginx-module-vts from v0.1.15 to v0.2.1 (#2793) - * Remove deprecated table for storing scim external_ids. Data has been migrated away in [release 2021-03-21 (Chart Release 2.103.0)](https://github.com/wireapp/wire-server/releases/tag/v2021-03-21) (see `/services/spar/migrate-data/src/Spar/DataMigration/V1_ExternalIds.hs`); last time it has been touched in production is before upgrade to [release 2021-03-23 (Chart Release 2.104.0)](https://github.com/wireapp/wire-server/releases/tag/v2021-03-23). (#2768) diff --git a/Makefile b/Makefile index a607d23bfe..9822fb1866 100644 --- a/Makefile +++ b/Makefile @@ -14,9 +14,7 @@ CHARTS_INTEGRATION := wire-server databases-ephemeral redis-cluster fake-aws # this list could be generated from the folder names under ./charts/ like so: # CHARTS_RELEASE := $(shell find charts/ -maxdepth 1 -type d | xargs -n 1 basename | grep -v charts) CHARTS_RELEASE := wire-server redis-ephemeral redis-cluster databases-ephemeral fake-aws fake-aws-s3 fake-aws-sqs aws-ingress fluent-bit kibana backoffice calling-test demo-smtp elasticsearch-curator elasticsearch-external elasticsearch-ephemeral minio-external cassandra-external nginx-ingress-controller nginx-ingress-services reaper sftd restund coturn inbucket -BUILDAH_PUSH ?= 0 KIND_CLUSTER_NAME := wire-server -BUILDAH_KIND_LOAD ?= 1 package ?= all EXE_SCHEMA := ./dist/$(package)-schema @@ -41,7 +39,6 @@ init: # Build all Haskell services and executables, run unit tests .PHONY: install install: init - cabal update cabal build all ./hack/bin/cabal-run-all-tests.sh ./hack/bin/cabal-install-artefacts.sh all @@ -110,12 +107,16 @@ ghcid: # Used by CI .PHONY: lint-all -lint-all: formatc hlint-check-all shellcheck check-local-nix-derivations +lint-all: formatc hlint-check-all check-local-nix-derivations treefmt .PHONY: hlint-check-all hlint-check-all: ./tools/hlint.sh -f all -m check +.PHONY: hlint-inplace-all +hlint-inplace-all: + ./tools/hlint.sh -f all -m inplace + .PHONY: hlint-check-pr hlint-check-pr: ./tools/hlint.sh -f pr -m check @@ -124,11 +125,6 @@ hlint-check-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 @@ -156,7 +152,12 @@ format: # formats all Haskell files even if local changes are not committed to git .PHONY: formatf formatf: - ./tools/ormolu.sh -f + ./tools/ormolu.sh -f pr + +# formats all Haskell files even if local changes are not committed to git +.PHONY: formatf-all +formatf-all: + ./tools/ormolu.sh -f all # checks that all Haskell files are formatted; fail if a `make format` run is needed. .PHONY: formatc @@ -173,13 +174,23 @@ add-license: @echo "" @echo "you might want to run 'make formatf' now to make sure ormolu is happy" -.PHONY: shellcheck -shellcheck: - ./hack/bin/shellcheck.sh +.PHONY: treefmt +treefmt: + treefmt ################################# ## docker targets +.PHONY: build-image-% +build-image-%: + nix-build ./nix -A wireServer.imagesNoDocs.$(*) && \ + ./result | docker load | tee /tmp/imageName-$(*) && \ + imageName=$$(grep quay.io /tmp/imageName-$(*) | awk '{print $$3}') && \ + echo 'You can run your image locally using' && \ + echo " docker run -it --entrypoint bash $$imageName" && \ + echo 'or upload it using' && \ + echo " docker push $$imageName" + .PHONY: upload-images upload-images: ./hack/bin/upload-images.sh imagesNoDocs @@ -200,7 +211,10 @@ git-add-cassandra-schema: db-reset git-add-cassandra-schema-impl .PHONY: git-add-cassandra-schema-impl git-add-cassandra-schema-impl: $(eval CASSANDRA_CONTAINER := $(shell docker ps | grep '/cassandra:' | perl -ne '/^(\S+)\s/ && print $$1')) - ( echo '-- automatically generated with `make git-add-cassandra-schema`' ; docker exec -i $(CASSANDRA_CONTAINER) /usr/bin/cqlsh -e "DESCRIBE schema;" ) > ./cassandra-schema.cql + ( echo '-- automatically generated with `make git-add-cassandra-schema`'; \ + docker exec -i $(CASSANDRA_CONTAINER) /usr/bin/cqlsh -e "DESCRIBE schema;" ) \ + | sed "s/CREATE TABLE galley_test.member_client/-- NOTE: this table is unused. It was replaced by mls_group_member_client\nCREATE TABLE galley_test.member_client/g" \ + > ./cassandra-schema.cql git add ./cassandra-schema.cql .PHONY: cqlsh @@ -409,24 +423,6 @@ upload-charts: charts-release echo-release-charts: @echo ${CHARTS_RELEASE} -.PHONY: buildah-docker -buildah-docker: buildah-docker-nginz - ./hack/bin/buildah-compile.sh all - BUILDAH_PUSH=${BUILDAH_PUSH} KIND_CLUSTER_NAME=${KIND_CLUSTER_NAME} BUILDAH_KIND_LOAD=${BUILDAH_KIND_LOAD} ./hack/bin/buildah-make-images.sh - -.PHONY: buildah-docker-nginz -buildah-docker-nginz: - BUILDAH_PUSH=${BUILDAH_PUSH} KIND_CLUSTER_NAME=${KIND_CLUSTER_NAME} BUILDAH_KIND_LOAD=${BUILDAH_KIND_LOAD} ./hack/bin/buildah-make-images-nginz.sh - -.PHONY: buildah-docker-% -buildah-docker-%: - ./hack/bin/buildah-compile.sh $(*) - BUILDAH_PUSH=${BUILDAH_PUSH} EXECUTABLES=$(*) KIND_CLUSTER_NAME=${KIND_CLUSTER_NAME} BUILDAH_KIND_LOAD=${BUILDAH_KIND_LOAD} ./hack/bin/buildah-make-images.sh - -.PHONY: buildah-clean -buildah-clean: - ./hack/bin/buildah-clean.sh - .PHONY: kind-cluster kind-cluster: kind create cluster --name $(KIND_CLUSTER_NAME) diff --git a/cassandra-schema.cql b/cassandra-schema.cql index 322e27ef81..e9e0b6c8b9 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -364,6 +364,7 @@ CREATE TABLE galley_test.group_id_conv_id ( AND read_repair_chance = 0.0 AND speculative_retry = '99PERCENTILE'; +-- NOTE: this table is unused. It was replaced by mls_group_member_client CREATE TABLE galley_test.member_client ( conv uuid, user_domain text, @@ -430,6 +431,29 @@ CREATE TABLE galley_test.conversation_codes ( AND read_repair_chance = 0.0 AND speculative_retry = '99PERCENTILE'; +CREATE TABLE galley_test.mls_group_member_client ( + group_id blob, + user_domain text, + user uuid, + client text, + key_package_ref blob, + PRIMARY KEY (group_id, 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.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 + 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 TABLE galley_test.clients ( user uuid PRIMARY KEY, clients set @@ -550,6 +574,7 @@ CREATE TABLE galley_test.mls_proposal_refs ( group_id blob, epoch bigint, ref blob, + origin int, proposal blob, PRIMARY KEY (group_id, epoch, ref) ) WITH CLUSTERING ORDER BY (epoch ASC, ref ASC) diff --git a/changelog.d/5-internal/bump-nginx-module-vts b/changelog.d/5-internal/bump-nginx-module-vts deleted file mode 100644 index a2e4ab6582..0000000000 --- a/changelog.d/5-internal/bump-nginx-module-vts +++ /dev/null @@ -1 +0,0 @@ -bump nginx-module-vts from v0.1.15 to v0.2.1 (#2827) diff --git a/charts/backoffice/templates/configmap.yaml b/charts/backoffice/templates/configmap.yaml index a0a2b09e82..e43214497f 100644 --- a/charts/backoffice/templates/configmap.yaml +++ b/charts/backoffice/templates/configmap.yaml @@ -7,10 +7,10 @@ data: logNetStrings: True # log using netstrings encoding: # http://cr.yp.to/proto/netstrings.txt logLevel: {{ .Values.config.logLevel }} + logFormat: {{ .Values.config.logFormat }} stern: host: 0.0.0.0 - port: 8081 - # Cannot listen on the same port as the frontend + port: 8080 brig: host: brig port: 8080 @@ -28,152 +28,3 @@ data: ibis: host: {{ .Values.config.ibisHost }} port: 8080 - nginx.conf: | - worker_processes 1; - worker_rlimit_nofile 1024; - pid /tmp/nginx.pid; - - events { - worker_connections 1024; - multi_accept off; - } - - http { - # - # Sockets - # - - sendfile on; - tcp_nopush on; - tcp_nodelay on; - - # - # Timeouts - # - - client_body_timeout 60s; - client_header_timeout 60s; - keepalive_timeout 30s; - send_timeout 60s; - - # - # Mapping for websocket connections - # - - map $http_upgrade $connection_upgrade { - websocket upgrade; - default ''; - } - - # - # Body - # - - client_max_body_size 16M; - - # - # Headers - # - - ignore_invalid_headers off; - - server_tokens off; - server_names_hash_bucket_size 64; - server_name_in_redirect off; - types_hash_max_size 2048; - - large_client_header_buffers 4 8k; - - # - # MIME - # - - include /etc/nginx/mime.types; - default_type application/octet-stream; - - # - # Logging - # - - access_log /dev/stdout; - error_log stderr; - - # - # Gzip - # - - gzip on; - gzip_disable msie6; - gzip_vary on; - gzip_proxied any; - gzip_comp_level 6; - gzip_buffers 16 8k; - gzip_http_version 1.1; - gzip_min_length 1024; - gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; - - # - # SSL - # - - add_header Strict-Transport-Security max-age=31536000; - - map $scheme $server_https { - default off; - https on; - } - - ssl_session_cache builtin:1000 shared:SSL:10m; - ssl_session_timeout 5m; - ssl_prefer_server_ciphers on; - ssl_protocols TLSv1.2 TLSv1.3; - # NOTE: These are some sane defaults (compliant to TR-02102-2), you may want to overrride them on your own installation - # For TR-02102-2 see https://www.bsi.bund.de/SharedDocs/Downloads/EN/BSI/Publications/TechGuidelines/TG02102/BSI-TR-02102-2.html - # As a Wire employee, for Wire-internal discussions and context see - # * https://wearezeta.atlassian.net/browse/FS-33 - # * https://wearezeta.atlassian.net/browse/FS-444 - ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384'; # for TLS 1.2 - # FUTUREWORK: upgrade nginx used for the backoffice to support ssl_conf_command (i.e. build a new backoffice-frontend), then uncomment below - # ssl_conf_command Ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384; # for TLS 1.3 - server { - listen {{ .Values.service.internalPort }}; - - # Backoffice code at /var/www - location / { - # NOTE: nginx's root is defined at compile time! This means that these roots - # depend on the values at the time of compilation for nginx, namely --conf-path - # and --prefix. If you don't use _full_ paths as root, they get resolved depending - # those prefixes... they really need to fix this! So we just assume that these - # paths can be created on any filesystem... - root /var/www/swagger-ui; - index index.html; - } - - # resources.json is needed by the backoffice app - location /api-docs { - # This asssumes the default location for the backoffice! - root /var/www/swagger-ui; - index resources.json; - } - - # The liveness/healthiness depends on stern - location /i/status { - proxy_pass http://localhost:8081; - proxy_http_version 1.1; - } - - rewrite ^/api-docs/stern /stern/api-docs?base_url={{ .Values.baseUrl }}/api break; - - # This path is used by swagger to fetch the docs from the service - location /stern { - proxy_pass http://localhost:8081; - proxy_http_version 1.1; - } - - # All others requests get proxied to stern, without the api prefix (which was added in the base_url above) - location ~ ^/api/(.*)$ { - proxy_pass http://localhost:8081/$1$is_args$query_string; - proxy_http_version 1.1; - } - } - } diff --git a/charts/backoffice/templates/deployment.yaml b/charts/backoffice/templates/deployment.yaml index 172e3fc135..6cc5c2df66 100644 --- a/charts/backoffice/templates/deployment.yaml +++ b/charts/backoffice/templates/deployment.yaml @@ -32,20 +32,12 @@ spec: name: "backoffice" containers: - name: stern - image: "{{ .Values.images.stern.repository }}:{{ .Values.images.stern.tag }}" - imagePullPolicy: {{ default "" .Values.images.stern.pullPolicy | quote }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ default "" .Values.image.pullPolicy | quote }} volumeMounts: - name: "backoffice-config" mountPath: /etc/wire/stern/conf/stern.yaml subPath: stern.yaml - - name: backoffice-frontend - image: "{{ .Values.images.frontend.repository }}:{{ .Values.images.frontend.tag }}" - imagePullPolicy: {{ default "" .Values.images.frontend.pullPolicy | quote }} - volumeMounts: - - name: "backoffice-config" - # We don't want to override existing files under /etc/nginx except for nginx.conf - mountPath: "/etc/nginx/nginx.conf" - subPath: nginx.conf ports: - containerPort: {{ .Values.service.internalPort }} livenessProbe: diff --git a/charts/backoffice/values.yaml b/charts/backoffice/values.yaml index bbdb1e881e..02c190d1bb 100644 --- a/charts/backoffice/values.yaml +++ b/charts/backoffice/values.yaml @@ -1,13 +1,8 @@ replicaCount: 1 -images: - frontend: - repository: quay.io/wire/backoffice-frontend - tag: 2.87.0 - pullPolicy: IfNotPresent - stern: - repository: quay.io/wire/stern - tag: do-not-use - pullPolicy: IfNotPresent +image: + repository: quay.io/wire/stern + tag: do-not-use + pullPolicy: IfNotPresent service: internalPort: 8080 externalPort: 8080 @@ -19,6 +14,7 @@ resources: memory: 50Mi config: logLevel: Info + logFormat: StructuredJSON galebHost: galeb.integrations ibisHost: ibis.integrations baseUrl: http://localhost:8080 diff --git a/charts/coturn/templates/statefulset.yaml b/charts/coturn/templates/statefulset.yaml index 8ab28192b5..37ce6aef3e 100644 --- a/charts/coturn/templates/statefulset.yaml +++ b/charts/coturn/templates/statefulset.yaml @@ -60,6 +60,11 @@ spec: volumeMounts: - name: external-ip mountPath: /external-ip + env: + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName command: - /bin/sh - -c @@ -67,10 +72,10 @@ spec: set -e # In the cloud, this setting is available to indicate the true IP address - addr=$(kubectl get node $HOSTNAME -ojsonpath='{.status.addresses[?(@.type=="ExternalIP")].address}') + addr=$(kubectl get node $NODE_NAME -ojsonpath='{.status.addresses[?(@.type=="ExternalIP")].address}') # On on-prem we allow people to set "wire.com/external-ip" to override this if [ -z "$addr" ]; then - addr=$(kubectl get node $HOSTNAME -ojsonpath='{.metadata.annotations.wire\.com/external-ip}') + addr=$(kubectl get node $NODE_NAME -ojsonpath='{.metadata.annotations.wire\.com/external-ip}') fi echo -n "$addr" | tee /dev/stderr > /external-ip/ip containers: diff --git a/charts/galley/templates/configmap.yaml b/charts/galley/templates/configmap.yaml index c5ce757ace..e5a4f7864a 100644 --- a/charts/galley/templates/configmap.yaml +++ b/charts/galley/templates/configmap.yaml @@ -48,7 +48,7 @@ data: settings: httpPoolSize: {{ .settings.httpPoolSize }} - intraListing: false + intraListing: {{ .settings.intraListing }} maxTeamSize: {{ .settings.maxTeamSize }} maxConvSize: {{ .settings.maxConvSize }} {{- if .settings.maxFanoutSize }} diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index a044931d76..15276dc945 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -28,6 +28,7 @@ config: maxTeamSize: 10000 exposeInvitationURLsTeamAllowlist: [] maxConvSize: 500 + intraListing: true # Before making indexedBillingTeamMember true while upgrading, please # refer to notes here: https://github.com/wireapp/wire-server-deploy/releases/tag/v2020-05-15 indexedBillingTeamMember: false diff --git a/charts/nginz/templates/conf/_nginx.conf.tpl b/charts/nginz/templates/conf/_nginx.conf.tpl index 7b28c77493..29f8e28e8f 100644 --- a/charts/nginz/templates/conf/_nginx.conf.tpl +++ b/charts/nginz/templates/conf/_nginx.conf.tpl @@ -37,6 +37,8 @@ http { types_hash_max_size 2048; map_hash_bucket_size 128; + variables_hash_bucket_size 256; + server_names_hash_bucket_size 64; server_name_in_redirect off; diff --git a/charts/restund/templates/statefulset.yaml b/charts/restund/templates/statefulset.yaml index 6063b38d60..da29825c4d 100644 --- a/charts/restund/templates/statefulset.yaml +++ b/charts/restund/templates/statefulset.yaml @@ -53,6 +53,11 @@ spec: volumeMounts: - name: external-ip mountPath: /external-ip + env: + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName command: - /bin/sh - -c @@ -60,11 +65,11 @@ spec: set -e # In the cloud, this setting is available to indicate the true IP address - addr=$(kubectl get node $HOSTNAME -ojsonpath='{.status.addresses[?(@.type=="ExternalIP")].address}') + addr=$(kubectl get node $NODE_NAME -ojsonpath='{.status.addresses[?(@.type=="ExternalIP")].address}') # On on-prem we allow people to set "wire.com/external-ip" to override this if [ -z "$addr" ]; then - addr=$(kubectl get node $HOSTNAME -ojsonpath='{.metadata.annotations.wire\.com/external-ip}') + addr=$(kubectl get node $NODE_NAME -ojsonpath='{.metadata.annotations.wire\.com/external-ip}') fi echo -n "$addr" | tee /dev/stderr > /external-ip/ip containers: diff --git a/charts/sftd/templates/statefulset.yaml b/charts/sftd/templates/statefulset.yaml index 3027ccf601..e9329922bf 100644 --- a/charts/sftd/templates/statefulset.yaml +++ b/charts/sftd/templates/statefulset.yaml @@ -43,6 +43,11 @@ spec: volumeMounts: - name: external-ip mountPath: /external-ip + env: + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName command: - /bin/sh - -c @@ -50,11 +55,11 @@ spec: set -e # In the cloud, this setting is available to indicate the true IP address - addr=$(kubectl get node $HOSTNAME -ojsonpath='{.status.addresses[?(@.type=="ExternalIP")].address}') + addr=$(kubectl get node $NODE_NAME -ojsonpath='{.status.addresses[?(@.type=="ExternalIP")].address}') # On on-prem we allow people to set "wire.com/external-ip" to override this if [ -z "$addr" ]; then - addr=$(kubectl get node $HOSTNAME -ojsonpath='{.metadata.annotations.wire\.com/external-ip}') + addr=$(kubectl get node $NODE_NAME -ojsonpath='{.metadata.annotations.wire\.com/external-ip}') fi echo -n "$addr" | tee /dev/stderr > /external-ip/ip - name: get-multi-sft-config diff --git a/docs/README.md b/docs/README.md index dbcbe4e930..adf1c9be46 100644 --- a/docs/README.md +++ b/docs/README.md @@ -69,6 +69,14 @@ and Direnv installed. This folder contains another `.envrc` file that adds all the binaries needed to build the docs to `$PATH`. +In short, when you `cd` into this folder, you should see this message: + +```sh +direnv: error wire-server/docs/.envrc is blocked. Run `direnv allow` to approve its content +``` + +Run `direnv allow` to allow the `.envrc` file to modify your environment. Then, you should have everything (binaries, environment variables) needed to build the docs. + ### Generating html output (one-off) ``` diff --git a/docs/src/developer/developer/dependencies.md b/docs/src/developer/developer/dependencies.md index 15896f6d75..68db64e56d 100644 --- a/docs/src/developer/developer/dependencies.md +++ b/docs/src/developer/developer/dependencies.md @@ -254,12 +254,6 @@ Requirements: In both cases, you need to adjust the various integration configuration files and names so that this can work. -## Buildah (optional) - -[Buildah](https://buildah.io/) is used for local docker image creation during development. See [buildah installation](https://github.com/containers/buildah/blob/master/install.md) - -See `make buildah-docker` for an entry point here. - ## Helm chart development, integration tests in kubernetes You need `kubectl`, `helm`, `helmfile`, and a valid kubernetes context. Refer to https://docs.wire.com for details. diff --git a/docs/src/developer/developer/how-to.md b/docs/src/developer/developer/how-to.md index 3c98cfa869..0ed606399b 100644 --- a/docs/src/developer/developer/how-to.md +++ b/docs/src/developer/developer/how-to.md @@ -108,10 +108,10 @@ FUTUREWORK: this process is in development (update this section after it's confi ##### (i) Build images -1. Ensure `buildah` is available on your system. -2. Compile the image using `make buildah-docker`. This will try to upload the - images into a `kind` cluster. If you'd prefer uploading images to quay.io, - you can run it with `make buildah-docker BUILDAH_PUSH=1 BUILDAH_KIND_LOAD=0` +(FUTUREWORK: implement a convenient shortcut to build images without actually uploading them also) +``` +make upload-images-dev +``` ##### (ii) Run tests in kind @@ -120,7 +120,6 @@ FUTUREWORK: this process is in development (update this section after it's confi 2. Run tests using `make kind-integration-test`. 3. Run end2end integration tests: `make kind-integration-e2e`. -NOTE: debug this process further as some images (e.g. nginz) are missing from the default buildah steps. * Implement re-tagging development tags as your user tag? #### 2.4 Deploy your local code to a kubernetes cluster @@ -138,9 +137,7 @@ make kube-integration-setup Then build and push the `brig` image by running ``` -export DOCKER_TAG_LOCAL_BUILD=$USER -hack/bin/buildah-compile.sh all -DOCKER_TAG=$DOCKER_TAG_LOCAL_BUILD EXECUTABLES=brig BUILDAH_PUSH=1 ./hack/bin/buildah-make-images.sh +#FUTUREWORK ``` To update the release with brig's local image run diff --git a/docs/src/how-to/associate/deeplink.rst b/docs/src/how-to/associate/deeplink.rst index 8ca5fe05a4..930cd4ad12 100644 --- a/docs/src/how-to/associate/deeplink.rst +++ b/docs/src/how-to/associate/deeplink.rst @@ -20,6 +20,9 @@ Supported client apps: - iOS - Android +.. note:: + Wire deeplinks can be used to redirect a mobile (Android, iOS) Wire app to a specific backend URL. Deeplinks have no further ability implemented at this stage. + Connecting to a custom backend utilizing a Deep Link ---------------------------------------------------- diff --git a/docs/src/how-to/install/planning.rst b/docs/src/how-to/install/planning.rst index ec186ddd45..29e84f97a6 100644 --- a/docs/src/how-to/install/planning.rst +++ b/docs/src/how-to/install/planning.rst @@ -69,6 +69,8 @@ VM on a cloud provider, etc. Make sure they run ubuntu 18.04. Ensure that your VMs have IP addresses that do not change. +Avoid `10.x.x.x` network address schemes, and instead use something like `192.168.x.x` or `172.x.x.x`. This is because internally, Kubernetes already uses a `10.x.x.x` address scheme, creating a potential conflict. + Next steps for high-available production installation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/src/understand/sft.rst b/docs/src/understand/sft.rst index 988e1710a6..aec41fe742 100644 --- a/docs/src/understand/sft.rst +++ b/docs/src/understand/sft.rst @@ -154,7 +154,7 @@ runs on a dedicated port number which is not used for regular TURN traffic. Unde configuration, only that single IP address and port is exposed for each federated TURN server with all SFT traffic multiplexed over the connection. The diagram below shows this scenario. Note that this TURN DTLS multiplexing is only used for SFT to SFT -communication and does not affect the connectivity requirements for normal one-on-one +communication into federated group calls, and does not affect the connectivity requirements for normal one-on-one calls. .. figure:: img/multi-sft-turn-dtls.png diff --git a/hack/bin/buildah-clean.sh b/hack/bin/buildah-clean.sh deleted file mode 100755 index d0f49026e3..0000000000 --- a/hack/bin/buildah-clean.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -set -e -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -TOP_LEVEL="$(cd "$DIR/../.." && pwd)" - -rm -rf "$TOP_LEVEL"/buildah -# buildah rm wire-server-dev -buildah rm output diff --git a/hack/bin/buildah-compile.sh b/hack/bin/buildah-compile.sh deleted file mode 100755 index b49871aa36..0000000000 --- a/hack/bin/buildah-compile.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash - -# This compiles wire-server inside an ubuntu-based container based on quay.io/wire/ubuntu20-builder. -# the tool 'buildah' is used to mount some folders in, and to -# keep the caches of /.root/.cabal and dist-newstyle (renamed to avoid conflicts) for the next compilation - -# After compilation, ./buildah-make-images.sh can be used -# to bake individual executables into their respective docker images used by kubernetes. - -set -ex - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -TOP_LEVEL="$(cd "$DIR/../.." && pwd)" - -# Note: keep the following names and paths in sync with the other buildah-* scripts. -mkdir -p "$TOP_LEVEL"/buildah/dot-cabal -mkdir -p "$TOP_LEVEL"/buildah/dist-newstyle -mkdir -p "$TOP_LEVEL"/buildah/dist - -CONTAINER_NAME=wire-server-dev - -# check for the existence of; or create a working container -buildah containers | awk '{print $5}' | grep "$CONTAINER_NAME" \ - || buildah from --name "$CONTAINER_NAME" -v "${TOP_LEVEL}":/src --pull quay.io/wire/ubuntu20-builder:develop - -# copy /root/.cabal out of the container -ls "$TOP_LEVEL"/buildah/dot-cabal/store 2> /dev/null \ - || buildah run "$CONTAINER_NAME" -- cp -a /root/.cabal/. /src/buildah/dot-cabal - -buildah run "$CONTAINER_NAME" -- /src/hack/bin/buildah-inside.sh "$@" diff --git a/hack/bin/buildah-inside.sh b/hack/bin/buildah-inside.sh deleted file mode 100755 index a750e21df8..0000000000 --- a/hack/bin/buildah-inside.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -# This script is meant to be run from inside a buildah container. See buildah-compile.sh for details. - -set -e -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -TOP_LEVEL="$(cd "$DIR/../.." && pwd)" - -cd "$TOP_LEVEL" - -cabal build \ - --prefix=./buildah/dot-cabal \ - --builddir=./buildah/dist-newstyle \ - "$@" - -DIST="$TOP_LEVEL"/buildah/dist PLAN_FILE="$TOP_LEVEL"/buildah/dist-newstyle/cache/plan.json ./hack/bin/cabal-install-artefacts.sh "$@" diff --git a/hack/bin/buildah-make-images-nginz.sh b/hack/bin/buildah-make-images-nginz.sh deleted file mode 100755 index dba85bf94f..0000000000 --- a/hack/bin/buildah-make-images-nginz.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash - -# Pulls nginz and nginz_disco images from quay.io into buildah store, and loads -# them into the kind cluster - -set -ex - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -TOP_LEVEL="$(cd "$DIR/../.." && pwd)" - -DOCKER_DOWNLOAD_TAG=latest -DOCKER_TAG=${DOCKER_TAG:-$USER} -EXECUTABLES=${EXECUTABLES:-"nginz nginz_disco"} - -for EX in $EXECUTABLES; do - CONTAINER_NAME=$EX - buildah containers | awk '{print $5}' | grep "$CONTAINER_NAME" || - buildah from --name "$CONTAINER_NAME" -v "${TOP_LEVEL}":/src --pull "quay.io/wire/$CONTAINER_NAME:$DOCKER_DOWNLOAD_TAG" - buildah tag "quay.io/wire/$CONTAINER_NAME:$DOCKER_DOWNLOAD_TAG" "quay.io/wire/$CONTAINER_NAME:$DOCKER_TAG" - if [[ "$BUILDAH_KIND_LOAD" -eq "1" ]]; then - archiveDir=$(mktemp -d) - imgPath="$archiveDir/${EX}_${DOCKER_TAG}.tar" - imgName="quay.io/wire/$EX:$DOCKER_TAG" - buildah push "$imgName" "docker-archive:$imgPath:$imgName" - kind load image-archive --name "$KIND_CLUSTER_NAME" "$imgPath" - rm -rf "$archiveDir" - fi -done - -if [[ "$BUILDAH_PUSH" -eq "1" ]]; then - for EX in $EXECUTABLES; do - buildah push "quay.io/wire/$EX:$DOCKER_TAG" - done -fi - -# general cleanup -"$DIR/buildah-purge-untagged.sh" diff --git a/hack/bin/buildah-make-images.sh b/hack/bin/buildah-make-images.sh deleted file mode 100755 index cc06f2c102..0000000000 --- a/hack/bin/buildah-make-images.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env bash - -set -ex - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -TOP_LEVEL="$(cd "$DIR/../.." && pwd)" - -EXECUTABLES=${EXECUTABLES:-"cannon brig cargohold galley gundeck federator brig-index brig-schema galley-schema galley-migrate-data gundeck-schema proxy spar spar-schema spar-migrate-data brig-integration galley-integration spar-integration gundeck-integration cargohold-integration federator-integration"} -CONTAINER_NAME="output" -DOCKER_TAG=${DOCKER_TAG:-$USER} - -buildah containers | awk '{print $5}' | grep "$CONTAINER_NAME" || - buildah from --name "$CONTAINER_NAME" -v "${TOP_LEVEL}":/src --pull quay.io/wire/ubuntu20-deps:develop - -# Only brig needs these templates, but for simplicity we add them to all resulting images (optimization FUTUREWORK) -buildah run "$CONTAINER_NAME" -- sh -c 'mkdir -p /usr/share/wire/ && cp -r "/src/services/brig/deb/opt/brig/templates/." "/usr/share/wire/templates"' - -for EX in $EXECUTABLES; do - # Copy the main executable into the PATH on the container - buildah run "$CONTAINER_NAME" -- cp "/src/buildah/dist/$EX" "/usr/bin/$EX" - - # Start that executable by default when launching the docker image - buildah config --entrypoint "[ \"/usr/bin/dumb-init\", \"--\", \"/usr/bin/$EX\" ]" "$CONTAINER_NAME" - buildah config --cmd null "$CONTAINER_NAME" - - # Bake an image - buildah commit "$CONTAINER_NAME" quay.io/wire/"$EX":"$DOCKER_TAG" - - # remove executable from the image in preparation for the next iteration - buildah run "$CONTAINER_NAME" -- rm "/usr/bin/$EX" -done - -if [[ "$BUILDAH_PUSH" -eq "1" ]]; then - for EX in $EXECUTABLES; do - buildah push "quay.io/wire/$EX:$DOCKER_TAG" - done -fi - -if [[ "$BUILDAH_KIND_LOAD" -eq "1" ]]; then - archiveDir=$(mktemp -d) - for EX in $EXECUTABLES; do - imgPath="$archiveDir/${EX}_${DOCKER_TAG}.tar" - imgName="quay.io/wire/$EX:$DOCKER_TAG" - buildah push "$imgName" "docker-archive:$imgPath:$imgName" - kind load image-archive --name "$KIND_CLUSTER_NAME" "$imgPath" - done - rm -rf "$archiveDir" -fi - -# general cleanup -"$DIR/buildah-purge-untagged.sh" diff --git a/hack/bin/buildah-purge-untagged.sh b/hack/bin/buildah-purge-untagged.sh deleted file mode 100755 index da74d36854..0000000000 --- a/hack/bin/buildah-purge-untagged.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -# Remove untagged images (if there are any) in the buildah store -if buildah images | grep ""; then - buildah images | grep "" | awk '{print $3}' | xargs -n 1 buildah rmi -fi diff --git a/hack/bin/generate-local-nix-packages.sh b/hack/bin/generate-local-nix-packages.sh index 50e6e047f2..abfdfeb925 100755 --- a/hack/bin/generate-local-nix-packages.sh +++ b/hack/bin/generate-local-nix-packages.sh @@ -21,7 +21,7 @@ echo "$cabalFiles" \ # shellcheck disable=SC2016 echo "$cabalFiles" \ - | xargs -I {} bash -c 'cd $(dirname {}); cabal2nix . --no-hpack --extra-arguments gitignoreSource | sed "s/.\/./gitignoreSource .\/./g" >> default.nix' + | xargs -I {} bash -c 'cd $(dirname {}); cabal2nix . --no-hpack --extra-arguments gitignoreSource | sed "s/.\/./gitignoreSource .\/./g" >> default.nix; nixpkgs-fmt default.nix &> /dev/null' overridesFile="$ROOT_DIR/nix/local-haskell-packages.nix" @@ -30,3 +30,6 @@ cat "$warningFile" <(echo "{ gitignoreSource }: hsuper: hself: {") > "$overrides echo "$cabalFiles" \ | xargs -I {} bash -c 'name=$(basename {} | sed "s|.cabal||"); echo " $name = hself.callPackage $(realpath --relative-to='"$ROOT_DIR/nix"' "$(dirname {})")/default.nix { inherit gitignoreSource; };"' >> "$overridesFile" echo "}" >> "$overridesFile" + +# ensure the file is formatted +nixpkgs-fmt "$overridesFile" &> /dev/null diff --git a/hack/bin/set-chart-image-version.sh b/hack/bin/set-chart-image-version.sh index 64c2cf0293..d133007e4a 100755 --- a/hack/bin/set-chart-image-version.sh +++ b/hack/bin/set-chart-image-version.sh @@ -12,9 +12,6 @@ do if [[ "$chart" == "nginz" ]]; then # nginz has a different docker tag indentation sed -i "s/^ tag: .*/ tag: $docker_tag/g" "$CHARTS_DIR/$chart/values.yaml" -elif [[ "$chart" == "backoffice" ]]; then - # There are two images at the same level and we want update only stern. - sed -i "s/tag: do-not-use/tag: $docker_tag/g" "$CHARTS_DIR/$chart/values.yaml" else sed -i "s/^ tag: .*/ tag: $docker_tag/g" "$CHARTS_DIR/$chart/values.yaml" fi diff --git a/hack/bin/set-wire-server-image-version.sh b/hack/bin/set-wire-server-image-version.sh index f439d42514..212ceed770 100755 --- a/hack/bin/set-wire-server-image-version.sh +++ b/hack/bin/set-wire-server-image-version.sh @@ -6,7 +6,7 @@ target_version=${1?$USAGE} TOP_LEVEL="$( cd "$( dirname "${BASH_SOURCE[0]}" )/../.." && pwd )" CHARTS_DIR="$TOP_LEVEL/.local/charts" -charts=(brig cannon galley gundeck spar cargohold proxy cassandra-migrations elasticsearch-index federator) +charts=(brig cannon galley gundeck spar cargohold proxy cassandra-migrations elasticsearch-index federator backoffice) for chart in "${charts[@]}"; do sed -i "s/^ tag: .*/ tag: $target_version/g" "$CHARTS_DIR/$chart/values.yaml" @@ -14,7 +14,3 @@ done # special case nginz sed -i "s/^ tag: .*/ tag: $target_version/g" "$CHARTS_DIR/nginz/values.yaml" - -# special case backoffice as there are two images at the same level and we want -# update only one. -sed -i "s/tag: do-not-use/tag: $target_version/g" "$CHARTS_DIR/backoffice/values.yaml" diff --git a/hack/bin/shellcheck.sh b/hack/bin/shellcheck.sh deleted file mode 100755 index ff551e5dc3..0000000000 --- a/hack/bin/shellcheck.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash - -set -eu - -# lint all shell scripts with ShellCheck -# FUTUREWORK: Fix issues of the explicitly (no globbing) excluded files. - -mapfile -t SHELL_FILES_TO_LINT < <( - git ls-files | - grep "\.sh$" | - grep -v "dist-newstyle/" | - grep -v "services/nginz/third_party/" | - grep -v "libs/wire-api/test/golden/gentests.sh" | - grep -v "changelog.d/mk-changelog.sh" | - grep -v "hack/bin/integration-teardown.sh" | - grep -v "hack/bin/diff-failure.sh" | - grep -v "hack/bin/integration-setup.sh" | - grep -v "hack/bin/cabal-run-tests.sh" | - grep -v "hack/bin/integration-teardown-federation.sh" | - grep -v "hack/bin/integration-setup-federation.sh" | - grep -v "hack/bin/serve-charts.sh" | - grep -v "hack/bin/cabal-install-artefacts.sh" | - grep -v "hack/bin/helm-template.sh" | - grep -v "hack/bin/set-chart-image-version.sh" | - grep -v "hack/bin/copy-charts.sh" | - grep -v "hack/bin/set-helm-chart-version.sh" | - grep -v "hack/bin/integration-spring-cleaning.sh" | - grep -v "hack/bin/upload-helm-charts-s3.sh" | - grep -v "hack/bin/integration-test-logs.sh" | - grep -v "services/nginz/nginz_reload.sh" | - grep -v "services/spar/test-scim-suite/mk_collection.sh" | - grep -v "services/spar/test-scim-suite/runsuite.sh" | - grep -v "services/spar/test-scim-suite/run.sh" | - grep -v "services/brig/federation-tests.sh" | - grep -v "services/integration.sh" | - grep -v "hack/bin/create_test_team_members.sh" | - grep -v "hack/bin/create_test_team_scim.sh" | - grep -v "hack/bin/create_test_user.sh" | - grep -v "hack/bin/create_team_members.sh" | - grep -v "hack/bin/register_idp_internal.sh" | - grep -v "hack/bin/create_test_team_admins.sh" | - grep -v "deploy/dockerephemeral/init.sh" | - grep -v "tools/nginz_disco/nginz_disco.sh" | - grep -v "tools/rebase-onto-formatter.sh" | - grep -v "tools/sftd_disco/sftd_disco.sh" | - grep -v "tools/ormolu.sh" | - grep -v "tools/db/move-team/dump_merge_teams.sh" -) - -shellcheck -x "${SHELL_FILES_TO_LINT[@]}" diff --git a/hack/bin/upload-image.sh b/hack/bin/upload-image.sh index 9540158d34..e49eaca08b 100755 --- a/hack/bin/upload-image.sh +++ b/hack/bin/upload-image.sh @@ -17,8 +17,8 @@ readonly DOCKER_TAG=${DOCKER_TAG:?"Please set the DOCKER_TAG env variable"} readonly usage="USAGE: $0 " readonly IMAGE_ATTR=${1:?$usage} -SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -ROOT_DIR=$(cd -- "$SCRIPT_DIR/../../" &> /dev/null && pwd) +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) +ROOT_DIR=$(cd -- "$SCRIPT_DIR/../../" &>/dev/null && pwd) readonly SCRIPT_DIR ROOT_DIR credsArgs="" @@ -27,6 +27,39 @@ if [[ "${DOCKER_USER+x}" != "" ]]; then credsArgs="--dest-creds=$DOCKER_USER:$DOCKER_PASSWORD" fi +# Retry a command with exponential backoff +# quay.io sometimes rate-limits us, so try again. +# Also, skopeo's retry logic doesn't properly work, look here if you want to see very badly written go code: +# https://github.com/containers/skopeo/blob/869d496f185cc086f22d6bbb79bb57ac3a415617/vendor/github.com/containers/common/pkg/retry/retry.go#L52-L113 +function retry { + local maxAttempts=$1 + local secondsDelay=1 + local attemptCount=1 + local output= + shift 1 + + while [ $attemptCount -le "$maxAttempts" ]; do + output=$("$@") + local status=$? + + if [ $status -eq 0 ]; then + break + fi + + if [ $attemptCount -lt "$maxAttempts" ]; then + echo "Command [$*] failed after attempt $attemptCount of $maxAttempts. Retrying in $secondsDelay second(s)." >&2 + sleep $secondsDelay + elif [ $attemptCount -eq "$maxAttempts" ]; then + echo "Command [$*] failed after $attemptCount attempt(s)" >&2 + return $status + fi + attemptCount=$((attemptCount + 1)) + secondsDelay=$((secondsDelay * 2)) + done + + echo "$output" +} + tmp_link_store=$(mktemp -d) # Using dockerTools.streamLayeredImage outputs an executable which prints the # image tar on stdout when executed. This is done so we don't store large images @@ -38,8 +71,8 @@ tmp_link_store=$(mktemp -d) image_stream_file="$tmp_link_store/image_stream" nix -v --show-trace -L build -f "$ROOT_DIR/nix" "$IMAGE_ATTR" -o "$image_stream_file" image_file="$tmp_link_store/image" -"$image_stream_file" > "$image_file" +"$image_stream_file" >"$image_file" repo=$(skopeo list-tags "docker-archive://$image_file" | jq -r '.Tags[0] | split(":") | .[0]') -printf "*** Uploading $image_file to %s:%s" "$repo" "$DOCKER_TAG" +printf "*** Uploading $image_file to %s:%s\n" "$repo" "$DOCKER_TAG" # shellcheck disable=SC2086 -skopeo --insecure-policy copy --retry-times 5 $credsArgs "docker-archive://$image_file" "docker://$repo:$DOCKER_TAG" +retry 5 skopeo --insecure-policy copy --retry-times 5 $credsArgs "docker-archive://$image_file" "docker://$repo:$DOCKER_TAG" diff --git a/hack/bin/upload-images.sh b/hack/bin/upload-images.sh index 205a5dfab0..79c0798f2c 100755 --- a/hack/bin/upload-images.sh +++ b/hack/bin/upload-images.sh @@ -17,8 +17,8 @@ readonly usage="USAGE: $0 " # nix attribute under wireServer from "$ROOT_DIR/nix" containing all the images readonly IMAGES_ATTR=${1:?$usage} -SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -ROOT_DIR=$(cd -- "$SCRIPT_DIR/../../" &> /dev/null && pwd) +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) +ROOT_DIR=$(cd -- "$SCRIPT_DIR/../../" &>/dev/null && pwd) readonly SCRIPT_DIR ROOT_DIR tmp_link_store=$(mktemp -d) @@ -28,8 +28,12 @@ nix -v --show-trace -L build -f "$ROOT_DIR/nix" wireServer.imagesList -o "$image # Build everything first so we can benefit the most from having many cores. nix -v --show-trace -L build -f "$ROOT_DIR/nix" "wireServer.$IMAGES_ATTR" --no-link -while IFS="" read -r image_name || [ -n "$image_name" ] -do +while IFS="" read -r image_name || [ -n "$image_name" ]; do printf '*** Uploading image %s\n' "$image_name" "$SCRIPT_DIR/upload-image.sh" "wireServer.$IMAGES_ATTR.$image_name" -done < "$image_list_file" +done <"$image_list_file" + +for image_name in nginz nginz-disco; do + printf '*** Uploading image %s\n' "$image_name" + "$SCRIPT_DIR/upload-image.sh" "$image_name" +done diff --git a/libs/api-bot/default.nix b/libs/api-bot/default.nix index 823fad86b1..8116ba9ceb 100644 --- a/libs/api-bot/default.nix +++ b/libs/api-bot/default.nix @@ -2,27 +2,89 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, aeson, ansi-terminal, api-client, async, attoparsec -, base, base64-bytestring, bilge, bytestring, bytestring-conversion -, cereal, containers, cryptobox-haskell, cryptonite, exceptions -, filepath, gitignoreSource, HaskellNet, HaskellNet-SSL -, http-client, imports, iso639, lib, memory, metrics-core, mime -, monad-control, mwc-random, optparse-applicative, resource-pool -, stm, text, time, tinylog, transformers-base, types-common -, unordered-containers, uuid, vector +{ mkDerivation +, aeson +, ansi-terminal +, api-client +, async +, attoparsec +, base +, base64-bytestring +, bilge +, bytestring +, bytestring-conversion +, cereal +, containers +, cryptobox-haskell +, cryptonite +, exceptions +, filepath +, gitignoreSource +, HaskellNet +, HaskellNet-SSL +, http-client +, imports +, iso639 +, lib +, memory +, metrics-core +, mime +, monad-control +, mwc-random +, optparse-applicative +, resource-pool +, stm +, text +, time +, tinylog +, transformers-base +, types-common +, unordered-containers +, uuid +, vector }: mkDerivation { pname = "api-bot"; version = "0.4.2"; src = gitignoreSource ./.; libraryHaskellDepends = [ - aeson ansi-terminal api-client async attoparsec base - base64-bytestring bilge bytestring bytestring-conversion cereal - containers cryptobox-haskell cryptonite exceptions filepath - HaskellNet HaskellNet-SSL http-client imports iso639 memory - metrics-core mime monad-control mwc-random optparse-applicative - resource-pool stm text time tinylog transformers-base types-common - unordered-containers uuid vector + aeson + ansi-terminal + api-client + async + attoparsec + base + base64-bytestring + bilge + bytestring + bytestring-conversion + cereal + containers + cryptobox-haskell + cryptonite + exceptions + filepath + HaskellNet + HaskellNet-SSL + http-client + imports + iso639 + memory + metrics-core + mime + monad-control + mwc-random + optparse-applicative + resource-pool + stm + text + time + tinylog + transformers-base + types-common + unordered-containers + uuid + vector ]; description = "(Internal) API automation around wire-client"; license = lib.licenses.agpl3Only; diff --git a/libs/api-client/default.nix b/libs/api-client/default.nix index 52b3c069cd..bbf842e4f8 100644 --- a/libs/api-client/default.nix +++ b/libs/api-client/default.nix @@ -2,11 +2,34 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, aeson, async, base, bilge, bytestring -, bytestring-conversion, connection, cookie, data-default-class -, errors, exceptions, gitignoreSource, http-client, http-types -, imports, lib, mime, retry, text, time, tinylog, transformers -, types-common, unliftio, unordered-containers, uuid, websockets +{ mkDerivation +, aeson +, async +, base +, bilge +, bytestring +, bytestring-conversion +, connection +, cookie +, data-default-class +, errors +, exceptions +, gitignoreSource +, http-client +, http-types +, imports +, lib +, mime +, retry +, text +, time +, tinylog +, transformers +, types-common +, unliftio +, unordered-containers +, uuid +, websockets , wire-api }: mkDerivation { @@ -14,10 +37,32 @@ mkDerivation { version = "0.4.2"; src = gitignoreSource ./.; libraryHaskellDepends = [ - aeson async base bilge bytestring bytestring-conversion connection - cookie data-default-class errors exceptions http-client http-types - imports mime retry text time tinylog transformers types-common - unliftio unordered-containers uuid websockets wire-api + aeson + async + base + bilge + bytestring + bytestring-conversion + connection + cookie + data-default-class + errors + exceptions + http-client + http-types + imports + mime + retry + text + time + tinylog + transformers + types-common + unliftio + unordered-containers + uuid + websockets + wire-api ]; description = "(Internal) Wire HTTP API Client"; license = lib.licenses.agpl3Only; diff --git a/libs/bilge/default.nix b/libs/bilge/default.nix index 07dba5db2f..6a32b06d68 100644 --- a/libs/bilge/default.nix +++ b/libs/bilge/default.nix @@ -2,21 +2,59 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, aeson, ansi-terminal, base, bytestring -, case-insensitive, cookie, errors, exceptions, gitignoreSource -, http-client, http-types, imports, lens, lib, monad-control, mtl -, text, tinylog, transformers-base, types-common, unliftio -, uri-bytestring, wai, wai-extra +{ mkDerivation +, aeson +, ansi-terminal +, base +, bytestring +, case-insensitive +, cookie +, errors +, exceptions +, gitignoreSource +, http-client +, http-types +, imports +, lens +, lib +, monad-control +, mtl +, text +, tinylog +, transformers-base +, types-common +, unliftio +, uri-bytestring +, wai +, wai-extra }: mkDerivation { pname = "bilge"; version = "0.22.0"; src = gitignoreSource ./.; libraryHaskellDepends = [ - aeson ansi-terminal base bytestring case-insensitive cookie errors - exceptions http-client http-types imports lens monad-control mtl - text tinylog transformers-base types-common unliftio uri-bytestring - wai wai-extra + aeson + ansi-terminal + base + bytestring + case-insensitive + cookie + errors + exceptions + http-client + http-types + imports + lens + monad-control + mtl + text + tinylog + transformers-base + types-common + unliftio + uri-bytestring + wai + wai-extra ]; description = "Library for composing HTTP requests"; license = lib.licenses.agpl3Only; diff --git a/libs/bilge/src/Bilge/Assert.hs b/libs/bilge/src/Bilge/Assert.hs index 2a584e5b6d..512aa0f925 100644 --- a/libs/bilge/src/Bilge/Assert.hs +++ b/libs/bilge/src/Bilge/Assert.hs @@ -26,6 +26,7 @@ module Bilge.Assert (===), (=/=), (=~=), + (=/~=), assertResponse, assertTrue, assertTrue_, @@ -141,6 +142,15 @@ f =/= g = Assertions $ tell [\r -> test " === " (/=) (f r) (g r)] Assertions () f =~= g = Assertions $ tell [\r -> test " not in " contains (f r) (g r)] +-- | Tests the assertion that the left-hand side is **not** contained in the right-hand side. +-- If it is, actual values will be printed. +(=/~=) :: + (HasCallStack, Show a, Contains a) => + (Response (Maybe Lazy.ByteString) -> a) -> + (Response (Maybe Lazy.ByteString) -> a) -> + Assertions () +f =/~= g = Assertions $ tell [\r -> test " in " ((not .) . contains) (f r) (g r)] + -- | Most generic assertion on a request. If the test function evaluates to -- @(Just msg)@ then the assertion fails with the error message @msg@. assertResponse :: HasCallStack => (Response (Maybe Lazy.ByteString) -> Maybe String) -> Assertions () diff --git a/libs/brig-types/default.nix b/libs/brig-types/default.nix index 46a87df55a..ba0b67dcec 100644 --- a/libs/brig-types/default.nix +++ b/libs/brig-types/default.nix @@ -2,29 +2,80 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, aeson, attoparsec, base, bytestring -, bytestring-conversion, cassandra-util, containers -, deriving-swagger2, gitignoreSource, imports, lib, QuickCheck -, schema-profunctor, servant-server, servant-swagger -, string-conversions, swagger2, tasty, tasty-hunit -, tasty-quickcheck, text, time, tinylog, types-common -, unordered-containers, wire-api +{ mkDerivation +, aeson +, attoparsec +, base +, bytestring +, bytestring-conversion +, cassandra-util +, containers +, deriving-swagger2 +, gitignoreSource +, imports +, lib +, QuickCheck +, schema-profunctor +, servant-server +, servant-swagger +, string-conversions +, swagger2 +, tasty +, tasty-hunit +, tasty-quickcheck +, text +, time +, tinylog +, types-common +, unordered-containers +, wire-api }: mkDerivation { pname = "brig-types"; version = "1.35.0"; src = gitignoreSource ./.; libraryHaskellDepends = [ - aeson attoparsec base bytestring bytestring-conversion - cassandra-util containers deriving-swagger2 imports QuickCheck - schema-profunctor servant-server servant-swagger string-conversions - swagger2 text time tinylog types-common unordered-containers + aeson + attoparsec + base + bytestring + bytestring-conversion + cassandra-util + containers + deriving-swagger2 + imports + QuickCheck + schema-profunctor + servant-server + servant-swagger + string-conversions + swagger2 + text + time + tinylog + types-common + unordered-containers wire-api ]; testHaskellDepends = [ - aeson attoparsec base bytestring bytestring-conversion containers - imports QuickCheck swagger2 tasty tasty-hunit tasty-quickcheck text - time tinylog types-common unordered-containers wire-api + aeson + attoparsec + base + bytestring + bytestring-conversion + containers + imports + QuickCheck + swagger2 + tasty + tasty-hunit + tasty-quickcheck + text + time + tinylog + types-common + unordered-containers + wire-api ]; description = "User Service"; license = lib.licenses.agpl3Only; diff --git a/libs/brig-types/src/Brig/Types/Intra.hs b/libs/brig-types/src/Brig/Types/Intra.hs index 88bc7fda2e..6535aa9685 100644 --- a/libs/brig-types/src/Brig/Types/Intra.hs +++ b/libs/brig-types/src/Brig/Types/Intra.hs @@ -33,6 +33,7 @@ import qualified Data.Schema as Schema import qualified Data.Swagger as S import Imports import Test.QuickCheck (Arbitrary) +import Wire.API.Team.Role import Wire.API.User import Wire.Arbitrary (GenericUniform (..)) @@ -111,7 +112,8 @@ data NewUserScimInvitation = NewUserScimInvitation { newUserScimInvTeamId :: TeamId, newUserScimInvLocale :: Maybe Locale, newUserScimInvName :: Name, - newUserScimInvEmail :: Email + newUserScimInvEmail :: Email, + newUserScimInvRole :: Role } deriving (Eq, Show, Generic) @@ -122,12 +124,14 @@ instance FromJSON NewUserScimInvitation where <*> o .:? "locale" <*> o .: "name" <*> o .: "email" + <*> o .: "role" instance ToJSON NewUserScimInvitation where - toJSON (NewUserScimInvitation tid loc name email) = + toJSON (NewUserScimInvitation tid loc name email role) = object [ "team_id" .= tid, "locale" .= loc, "name" .= name, - "email" .= email + "email" .= email, + "role" .= role ] diff --git a/libs/brig-types/test/unit/Test/Brig/Types/User.hs b/libs/brig-types/test/unit/Test/Brig/Types/User.hs index 2b2cb07eca..ce800cee80 100644 --- a/libs/brig-types/test/unit/Test/Brig/Types/User.hs +++ b/libs/brig-types/test/unit/Test/Brig/Types/User.hs @@ -66,7 +66,7 @@ instance Arbitrary ReAuthUser where arbitrary = ReAuthUser <$> arbitrary <*> arbitrary <*> arbitrary instance Arbitrary NewUserScimInvitation where - arbitrary = NewUserScimInvitation <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary + arbitrary = NewUserScimInvitation <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary testCaseUserAccount :: TestTree testCaseUserAccount = testCase "UserAcccount" $ do diff --git a/libs/cargohold-types/default.nix b/libs/cargohold-types/default.nix index 523e4eab3a..6415170a56 100644 --- a/libs/cargohold-types/default.nix +++ b/libs/cargohold-types/default.nix @@ -2,15 +2,25 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, base, bytestring-conversion, gitignoreSource -, imports, lib, types-common, wire-api +{ mkDerivation +, base +, bytestring-conversion +, gitignoreSource +, imports +, lib +, types-common +, wire-api }: mkDerivation { pname = "cargohold-types"; version = "1.5.0"; src = gitignoreSource ./.; libraryHaskellDepends = [ - base bytestring-conversion imports types-common wire-api + base + bytestring-conversion + imports + types-common + wire-api ]; description = "Asset Storage API Types"; license = lib.licenses.agpl3Only; diff --git a/libs/cassandra-util/default.nix b/libs/cassandra-util/default.nix index fdc62d62b5..9f57e34427 100644 --- a/libs/cassandra-util/default.nix +++ b/libs/cassandra-util/default.nix @@ -2,19 +2,53 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, aeson, base, conduit, containers, cql, cql-io -, cql-io-tinylog, exceptions, gitignoreSource, imports, lens -, lens-aeson, lib, optparse-applicative, retry, split, text, time -, tinylog, uuid, wreq +{ mkDerivation +, aeson +, base +, conduit +, containers +, cql +, cql-io +, cql-io-tinylog +, exceptions +, gitignoreSource +, imports +, lens +, lens-aeson +, lib +, optparse-applicative +, retry +, split +, text +, time +, tinylog +, uuid +, wreq }: mkDerivation { pname = "cassandra-util"; version = "0.16.5"; src = gitignoreSource ./.; libraryHaskellDepends = [ - aeson base conduit containers cql cql-io cql-io-tinylog exceptions - imports lens lens-aeson optparse-applicative retry split text time - tinylog uuid wreq + aeson + base + conduit + containers + cql + cql-io + cql-io-tinylog + exceptions + imports + lens + lens-aeson + optparse-applicative + retry + split + text + time + tinylog + uuid + wreq ]; description = "Cassandra Utilities"; license = lib.licenses.agpl3Only; diff --git a/libs/deriving-swagger2/default.nix b/libs/deriving-swagger2/default.nix index b11f6619f0..fdf39de254 100644 --- a/libs/deriving-swagger2/default.nix +++ b/libs/deriving-swagger2/default.nix @@ -2,7 +2,12 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, base, extra, gitignoreSource, imports, lib +{ mkDerivation +, base +, extra +, gitignoreSource +, imports +, lib , swagger2 }: mkDerivation { diff --git a/libs/dns-util/default.nix b/libs/dns-util/default.nix index 1da76aea20..509dedafdc 100644 --- a/libs/dns-util/default.nix +++ b/libs/dns-util/default.nix @@ -2,18 +2,38 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, base, dns, gitignoreSource, hspec, hspec-discover -, imports, iproute, lib, polysemy, random +{ mkDerivation +, base +, dns +, gitignoreSource +, hspec +, hspec-discover +, imports +, iproute +, lib +, polysemy +, random }: mkDerivation { pname = "dns-util"; version = "0.1.0"; src = gitignoreSource ./.; libraryHaskellDepends = [ - base dns imports iproute polysemy random + base + dns + imports + iproute + polysemy + random ]; testHaskellDepends = [ - base dns hspec imports iproute polysemy random + base + dns + hspec + imports + iproute + polysemy + random ]; testToolDepends = [ hspec-discover ]; description = "Library to deal with DNS SRV records"; diff --git a/libs/extended/default.nix b/libs/extended/default.nix index 0f193232ce..d51ab0466c 100644 --- a/libs/extended/default.nix +++ b/libs/extended/default.nix @@ -2,26 +2,76 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, aeson, base, bytestring, cassandra-util, containers -, errors, exceptions, extra, gitignoreSource, hspec, hspec-discover -, http-types, imports, lib, metrics-wai, optparse-applicative -, servant, servant-server, servant-swagger, string-conversions -, temporary, tinylog, wai +{ mkDerivation +, aeson +, base +, bytestring +, cassandra-util +, containers +, errors +, exceptions +, extra +, gitignoreSource +, hspec +, hspec-discover +, http-types +, imports +, lib +, metrics-wai +, optparse-applicative +, servant +, servant-server +, servant-swagger +, string-conversions +, temporary +, tinylog +, wai }: mkDerivation { pname = "extended"; version = "0.1.0"; src = gitignoreSource ./.; libraryHaskellDepends = [ - aeson base bytestring cassandra-util containers errors exceptions - extra http-types imports metrics-wai optparse-applicative servant - servant-server servant-swagger string-conversions tinylog wai + aeson + base + bytestring + cassandra-util + containers + errors + exceptions + extra + http-types + imports + metrics-wai + optparse-applicative + servant + servant-server + servant-swagger + string-conversions + tinylog + wai ]; testHaskellDepends = [ - aeson base bytestring cassandra-util containers errors exceptions - extra hspec http-types imports metrics-wai optparse-applicative - servant servant-server servant-swagger string-conversions temporary - tinylog wai + aeson + base + bytestring + cassandra-util + containers + errors + exceptions + extra + hspec + http-types + imports + metrics-wai + optparse-applicative + servant + servant-server + servant-swagger + string-conversions + temporary + tinylog + wai ]; testToolDepends = [ hspec-discover ]; description = "Extended versions of common modules"; diff --git a/libs/galley-types/default.nix b/libs/galley-types/default.nix index 9c499e4440..478a0c540e 100644 --- a/libs/galley-types/default.nix +++ b/libs/galley-types/default.nix @@ -2,11 +2,33 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, aeson, base, bytestring, bytestring-conversion -, containers, cryptonite, currency-codes, errors, exceptions -, gitignoreSource, imports, lens, lib, memory, QuickCheck -, schema-profunctor, string-conversions, swagger2, tagged, tasty -, tasty-hunit, tasty-quickcheck, text, time, types-common, uuid +{ mkDerivation +, aeson +, base +, bytestring +, bytestring-conversion +, containers +, cryptonite +, currency-codes +, errors +, exceptions +, gitignoreSource +, imports +, lens +, lib +, memory +, QuickCheck +, schema-profunctor +, string-conversions +, swagger2 +, tagged +, tasty +, tasty-hunit +, tasty-quickcheck +, text +, time +, types-common +, uuid , wire-api }: mkDerivation { @@ -14,14 +36,41 @@ mkDerivation { version = "0.81.0"; src = gitignoreSource ./.; libraryHaskellDepends = [ - aeson base bytestring bytestring-conversion containers cryptonite - currency-codes errors exceptions imports lens memory QuickCheck - schema-profunctor string-conversions swagger2 tagged text time - types-common uuid wire-api + aeson + base + bytestring + bytestring-conversion + containers + cryptonite + currency-codes + errors + exceptions + imports + lens + memory + QuickCheck + schema-profunctor + string-conversions + swagger2 + tagged + text + time + types-common + uuid + wire-api ]; testHaskellDepends = [ - aeson base containers imports lens QuickCheck tasty tasty-hunit - tasty-quickcheck types-common wire-api + aeson + base + containers + imports + lens + QuickCheck + tasty + tasty-hunit + tasty-quickcheck + types-common + wire-api ]; license = lib.licenses.agpl3Only; } diff --git a/libs/gundeck-types/default.nix b/libs/gundeck-types/default.nix index 19d76ecf6f..ea45673e17 100644 --- a/libs/gundeck-types/default.nix +++ b/libs/gundeck-types/default.nix @@ -2,9 +2,21 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, aeson, attoparsec, base, bytestring -, bytestring-conversion, containers, gitignoreSource, imports, lens -, lib, network-uri, text, types-common, unordered-containers +{ mkDerivation +, aeson +, attoparsec +, base +, bytestring +, bytestring-conversion +, containers +, gitignoreSource +, imports +, lens +, lib +, network-uri +, text +, types-common +, unordered-containers , wire-api }: mkDerivation { @@ -12,8 +24,18 @@ mkDerivation { version = "1.45.0"; src = gitignoreSource ./.; libraryHaskellDepends = [ - aeson attoparsec base bytestring bytestring-conversion containers - imports lens network-uri text types-common unordered-containers + aeson + attoparsec + base + bytestring + bytestring-conversion + containers + imports + lens + network-uri + text + types-common + unordered-containers wire-api ]; license = lib.licenses.agpl3Only; diff --git a/libs/hscim/default.nix b/libs/hscim/default.nix index 4ee4f9c87d..ff9dda2955 100644 --- a/libs/hscim/default.nix +++ b/libs/hscim/default.nix @@ -2,15 +2,49 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, aeson, aeson-qq, attoparsec, base, bytestring -, case-insensitive, email-validate, gitignoreSource, hashable -, hedgehog, hspec, hspec-discover, hspec-expectations, hspec-wai -, http-api-data, http-media, http-types, hw-hspec-hedgehog -, indexed-traversable, lib, list-t, microlens, mmorph, mtl -, network-uri, retry, scientific, servant, servant-client -, servant-client-core, servant-server, stm, stm-containers -, string-conversions, template-haskell, text, time -, unordered-containers, uuid, wai, wai-extra, warp +{ mkDerivation +, aeson +, aeson-qq +, attoparsec +, base +, bytestring +, case-insensitive +, email-validate +, gitignoreSource +, hashable +, hedgehog +, hspec +, hspec-discover +, hspec-expectations +, hspec-wai +, http-api-data +, http-media +, http-types +, hw-hspec-hedgehog +, indexed-traversable +, lib +, list-t +, microlens +, mmorph +, mtl +, network-uri +, retry +, scientific +, servant +, servant-client +, servant-client-core +, servant-server +, stm +, stm-containers +, string-conversions +, template-haskell +, text +, time +, unordered-containers +, uuid +, wai +, wai-extra +, warp }: mkDerivation { pname = "hscim"; @@ -19,31 +53,124 @@ mkDerivation { isLibrary = true; isExecutable = true; libraryHaskellDepends = [ - aeson aeson-qq attoparsec base bytestring case-insensitive - email-validate hashable hedgehog hspec hspec-expectations hspec-wai - http-api-data http-media http-types hw-hspec-hedgehog list-t - microlens mmorph mtl network-uri retry scientific servant - servant-client servant-client-core servant-server stm - stm-containers string-conversions template-haskell text time - unordered-containers uuid wai wai-extra warp + aeson + aeson-qq + attoparsec + base + bytestring + case-insensitive + email-validate + hashable + hedgehog + hspec + hspec-expectations + hspec-wai + http-api-data + http-media + http-types + hw-hspec-hedgehog + list-t + microlens + mmorph + mtl + network-uri + retry + scientific + servant + servant-client + servant-client-core + servant-server + stm + stm-containers + string-conversions + template-haskell + text + time + unordered-containers + uuid + wai + wai-extra + warp ]; executableHaskellDepends = [ - aeson aeson-qq attoparsec base bytestring case-insensitive - email-validate hashable hedgehog hspec hspec-expectations hspec-wai - http-api-data http-media http-types hw-hspec-hedgehog list-t - microlens mmorph mtl network-uri retry scientific servant - servant-client servant-client-core servant-server stm - stm-containers string-conversions template-haskell text time - unordered-containers uuid wai wai-extra warp + aeson + aeson-qq + attoparsec + base + bytestring + case-insensitive + email-validate + hashable + hedgehog + hspec + hspec-expectations + hspec-wai + http-api-data + http-media + http-types + hw-hspec-hedgehog + list-t + microlens + mmorph + mtl + network-uri + retry + scientific + servant + servant-client + servant-client-core + servant-server + stm + stm-containers + string-conversions + template-haskell + text + time + unordered-containers + uuid + wai + wai-extra + warp ]; testHaskellDepends = [ - aeson aeson-qq attoparsec base bytestring case-insensitive - email-validate hashable hedgehog hspec hspec-expectations hspec-wai - http-api-data http-media http-types hw-hspec-hedgehog - indexed-traversable list-t microlens mmorph mtl network-uri retry - scientific servant servant-client servant-client-core - servant-server stm stm-containers string-conversions - template-haskell text time unordered-containers uuid wai wai-extra + aeson + aeson-qq + attoparsec + base + bytestring + case-insensitive + email-validate + hashable + hedgehog + hspec + hspec-expectations + hspec-wai + http-api-data + http-media + http-types + hw-hspec-hedgehog + indexed-traversable + list-t + microlens + mmorph + mtl + network-uri + retry + scientific + servant + servant-client + servant-client-core + servant-server + stm + stm-containers + string-conversions + template-haskell + text + time + unordered-containers + uuid + wai + wai-extra warp ]; testToolDepends = [ hspec-discover ]; diff --git a/libs/hscim/src/Web/Scim/Schema/User.hs b/libs/hscim/src/Web/Scim/Schema/User.hs index c453309b83..16cac92a88 100644 --- a/libs/hscim/src/Web/Scim/Schema/User.hs +++ b/libs/hscim/src/Web/Scim/Schema/User.hs @@ -315,12 +315,14 @@ applyUserOperation user (Operation Replace (Just (NormalPath (AttrPath _schema a (\x -> user {externalId = x}) <$> resultToScimError (fromJSON value) "active" -> (\x -> user {active = x}) <$> resultToScimError (fromJSON value) - _ -> throwError (badRequest InvalidPath (Just "we only support attributes username, displayname, externalid, active")) + "roles" -> + (\x -> user {roles = x}) <$> resultToScimError (fromJSON value) + _ -> throwError (badRequest InvalidPath (Just "we only support attributes username, displayname, externalid, active, roles")) applyUserOperation _ (Operation Replace (Just (IntoValuePath _ _)) _) = do throwError (badRequest InvalidPath (Just "can not lens into multi-valued attributes yet")) applyUserOperation user (Operation Replace Nothing (Just value)) = do case value of - Object hm | null ((AttrName . Key.toText <$> KeyMap.keys hm) \\ ["username", "displayname", "externalid", "active"]) -> do + Object hm | null ((AttrName . Key.toText <$> KeyMap.keys hm) \\ ["username", "displayname", "externalid", "active", "roles"]) -> do (u :: User tag) <- resultToScimError $ fromJSON value pure $ user @@ -329,7 +331,7 @@ applyUserOperation user (Operation Replace Nothing (Just value)) = do externalId = externalId u, active = active u } - _ -> throwError (badRequest InvalidPath (Just "we only support attributes username, displayname, externalid, active")) + _ -> throwError (badRequest InvalidPath (Just "we only support attributes username, displayname, externalid, active, roles")) applyUserOperation _ (Operation Replace _ Nothing) = throwError (badRequest InvalidValue (Just "No value was provided")) applyUserOperation _ (Operation Remove Nothing _) = throwError (badRequest NoTarget Nothing) @@ -339,6 +341,7 @@ applyUserOperation user (Operation Remove (Just (NormalPath (AttrPath _schema at "displayname" -> pure $ user {displayName = Nothing} "externalid" -> pure $ user {externalId = Nothing} "active" -> pure $ user {active = Nothing} + "roles" -> pure $ user {roles = []} _ -> pure user applyUserOperation _ (Operation Remove (Just (IntoValuePath _ _)) _) = do throwError (badRequest InvalidPath (Just "can not lens into multi-valued attributes yet")) diff --git a/libs/hscim/test/Test/Schema/UserSpec.hs b/libs/hscim/test/Test/Schema/UserSpec.hs index 6ebcf1c4ae..6f7ae9180a 100644 --- a/libs/hscim/test/Test/Schema/UserSpec.hs +++ b/libs/hscim/test/Test/Schema/UserSpec.hs @@ -103,7 +103,6 @@ spec = do ("photos", toJSON @[Photo] mempty), ("addresses", toJSON @[Address] mempty), ("entitlements", toJSON @[Text] mempty), - ("roles", toJSON @[Text] mempty), ("x509Certificates", toJSON @[Certificate] mempty) ] $ \(key, upd) -> do diff --git a/libs/imports/default.nix b/libs/imports/default.nix index d8b9861d17..728fca8f3b 100644 --- a/libs/imports/default.nix +++ b/libs/imports/default.nix @@ -2,17 +2,37 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, base, bytestring, containers, deepseq, extra -, gitignoreSource, lib, mtl, text, transformers, unliftio -, unliftio-core, unordered-containers +{ mkDerivation +, base +, bytestring +, containers +, deepseq +, extra +, gitignoreSource +, lib +, mtl +, text +, transformers +, unliftio +, unliftio-core +, unordered-containers }: mkDerivation { pname = "imports"; version = "0.1.0"; src = gitignoreSource ./.; libraryHaskellDepends = [ - base bytestring containers deepseq extra mtl text transformers - unliftio unliftio-core unordered-containers + base + bytestring + containers + deepseq + extra + mtl + text + transformers + unliftio + unliftio-core + unordered-containers ]; description = "Very common imports"; license = lib.licenses.agpl3Only; diff --git a/libs/jwt-tools/default.nix b/libs/jwt-tools/default.nix index 13a50bca3c..4dff35909a 100644 --- a/libs/jwt-tools/default.nix +++ b/libs/jwt-tools/default.nix @@ -2,24 +2,58 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, base, base64-bytestring, bytestring -, bytestring-conversion, either, extra, gitignoreSource, hspec -, http-types, imports, lib, QuickCheck, rusty_jwt_tools_ffi -, string-conversions, text, transformers, unliftio, uuid +{ mkDerivation +, base +, base64-bytestring +, bytestring +, bytestring-conversion +, either +, extra +, gitignoreSource +, hspec +, http-types +, imports +, lib +, QuickCheck +, rusty_jwt_tools_ffi +, string-conversions +, text +, transformers +, unliftio +, uuid }: mkDerivation { pname = "jwt-tools"; version = "0.1.0"; src = gitignoreSource ./.; libraryHaskellDepends = [ - base base64-bytestring bytestring bytestring-conversion http-types - imports QuickCheck string-conversions transformers unliftio + base + base64-bytestring + bytestring + bytestring-conversion + http-types + imports + QuickCheck + string-conversions + transformers + unliftio ]; librarySystemDepends = [ rusty_jwt_tools_ffi ]; testHaskellDepends = [ - base base64-bytestring bytestring bytestring-conversion either - extra hspec http-types imports QuickCheck string-conversions text - transformers uuid + base + base64-bytestring + bytestring + bytestring-conversion + either + extra + hspec + http-types + imports + QuickCheck + string-conversions + text + transformers + uuid ]; description = "FFI to rusty-jwt-tools"; license = lib.licenses.agpl3Only; diff --git a/libs/metrics-core/default.nix b/libs/metrics-core/default.nix index eb21ca9b1e..f3eab69051 100644 --- a/libs/metrics-core/default.nix +++ b/libs/metrics-core/default.nix @@ -2,8 +2,17 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, base, containers, gitignoreSource, hashable -, immortal, imports, lib, prometheus-client, text, time +{ mkDerivation +, base +, containers +, gitignoreSource +, hashable +, immortal +, imports +, lib +, prometheus-client +, text +, time , unordered-containers }: mkDerivation { @@ -11,8 +20,15 @@ mkDerivation { version = "0.3.2"; src = gitignoreSource ./.; libraryHaskellDepends = [ - base containers hashable immortal imports prometheus-client text - time unordered-containers + base + containers + hashable + immortal + imports + prometheus-client + text + time + unordered-containers ]; description = "Metrics core"; license = lib.licenses.agpl3Only; diff --git a/libs/metrics-wai/default.nix b/libs/metrics-wai/default.nix index 9449e05d91..7a3a90c45d 100644 --- a/libs/metrics-wai/default.nix +++ b/libs/metrics-wai/default.nix @@ -2,24 +2,62 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, base, bytestring, containers, gitignoreSource -, hspec, hspec-discover, http-types, imports, lib, metrics-core -, servant, servant-multipart, string-conversions, text, wai -, wai-middleware-prometheus, wai-route, wai-routing +{ mkDerivation +, base +, bytestring +, containers +, gitignoreSource +, hspec +, hspec-discover +, http-types +, imports +, lib +, metrics-core +, servant +, servant-multipart +, string-conversions +, text +, wai +, wai-middleware-prometheus +, wai-route +, wai-routing }: mkDerivation { pname = "metrics-wai"; version = "0.5.7"; src = gitignoreSource ./.; libraryHaskellDepends = [ - base bytestring containers http-types imports metrics-core servant - servant-multipart string-conversions text wai - wai-middleware-prometheus wai-route wai-routing + base + bytestring + containers + http-types + imports + metrics-core + servant + servant-multipart + string-conversions + text + wai + wai-middleware-prometheus + wai-route + wai-routing ]; testHaskellDepends = [ - base bytestring containers hspec http-types imports metrics-core - servant servant-multipart string-conversions text wai - wai-middleware-prometheus wai-route wai-routing + base + bytestring + containers + hspec + http-types + imports + metrics-core + servant + servant-multipart + string-conversions + text + wai + wai-middleware-prometheus + wai-route + wai-routing ]; testToolDepends = [ hspec-discover ]; description = "Metrics WAI integration"; diff --git a/libs/polysemy-wire-zoo/default.nix b/libs/polysemy-wire-zoo/default.nix index 178182a141..ea3e21fdf0 100644 --- a/libs/polysemy-wire-zoo/default.nix +++ b/libs/polysemy-wire-zoo/default.nix @@ -2,23 +2,59 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, base, cassandra-util, containers, gitignoreSource -, HsOpenSSL, hspec, hspec-discover, imports, lib, polysemy -, polysemy-check, polysemy-plugin, QuickCheck, saml2-web-sso, time -, tinylog, types-common, unliftio, uuid, wire-api +{ mkDerivation +, base +, cassandra-util +, containers +, gitignoreSource +, HsOpenSSL +, hspec +, hspec-discover +, imports +, lib +, polysemy +, polysemy-check +, polysemy-plugin +, QuickCheck +, saml2-web-sso +, time +, tinylog +, types-common +, unliftio +, uuid +, wire-api }: mkDerivation { pname = "polysemy-wire-zoo"; version = "0.1.0"; src = gitignoreSource ./.; libraryHaskellDepends = [ - base cassandra-util HsOpenSSL hspec imports polysemy polysemy-check - polysemy-plugin QuickCheck saml2-web-sso time tinylog types-common - unliftio uuid wire-api + base + cassandra-util + HsOpenSSL + hspec + imports + polysemy + polysemy-check + polysemy-plugin + QuickCheck + saml2-web-sso + time + tinylog + types-common + unliftio + uuid + wire-api ]; testHaskellDepends = [ - base containers hspec imports polysemy polysemy-check - polysemy-plugin unliftio + base + containers + hspec + imports + polysemy + polysemy-check + polysemy-plugin + unliftio ]; testToolDepends = [ hspec-discover ]; description = "Polysemy interface for various libraries"; diff --git a/libs/ropes/default.nix b/libs/ropes/default.nix index a97db02000..6dd3c1ed69 100644 --- a/libs/ropes/default.nix +++ b/libs/ropes/default.nix @@ -2,17 +2,35 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, aeson, base, bytestring, errors, gitignoreSource -, http-client, http-types, imports, iso3166-country-codes, lib -, text, time +{ mkDerivation +, aeson +, base +, bytestring +, errors +, gitignoreSource +, http-client +, http-types +, imports +, iso3166-country-codes +, lib +, text +, time }: mkDerivation { pname = "ropes"; version = "0.4.20"; src = gitignoreSource ./.; libraryHaskellDepends = [ - aeson base bytestring errors http-client http-types imports - iso3166-country-codes text time + aeson + base + bytestring + errors + http-client + http-types + imports + iso3166-country-codes + text + time ]; description = "Various ropes to tie together with external web services"; license = lib.licenses.agpl3Only; diff --git a/libs/schema-profunctor/default.nix b/libs/schema-profunctor/default.nix index f2d47ba224..a498d97378 100644 --- a/libs/schema-profunctor/default.nix +++ b/libs/schema-profunctor/default.nix @@ -2,22 +2,55 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, aeson, aeson-qq, base, bifunctors, comonad -, containers, gitignoreSource, imports, insert-ordered-containers -, lens, lib, profunctors, swagger2, tasty, tasty-hunit, text -, transformers, vector +{ mkDerivation +, aeson +, aeson-qq +, base +, bifunctors +, comonad +, containers +, gitignoreSource +, imports +, insert-ordered-containers +, lens +, lib +, profunctors +, swagger2 +, tasty +, tasty-hunit +, text +, transformers +, vector }: mkDerivation { pname = "schema-profunctor"; version = "0.1.0"; src = gitignoreSource ./.; libraryHaskellDepends = [ - aeson base bifunctors comonad containers imports lens profunctors - swagger2 text transformers vector + aeson + base + bifunctors + comonad + containers + imports + lens + profunctors + swagger2 + text + transformers + vector ]; testHaskellDepends = [ - aeson aeson-qq base imports insert-ordered-containers lens swagger2 - tasty tasty-hunit text + aeson + aeson-qq + base + imports + insert-ordered-containers + lens + swagger2 + tasty + tasty-hunit + text ]; license = lib.licenses.agpl3Only; } diff --git a/libs/schema-profunctor/src/Data/Schema.hs b/libs/schema-profunctor/src/Data/Schema.hs index f2cdd73dcb..103e81429e 100644 --- a/libs/schema-profunctor/src/Data/Schema.hs +++ b/libs/schema-profunctor/src/Data/Schema.hs @@ -861,6 +861,8 @@ instance ToSchema String where schema = genericToSchema instance ToSchema Bool where schema = genericToSchema +instance ToSchema Natural where schema = genericToSchema + declareSwaggerSchema :: SchemaP (WithDeclare d) v w a b -> Declare d declareSwaggerSchema = runDeclare . schemaDoc diff --git a/libs/sodium-crypto-sign/default.nix b/libs/sodium-crypto-sign/default.nix index b7ab77265d..16278c2952 100644 --- a/libs/sodium-crypto-sign/default.nix +++ b/libs/sodium-crypto-sign/default.nix @@ -2,15 +2,24 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, base, base64-bytestring, bytestring -, gitignoreSource, imports, lib, libsodium +{ mkDerivation +, base +, base64-bytestring +, bytestring +, gitignoreSource +, imports +, lib +, libsodium }: mkDerivation { pname = "sodium-crypto-sign"; version = "0.1.2"; src = gitignoreSource ./.; libraryHaskellDepends = [ - base base64-bytestring bytestring imports + base + base64-bytestring + bytestring + imports ]; libraryPkgconfigDepends = [ libsodium ]; description = "FFI to some of the libsodium crypto_sign_* functions"; diff --git a/libs/ssl-util/default.nix b/libs/ssl-util/default.nix index 1ff0f94b88..1ec717b7f7 100644 --- a/libs/ssl-util/default.nix +++ b/libs/ssl-util/default.nix @@ -2,15 +2,29 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, base, byteable, bytestring, gitignoreSource -, HsOpenSSL, http-client, imports, lib, time +{ mkDerivation +, base +, byteable +, bytestring +, gitignoreSource +, HsOpenSSL +, http-client +, imports +, lib +, time }: mkDerivation { pname = "ssl-util"; version = "0.1.0"; src = gitignoreSource ./.; libraryHaskellDepends = [ - base byteable bytestring HsOpenSSL http-client imports time + base + byteable + bytestring + HsOpenSSL + http-client + imports + time ]; description = "SSL-related utilities"; license = lib.licenses.agpl3Only; diff --git a/libs/tasty-cannon/default.nix b/libs/tasty-cannon/default.nix index 4b79d90f24..297f3ce945 100644 --- a/libs/tasty-cannon/default.nix +++ b/libs/tasty-cannon/default.nix @@ -2,19 +2,47 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, aeson, async, base, bilge, bytestring -, bytestring-conversion, data-timeout, exceptions, gitignoreSource -, http-client, http-types, imports, lib, random, tasty-hunit -, types-common, websockets, wire-api +{ mkDerivation +, aeson +, async +, base +, bilge +, bytestring +, bytestring-conversion +, data-timeout +, exceptions +, gitignoreSource +, http-client +, http-types +, imports +, lib +, random +, tasty-hunit +, types-common +, websockets +, wire-api }: mkDerivation { pname = "tasty-cannon"; version = "0.4.0"; src = gitignoreSource ./.; libraryHaskellDepends = [ - aeson async base bilge bytestring bytestring-conversion - data-timeout exceptions http-client http-types imports random - tasty-hunit types-common websockets wire-api + aeson + async + base + bilge + bytestring + bytestring-conversion + data-timeout + exceptions + http-client + http-types + imports + random + tasty-hunit + types-common + websockets + wire-api ]; description = "Cannon Integration Testing Utilities"; license = lib.licenses.agpl3Only; diff --git a/libs/types-common-aws/default.nix b/libs/types-common-aws/default.nix index 229d7060bf..647dd6884d 100644 --- a/libs/types-common-aws/default.nix +++ b/libs/types-common-aws/default.nix @@ -2,17 +2,44 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, amazonka, amazonka-sqs, base, base64-bytestring -, exceptions, gitignoreSource, imports, lens, lib, monad-control -, proto-lens, resourcet, safe, tasty, tasty-hunit, text, time +{ mkDerivation +, amazonka +, amazonka-sqs +, base +, base64-bytestring +, exceptions +, gitignoreSource +, imports +, lens +, lib +, monad-control +, proto-lens +, resourcet +, safe +, tasty +, tasty-hunit +, text +, time }: mkDerivation { pname = "types-common-aws"; version = "0.16.0"; src = gitignoreSource ./.; libraryHaskellDepends = [ - amazonka amazonka-sqs base base64-bytestring exceptions imports - lens monad-control proto-lens resourcet safe tasty tasty-hunit text + amazonka + amazonka-sqs + base + base64-bytestring + exceptions + imports + lens + monad-control + proto-lens + resourcet + safe + tasty + tasty-hunit + text time ]; description = "Shared AWS type definitions"; diff --git a/libs/types-common-journal/default.nix b/libs/types-common-journal/default.nix index 338c21abae..7dae825dfb 100644 --- a/libs/types-common-journal/default.nix +++ b/libs/types-common-journal/default.nix @@ -2,9 +2,19 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, base, bytestring, Cabal, gitignoreSource, imports -, lib, proto-lens-protoc, proto-lens-runtime, proto-lens-setup -, time, types-common, uuid +{ mkDerivation +, base +, bytestring +, Cabal +, gitignoreSource +, imports +, lib +, proto-lens-protoc +, proto-lens-runtime +, proto-lens-setup +, time +, types-common +, uuid }: mkDerivation { pname = "types-common-journal"; @@ -12,7 +22,13 @@ mkDerivation { src = gitignoreSource ./.; setupHaskellDepends = [ base Cabal proto-lens-setup ]; libraryHaskellDepends = [ - base bytestring imports proto-lens-runtime time types-common uuid + base + bytestring + imports + proto-lens-runtime + time + types-common + uuid ]; libraryToolDepends = [ proto-lens-protoc ]; description = "Shared protobuf type definitions"; diff --git a/libs/types-common/default.nix b/libs/types-common/default.nix index 425dfeffff..b1f22221ba 100644 --- a/libs/types-common/default.nix +++ b/libs/types-common/default.nix @@ -2,40 +2,140 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, aeson, attoparsec, attoparsec-iso8601, base -, base16-bytestring, base64-bytestring, binary, bytestring -, bytestring-conversion, cassandra-util, cereal, containers -, cryptohash-md5, cryptohash-sha1, cryptonite, currency-codes -, data-default, generic-random, gitignoreSource, hashable -, http-api-data, imports, iproute, iso3166-country-codes, iso639 -, lens, lens-datetime, lib, mime, optparse-applicative, pem -, protobuf, QuickCheck, quickcheck-instances, random -, schema-profunctor, scientific, servant-server, singletons -, string-conversions, swagger, swagger2, tagged, tasty, tasty-hunit -, tasty-quickcheck, text, time, time-locale-compat, tinylog, unix -, unordered-containers, uri-bytestring, uuid, vector, yaml +{ mkDerivation +, aeson +, attoparsec +, attoparsec-iso8601 +, base +, base16-bytestring +, base64-bytestring +, binary +, bytestring +, bytestring-conversion +, cassandra-util +, cereal +, containers +, cryptohash-md5 +, cryptohash-sha1 +, cryptonite +, currency-codes +, data-default +, generic-random +, gitignoreSource +, hashable +, http-api-data +, imports +, iproute +, iso3166-country-codes +, iso639 +, lens +, lens-datetime +, lib +, mime +, optparse-applicative +, pem +, protobuf +, QuickCheck +, quickcheck-instances +, random +, schema-profunctor +, scientific +, servant-server +, singletons +, string-conversions +, swagger +, swagger2 +, tagged +, tasty +, tasty-hunit +, tasty-quickcheck +, text +, time +, time-locale-compat +, tinylog +, unix +, unordered-containers +, uri-bytestring +, uuid +, vector +, yaml }: mkDerivation { pname = "types-common"; version = "0.16.0"; src = gitignoreSource ./.; libraryHaskellDepends = [ - aeson attoparsec attoparsec-iso8601 base base16-bytestring - base64-bytestring binary bytestring bytestring-conversion - cassandra-util containers cryptohash-md5 cryptohash-sha1 cryptonite - currency-codes data-default generic-random hashable http-api-data - imports iproute iso3166-country-codes iso639 lens lens-datetime - mime optparse-applicative pem protobuf QuickCheck - quickcheck-instances random schema-profunctor scientific - servant-server singletons string-conversions swagger swagger2 - tagged tasty text time time-locale-compat tinylog unix - unordered-containers uri-bytestring uuid vector yaml + aeson + attoparsec + attoparsec-iso8601 + base + base16-bytestring + base64-bytestring + binary + bytestring + bytestring-conversion + cassandra-util + containers + cryptohash-md5 + cryptohash-sha1 + cryptonite + currency-codes + data-default + generic-random + hashable + http-api-data + imports + iproute + iso3166-country-codes + iso639 + lens + lens-datetime + mime + optparse-applicative + pem + protobuf + QuickCheck + quickcheck-instances + random + schema-profunctor + scientific + servant-server + singletons + string-conversions + swagger + swagger2 + tagged + tasty + text + time + time-locale-compat + tinylog + unix + unordered-containers + uri-bytestring + uuid + vector + yaml ]; testHaskellDepends = [ - aeson base base16-bytestring base64-bytestring bytestring - bytestring-conversion cereal imports protobuf QuickCheck - string-conversions tasty tasty-hunit tasty-quickcheck text time - unordered-containers uuid + aeson + base + base16-bytestring + base64-bytestring + bytestring + bytestring-conversion + cereal + imports + protobuf + QuickCheck + string-conversions + tasty + tasty-hunit + tasty-quickcheck + text + time + unordered-containers + uuid ]; description = "Shared type definitions"; license = lib.licenses.agpl3Only; diff --git a/libs/types-common/src/Data/Misc.hs b/libs/types-common/src/Data/Misc.hs index c049381e78..30d3196596 100644 --- a/libs/types-common/src/Data/Misc.hs +++ b/libs/types-common/src/Data/Misc.hs @@ -74,6 +74,7 @@ import Data.ByteString.Lazy (toStrict) import Data.IP (IP (IPv4, IPv6), toIPv4, toIPv6b) import Data.Range import Data.Schema +import Data.String.Conversions (cs) import qualified Data.Swagger as S import qualified Data.Swagger.Build.Api as Doc import qualified Data.Text as Text @@ -83,7 +84,7 @@ import Servant (FromHttpApiData (..)) import Test.QuickCheck (Arbitrary (arbitrary), chooseInteger) import qualified Test.QuickCheck as QC import Text.Read (Read (..)) -import URI.ByteString hiding (Port) +import URI.ByteString hiding (Port, portNumber) import qualified URI.ByteString.QQ as URI.QQ -------------------------------------------------------------------------------- @@ -91,6 +92,7 @@ import qualified URI.ByteString.QQ as URI.QQ newtype IpAddr = IpAddr {ipAddr :: IP} deriving stock (Eq, Ord, Show, Generic) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema IpAddr) instance S.ToParamSchema IpAddr where toParamSchema _ = mempty & S.type_ ?~ S.SwaggerString @@ -125,24 +127,22 @@ newtype Port = Port {portNumber :: Word16} deriving stock (Eq, Ord, Show, Generic) deriving newtype (Real, Enum, Num, Integral, NFData, Arbitrary) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema Port) instance Read Port where readsPrec n = map (first Port) . readsPrec n -instance ToJSON IpAddr where - toJSON (IpAddr ip) = A.String (Text.pack $ show ip) - -instance FromJSON IpAddr where - parseJSON = A.withText "IpAddr" $ \txt -> - case readMaybe (Text.unpack txt) of - Nothing -> fail "Failed parsing IP address." - Just ip -> pure (IpAddr ip) +instance ToSchema IpAddr where + schema = toText .= parsedText "IpAddr" fromText + where + toText :: IpAddr -> Text + toText = cs . toByteString -instance ToJSON Port where - toJSON (Port p) = toJSON p + fromText :: Text -> Either String IpAddr + fromText = maybe (Left "Failed parsing IP address.") Right . fromByteString . cs -instance FromJSON Port where - parseJSON = fmap Port . parseJSON +instance ToSchema Port where + schema = Port <$> portNumber .= schema -------------------------------------------------------------------------------- -- Location @@ -158,8 +158,10 @@ instance ToSchema Location where schema = object "Location" $ Location - <$> _latitude .= field "lat" genericToSchema - <*> _longitude .= field "lon" genericToSchema + <$> _latitude + .= field "lat" genericToSchema + <*> _longitude + .= field "lon" genericToSchema instance Show Location where show p = @@ -273,7 +275,10 @@ instance ToSchema HttpsUrl where schema = (decodeUtf8 . toByteString') .= parsedText "HttpsUrl" (runParser parser . encodeUtf8) - & doc' . S.schema . S.example ?~ toJSON ("https://example.com" :: Text) + & doc' + . S.schema + . S.example + ?~ toJSON ("https://example.com" :: Text) instance Cql HttpsUrl where ctype = Tagged BlobColumn @@ -319,7 +324,10 @@ instance ToSchema (Fingerprint Rsa) where schema = (decodeUtf8 . B64.encode . fingerprintBytes) .= parsedText "Fingerprint" (runParser p . encodeUtf8) - & doc' . S.schema . S.example ?~ toJSON ("ioy3GeIjgQRsobf2EKGO3O8mq/FofFxHRqy0T4ERIZ8=" :: Text) + & doc' + . S.schema + . S.example + ?~ toJSON ("ioy3GeIjgQRsobf2EKGO3O8mq/FofFxHRqy0T4ERIZ8=" :: Text) where p :: Chars.Parser (Fingerprint Rsa) p = do @@ -353,7 +361,8 @@ instance Show PlainTextPassword where instance ToSchema PlainTextPassword where schema = PlainTextPassword - <$> fromPlainTextPassword .= untypedRangedSchema 6 1024 schema + <$> fromPlainTextPassword + .= untypedRangedSchema 6 1024 schema instance Arbitrary PlainTextPassword where -- TODO: why 6..1024? For tests we might want invalid passwords as well, e.g. 3 chars diff --git a/libs/wai-utilities/default.nix b/libs/wai-utilities/default.nix index ea9cebed79..93a249b3c7 100644 --- a/libs/wai-utilities/default.nix +++ b/libs/wai-utilities/default.nix @@ -2,24 +2,71 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, aeson, async, base, bytestring -, bytestring-conversion, errors, exceptions, gitignoreSource -, http-types, imports, kan-extensions, lib, metrics-core -, metrics-wai, pipes, prometheus-client, servant-server -, streaming-commons, string-conversions, swagger, swagger2, text -, tinylog, types-common, unix, wai, wai-predicates, wai-routing -, warp, warp-tls +{ mkDerivation +, aeson +, async +, base +, bytestring +, bytestring-conversion +, errors +, exceptions +, gitignoreSource +, http-types +, imports +, kan-extensions +, lib +, metrics-core +, metrics-wai +, pipes +, prometheus-client +, servant-server +, streaming-commons +, string-conversions +, swagger +, swagger2 +, text +, tinylog +, types-common +, unix +, wai +, wai-predicates +, wai-routing +, warp +, warp-tls }: mkDerivation { pname = "wai-utilities"; version = "0.16.1"; src = gitignoreSource ./.; libraryHaskellDepends = [ - aeson async base bytestring bytestring-conversion errors exceptions - http-types imports kan-extensions metrics-core metrics-wai pipes - prometheus-client servant-server streaming-commons - string-conversions swagger swagger2 text tinylog types-common unix - wai wai-predicates wai-routing warp warp-tls + aeson + async + base + bytestring + bytestring-conversion + errors + exceptions + http-types + imports + kan-extensions + metrics-core + metrics-wai + pipes + prometheus-client + servant-server + streaming-commons + string-conversions + swagger + swagger2 + text + tinylog + types-common + unix + wai + wai-predicates + wai-routing + warp + warp-tls ]; description = "Various helpers for WAI"; license = lib.licenses.agpl3Only; diff --git a/libs/wire-api-federation/default.nix b/libs/wire-api-federation/default.nix index 1fffa573fb..3c7d80254e 100644 --- a/libs/wire-api-federation/default.nix +++ b/libs/wire-api-federation/default.nix @@ -2,39 +2,145 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, aeson, aeson-pretty, async, base, bytestring -, bytestring-conversion, case-insensitive, containers, either -, errors, exceptions, gitignoreSource, hspec, hspec-discover -, http-media, http-types, http2, HUnit, imports, kan-extensions -, lens, lib, lifted-base, metrics-wai, mtl, network, QuickCheck -, retry, schema-profunctor, servant, servant-client -, servant-client-core, servant-server, singletons, sop-core -, streaming-commons, swagger2, template-haskell, text, time -, time-manager, tls, transformers, types-common, uuid -, wai-utilities, wire-api +{ mkDerivation +, aeson +, aeson-pretty +, async +, base +, bytestring +, bytestring-conversion +, case-insensitive +, containers +, either +, errors +, exceptions +, gitignoreSource +, hspec +, hspec-discover +, http-media +, http-types +, http2 +, HUnit +, imports +, kan-extensions +, lens +, lib +, lifted-base +, metrics-wai +, mtl +, network +, QuickCheck +, retry +, schema-profunctor +, servant +, servant-client +, servant-client-core +, servant-server +, singletons +, sop-core +, streaming-commons +, swagger2 +, template-haskell +, text +, time +, time-manager +, tls +, transformers +, types-common +, uuid +, wai-utilities +, wire-api }: mkDerivation { pname = "wire-api-federation"; version = "0.1.0"; src = gitignoreSource ./.; libraryHaskellDepends = [ - aeson async base bytestring bytestring-conversion case-insensitive - containers either errors exceptions http-media http-types http2 - imports kan-extensions lens lifted-base metrics-wai mtl network - QuickCheck schema-profunctor servant servant-client - servant-client-core servant-server singletons sop-core - streaming-commons swagger2 template-haskell text time time-manager - tls transformers types-common wai-utilities wire-api + aeson + async + base + bytestring + bytestring-conversion + case-insensitive + containers + either + errors + exceptions + http-media + http-types + http2 + imports + kan-extensions + lens + lifted-base + metrics-wai + mtl + network + QuickCheck + schema-profunctor + servant + servant-client + servant-client-core + servant-server + singletons + sop-core + streaming-commons + swagger2 + template-haskell + text + time + time-manager + tls + transformers + types-common + wai-utilities + wire-api ]; testHaskellDepends = [ - aeson aeson-pretty async base bytestring bytestring-conversion - case-insensitive containers either errors exceptions hspec - http-media http-types http2 HUnit imports kan-extensions lens - lifted-base metrics-wai mtl network QuickCheck retry - schema-profunctor servant servant-client servant-client-core - servant-server singletons sop-core streaming-commons swagger2 - template-haskell text time time-manager tls transformers - types-common uuid wai-utilities wire-api + aeson + aeson-pretty + async + base + bytestring + bytestring-conversion + case-insensitive + containers + either + errors + exceptions + hspec + http-media + http-types + http2 + HUnit + imports + kan-extensions + lens + lifted-base + metrics-wai + mtl + network + QuickCheck + retry + schema-profunctor + servant + servant-client + servant-client-core + servant-server + singletons + sop-core + streaming-commons + swagger2 + template-haskell + text + time + time-manager + tls + transformers + types-common + uuid + wai-utilities + wire-api ]; testToolDepends = [ hspec-discover ]; description = "The Wire server-to-server API for federation"; 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 ee690d531d..e7d86ab700 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 @@ -35,7 +35,7 @@ import Wire.API.Error.Galley import Wire.API.Federation.API.Common import Wire.API.Federation.Endpoint import Wire.API.Message -import Wire.API.Routes.Public.Galley +import Wire.API.Routes.Public.Galley.Messaging import Wire.API.Util.Aeson (CustomEncoded (..)) import Wire.Arbitrary (Arbitrary, GenericUniform (..)) diff --git a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/MessageSendResponse.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/MessageSendResponse.hs index 21ec69477c..efbcb2d93e 100644 --- a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/MessageSendResponse.hs +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/MessageSendResponse.hs @@ -25,7 +25,7 @@ import GHC.Exts (IsList (fromList)) import Imports import Wire.API.Federation.API.Galley (MessageSendResponse (..)) import Wire.API.Message -import Wire.API.Routes.Public.Galley +import Wire.API.Routes.Public.Galley.Messaging import Wire.API.User.Client (QualifiedUserClients (..)) missing :: QualifiedUserClients diff --git a/libs/wire-api/default.nix b/libs/wire-api/default.nix index 0569448d06..8564a5767a 100644 --- a/libs/wire-api/default.nix +++ b/libs/wire-api/default.nix @@ -2,61 +2,254 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, aeson, aeson-pretty, aeson-qq, async, attoparsec -, base, base64-bytestring, binary, binary-parsers, bytestring -, bytestring-arbitrary, bytestring-conversion, case-insensitive -, cassandra-util, cassava, cereal, comonad, conduit, constraints -, containers, cookie, cryptonite, currency-codes, deriving-aeson -, deriving-swagger2, directory, either, email-validate, errors -, extended, extra, filepath, generic-random, generics-sop, ghc-prim -, gitignoreSource, hashable, hex, hostname-validate, hscim -, http-api-data, http-media, http-types, imports -, insert-ordered-containers, iproute, iso3166-country-codes, iso639 -, lens, lib, memory, metrics-wai, mime, mtl, pem, polysemy, pretty -, process, proto-lens, protobuf, QuickCheck, quickcheck-instances -, random, resourcet, saml2-web-sso, schema-profunctor, scientific -, servant, servant-client, servant-client-core, servant-conduit -, servant-multipart, servant-server, servant-swagger -, servant-swagger-ui, singletons, sop-core, string-conversions -, swagger, swagger2, tagged, tasty, tasty-expected-failure -, tasty-hunit, tasty-quickcheck, text, time, types-common, unliftio -, unordered-containers, uri-bytestring, utf8-string, uuid, vector -, wai, wai-extra, wai-utilities, wai-websockets, websockets -, wire-message-proto-lens, x509, zauth +{ mkDerivation +, aeson +, aeson-pretty +, aeson-qq +, async +, attoparsec +, base +, base64-bytestring +, binary +, binary-parsers +, bytestring +, bytestring-arbitrary +, bytestring-conversion +, case-insensitive +, cassandra-util +, cassava +, cereal +, comonad +, conduit +, constraints +, containers +, cookie +, cryptonite +, currency-codes +, deriving-aeson +, deriving-swagger2 +, directory +, either +, email-validate +, errors +, extended +, extra +, filepath +, generic-random +, generics-sop +, ghc-prim +, gitignoreSource +, hashable +, hex +, hostname-validate +, hscim +, http-api-data +, http-media +, http-types +, imports +, insert-ordered-containers +, iproute +, iso3166-country-codes +, iso639 +, lens +, lib +, memory +, metrics-wai +, mime +, mtl +, pem +, polysemy +, pretty +, process +, proto-lens +, protobuf +, QuickCheck +, quickcheck-instances +, random +, resourcet +, saml2-web-sso +, schema-profunctor +, scientific +, servant +, servant-client +, servant-client-core +, servant-conduit +, servant-multipart +, servant-server +, servant-swagger +, servant-swagger-ui +, singletons +, sop-core +, string-conversions +, swagger +, swagger2 +, tagged +, tasty +, tasty-expected-failure +, tasty-hunit +, tasty-quickcheck +, text +, time +, types-common +, unliftio +, unordered-containers +, uri-bytestring +, utf8-string +, uuid +, vector +, wai +, wai-extra +, wai-utilities +, wai-websockets +, websockets +, wire-message-proto-lens +, x509 +, zauth }: mkDerivation { pname = "wire-api"; version = "0.1.0"; src = gitignoreSource ./.; libraryHaskellDepends = [ - aeson attoparsec base base64-bytestring binary binary-parsers - bytestring bytestring-conversion case-insensitive cassandra-util - cassava cereal comonad conduit constraints containers cookie - cryptonite currency-codes deriving-aeson deriving-swagger2 either - email-validate errors extended extra filepath generic-random - generics-sop ghc-prim hashable hostname-validate hscim - http-api-data http-media http-types imports - insert-ordered-containers iproute iso3166-country-codes iso639 lens - memory metrics-wai mime mtl pem polysemy proto-lens protobuf - QuickCheck quickcheck-instances random resourcet saml2-web-sso - schema-profunctor scientific servant servant-client - servant-client-core servant-conduit servant-multipart - servant-server servant-swagger servant-swagger-ui singletons - sop-core string-conversions swagger swagger2 tagged text time - types-common unordered-containers uri-bytestring utf8-string uuid - vector wai wai-extra wai-utilities wai-websockets websockets - wire-message-proto-lens x509 zauth + aeson + attoparsec + base + base64-bytestring + binary + binary-parsers + bytestring + bytestring-conversion + case-insensitive + cassandra-util + cassava + cereal + comonad + conduit + constraints + containers + cookie + cryptonite + currency-codes + deriving-aeson + deriving-swagger2 + either + email-validate + errors + extended + extra + filepath + generic-random + generics-sop + ghc-prim + hashable + hostname-validate + hscim + http-api-data + http-media + http-types + imports + insert-ordered-containers + iproute + iso3166-country-codes + iso639 + lens + memory + metrics-wai + mime + mtl + pem + polysemy + proto-lens + protobuf + QuickCheck + quickcheck-instances + random + resourcet + saml2-web-sso + schema-profunctor + scientific + servant + servant-client + servant-client-core + servant-conduit + servant-multipart + servant-server + servant-swagger + servant-swagger-ui + singletons + sop-core + string-conversions + swagger + swagger2 + tagged + text + time + types-common + unordered-containers + uri-bytestring + utf8-string + uuid + vector + wai + wai-extra + wai-utilities + wai-websockets + websockets + wire-message-proto-lens + x509 + zauth ]; testHaskellDepends = [ - aeson aeson-pretty aeson-qq async base binary bytestring - bytestring-arbitrary bytestring-conversion case-insensitive cassava - containers cryptonite currency-codes directory either filepath hex - hscim imports iso3166-country-codes iso639 lens memory metrics-wai - mime pem pretty process proto-lens QuickCheck saml2-web-sso - schema-profunctor servant servant-swagger-ui string-conversions - swagger2 tasty tasty-expected-failure tasty-hunit tasty-quickcheck - text time types-common unliftio unordered-containers uri-bytestring - uuid vector wire-message-proto-lens + aeson + aeson-pretty + aeson-qq + async + base + binary + bytestring + bytestring-arbitrary + bytestring-conversion + case-insensitive + cassava + containers + cryptonite + currency-codes + directory + either + filepath + hex + hscim + imports + iso3166-country-codes + iso639 + lens + memory + metrics-wai + mime + pem + pretty + process + proto-lens + QuickCheck + saml2-web-sso + schema-profunctor + servant + servant-swagger-ui + string-conversions + swagger2 + tasty + tasty-expected-failure + tasty-hunit + tasty-quickcheck + text + time + types-common + unliftio + unordered-containers + uri-bytestring + uuid + vector + wire-message-proto-lens ]; license = lib.licenses.agpl3Only; } diff --git a/libs/wire-api/src/Wire/API/Call/Config.hs b/libs/wire-api/src/Wire/API/Call/Config.hs index 4c45d67875..f01c1274ff 100644 --- a/libs/wire-api/src/Wire/API/Call/Config.hs +++ b/libs/wire-api/src/Wire/API/Call/Config.hs @@ -66,23 +66,24 @@ module Wire.API.Call.Config isTcp, isTls, limitServers, - - -- * Swagger - modelRtcConfiguration, - modelRtcIceServer, ) where import Control.Applicative (optional) -import Control.Lens hiding ((.=)) -import Data.Aeson hiding (()) -import Data.Attoparsec.Text hiding (parse) +import Control.Lens hiding (element, enum, (.=)) +import qualified Data.Aeson as A hiding (()) +import qualified Data.Aeson.Types as A +import Data.Attoparsec.Text hiding (Parser, parse) +import qualified Data.Attoparsec.Text as Text import Data.ByteString.Builder +import Data.ByteString.Conversion (toByteString) import qualified Data.ByteString.Conversion as BC import qualified Data.IP as IP import Data.List.NonEmpty (NonEmpty) import Data.Misc (HttpsUrl (..), IpAddr (IpAddr), Port (..)) -import qualified Data.Swagger.Build.Api as Doc +import Data.Schema +import Data.String.Conversions (cs) +import qualified Data.Swagger as S import qualified Data.Text as Text import Data.Text.Ascii import qualified Data.Text.Encoding as TE @@ -110,6 +111,7 @@ data RTCConfiguration = RTCConfiguration } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform RTCConfiguration) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema RTCConfiguration) rtcConfiguration :: NonEmpty RTCIceServer -> @@ -119,35 +121,18 @@ rtcConfiguration :: RTCConfiguration rtcConfiguration = RTCConfiguration -modelRtcConfiguration :: Doc.Model -modelRtcConfiguration = Doc.defineModel "RTCConfiguration" $ do - Doc.description "A subset of the WebRTC 'RTCConfiguration' dictionary" - Doc.property "ice_servers" (Doc.array (Doc.ref modelRtcIceServer)) $ - Doc.description "Array of 'RTCIceServer' objects" - Doc.property "sft_servers" (Doc.array (Doc.ref modelRtcSftServer)) $ - Doc.description "Array of 'SFTServer' objects (optional)" - Doc.property "ttl" Doc.int32' $ - Doc.description "Number of seconds after which the configuration should be refreshed (advisory)" - Doc.property "sft_servers_all" (Doc.array (Doc.ref modelRtcSftServerUrl)) $ - Doc.description "Array of all SFT servers" - -instance ToJSON RTCConfiguration where - toJSON (RTCConfiguration srvs sfts ttl all_servers) = - object - ( [ "ice_servers" .= srvs, - "ttl" .= ttl - ] - <> ["sft_servers" .= sfts | isJust sfts] - <> ["sft_servers_all" .= all_servers | isJust all_servers] - ) - -instance FromJSON RTCConfiguration where - parseJSON = withObject "RTCConfiguration" $ \o -> - RTCConfiguration - <$> o .: "ice_servers" - <*> o .:? "sft_servers" - <*> o .: "ttl" - <*> o .:? "sft_servers_all" +instance ToSchema RTCConfiguration where + schema = + objectWithDocModifier "RTCConfiguration" (description ?~ "A subset of the WebRTC 'RTCConfiguration' dictionary") $ + RTCConfiguration + <$> _rtcConfIceServers + .= fieldWithDocModifier "ice_servers" (description ?~ "Array of 'RTCIceServer' objects") (nonEmptyArray schema) + <*> _rtcConfSftServers + .= maybe_ (optFieldWithDocModifier "sft_servers" (description ?~ "Array of 'SFTServer' objects (optional)") (nonEmptyArray schema)) + <*> _rtcConfTTL + .= fieldWithDocModifier "ttl" (description ?~ "Number of seconds after which the configuration should be refreshed (advisory)") schema + <*> _rtcConfSftServersAll + .= maybe_ (optFieldWithDocModifier "sft_servers_all" (description ?~ "Array of all SFT servers") (array schema)) -------------------------------------------------------------------------------- -- SFTServer @@ -157,34 +142,22 @@ newtype SFTServer = SFTServer } deriving stock (Eq, Show, Ord, Generic) deriving (Arbitrary) via (GenericUniform SFTServer) - -instance ToJSON SFTServer where - toJSON (SFTServer url) = - object - [ "urls" .= [url] - ] - -instance FromJSON SFTServer where - parseJSON = withObject "SFTServer" $ \o -> - o .: "urls" >>= \case - [url] -> pure $ SFTServer url - xs -> fail $ "SFTServer can only have exactly one URL, found " <> show (length xs) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema SFTServer) + +instance ToSchema SFTServer where + schema = + objectWithDocModifier "SftServer" (description ?~ "Inspired by WebRTC 'RTCIceServer' object, contains details of SFT servers") $ + SFTServer + <$> (pure . _sftURL) + .= fieldWithDocModifier "urls" (description ?~ "Array containing exactly one SFT server address of the form 'https://:'") (withParser (array schema) p) + where + p :: [HttpsUrl] -> A.Parser HttpsUrl + p [url] = pure url + p xs = fail $ "SFTServer can only have exactly one URL, found " <> show (length xs) sftServer :: HttpsUrl -> SFTServer sftServer = SFTServer -modelRtcSftServer :: Doc.Model -modelRtcSftServer = Doc.defineModel "RTC SFT Server" $ do - Doc.description "Inspired by WebRTC 'RTCIceServer' object, contains details of SFT servers" - Doc.property "urls" (Doc.array Doc.string') $ - Doc.description "Array containing exactly one SFT server address of the form 'https://:'" - -modelRtcSftServerUrl :: Doc.Model -modelRtcSftServerUrl = Doc.defineModel "RTC SFT Server URL" $ do - Doc.description "Inspired by WebRTC 'RTCIceServer' object, contains details of SFT servers" - Doc.property "urls" (Doc.array Doc.string') $ - Doc.description "Array containing exactly one SFT server URL" - -------------------------------------------------------------------------------- -- RTCIceServer @@ -198,31 +171,21 @@ data RTCIceServer = RTCIceServer } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform RTCIceServer) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema RTCIceServer) rtcIceServer :: NonEmpty TurnURI -> TurnUsername -> AsciiBase64 -> RTCIceServer rtcIceServer = RTCIceServer -modelRtcIceServer :: Doc.Model -modelRtcIceServer = Doc.defineModel "RTCIceServer" $ do - Doc.description "A subset of the WebRTC 'RTCIceServer' object" - Doc.property "urls" (Doc.array Doc.string') $ - Doc.description "Array of TURN server addresses of the form 'turn::'" - Doc.property "username" Doc.string' $ - Doc.description "Username to use for authenticating against the given TURN servers" - Doc.property "credential" Doc.string' $ - Doc.description "Password to use for authenticating against the given TURN servers" - -instance ToJSON RTCIceServer where - toJSON (RTCIceServer urls name cred) = - object - [ "urls" .= urls, - "username" .= name, - "credential" .= cred - ] - -instance FromJSON RTCIceServer where - parseJSON = withObject "RTCIceServer" $ \o -> - RTCIceServer <$> o .: "urls" <*> o .: "username" <*> o .: "credential" +instance ToSchema RTCIceServer where + schema = + objectWithDocModifier "RTCIceServer" (description ?~ "A subset of the WebRTC 'RTCIceServer' object") $ + RTCIceServer + <$> _iceURLs + .= fieldWithDocModifier "urls" (description ?~ "Array of TURN server addresses of the form 'turn::'") (nonEmptyArray schema) + <*> _iceUsername + .= fieldWithDocModifier "username" (description ?~ "Username to use for authenticating against the given TURN servers") schema + <*> _iceCredential + .= fieldWithDocModifier "credential" (description ?~ "Password to use for authenticating against the given TURN servers") schema -------------------------------------------------------------------------------- -- TurnURI @@ -244,6 +207,10 @@ data TurnURI = TurnURI _turiTransport :: Maybe Transport } deriving stock (Eq, Show, Ord, Generic) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema TurnURI) + +instance ToSchema TurnURI where + schema = (cs . toByteString) .= parsedText "TurnURI" parseTurnURI turnURI :: Scheme -> TurnHost -> Port -> Maybe Transport -> TurnURI turnURI = TurnURI @@ -277,12 +244,6 @@ parseTurnURI = parseOnly (parser <* endOfInput) Just ok -> pure ok Nothing -> fail (err ++ " failed when parsing: " ++ show x) -instance ToJSON TurnURI where - toJSON = String . TE.decodeUtf8 . BC.toByteString' - -instance FromJSON TurnURI where - parseJSON = withText "TurnURI" $ either fail pure . parseTurnURI - instance Arbitrary TurnURI where arbitrary = (getGenericUniform <$> arbitrary) `QC.suchThat` (not . isIPv6) where @@ -295,6 +256,7 @@ data Scheme | SchemeTurns deriving stock (Eq, Show, Ord, Generic) deriving (Arbitrary) via (GenericUniform Scheme) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema Scheme) instance BC.ToByteString Scheme where builder SchemeTurn = "turn" @@ -307,19 +269,63 @@ instance BC.FromByteString Scheme where "turns" -> pure SchemeTurns _ -> fail $ "Invalid turn scheme: " ++ show t -instance ToJSON Scheme where - toJSON = String . TE.decodeUtf8 . BC.toByteString' - -instance FromJSON Scheme where - parseJSON = - withText "Scheme" $ - either fail pure . BC.runParser BC.parser . TE.encodeUtf8 +instance ToSchema Scheme where + schema = + enum @Text "Scheme" $ + mconcat + [ element "turn" SchemeTurn, + element "turns" SchemeTurns + ] data TurnHost = TurnHostIp IpAddr | TurnHostName Text deriving stock (Eq, Show, Ord, Generic) - deriving anyclass (ToJSON, FromJSON) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema TurnHost) + +instance ToSchema TurnHost where + schema = turnHostSchema + +data TurnHostTag = TurnHostIpTag | TurnHostNameTag + deriving (Eq, Enum, Bounded) + +tagSchema :: ValueSchema NamedSwaggerDoc TurnHostTag +tagSchema = + enum @Text "TurnHostTag" $ + mconcat + [ element "TurnHostIp" TurnHostIpTag, + element "TurnHostName" TurnHostNameTag + ] + +turnHostSchema :: ValueSchema NamedSwaggerDoc TurnHost +turnHostSchema = + object "TurnHost" $ + fromTagged + <$> toTagged + .= bind + (fst .= field "tag" tagSchema) + (snd .= fieldOver _1 "contents" untaggedSchema) + where + toTagged :: TurnHost -> (TurnHostTag, TurnHost) + toTagged d@(TurnHostIp _) = (TurnHostIpTag, d) + toTagged d@(TurnHostName _) = (TurnHostNameTag, d) + + fromTagged :: (TurnHostTag, TurnHost) -> TurnHost + fromTagged = snd + + untaggedSchema = dispatch $ \case + TurnHostIpTag -> tag _TurnHostIp (unnamed schema) + TurnHostNameTag -> tag _TurnHostName (unnamed schema) + + _TurnHostIp :: Prism' TurnHost IpAddr + _TurnHostIp = prism' TurnHostIp $ \case + TurnHostIp a -> Just a + _ -> Nothing + + _TurnHostName :: Prism' TurnHost Text + _TurnHostName = prism' TurnHostName $ \case + TurnHostName b -> Just b + _ -> Nothing instance BC.FromByteString TurnHost where parser = BC.parser >>= maybe (fail "Invalid turn host") pure . parseTurnHost @@ -362,6 +368,7 @@ data Transport | TransportTCP deriving stock (Eq, Show, Ord, Generic) deriving (Arbitrary) via (GenericUniform Transport) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema Transport) instance BC.ToByteString Transport where builder TransportUDP = "udp" @@ -374,13 +381,13 @@ instance BC.FromByteString Transport where "tcp" -> pure TransportTCP _ -> fail $ "Invalid turn transport: " ++ show t -instance ToJSON Transport where - toJSON = String . TE.decodeUtf8 . BC.toByteString' - -instance FromJSON Transport where - parseJSON = - withText "Transport" $ - either fail pure . BC.runParser BC.parser . TE.encodeUtf8 +instance ToSchema Transport where + schema = + enum @Text "Transport" $ + mconcat + [ element "udp" TransportUDP, + element "tcp" TransportTCP + ] -------------------------------------------------------------------------------- -- TurnUsername @@ -397,6 +404,7 @@ data TurnUsername = TurnUsername _tuRandom :: Text } deriving stock (Eq, Show, Generic) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema TurnUsername) -- note that the random value is not checked for well-formedness turnUsername :: POSIXTime -> Text -> TurnUsername @@ -409,13 +417,14 @@ turnUsername expires rnd = _tuRandom = rnd } -instance ToJSON TurnUsername where - toJSON = String . view utf8 . BC.toByteString' +instance ToSchema TurnUsername where + schema = toText .= parsedText "" fromText + where + fromText :: Text -> Either String TurnUsername + fromText = parseOnly (parseTurnUsername <* endOfInput) -instance FromJSON TurnUsername where - parseJSON = - withText "TurnUsername" $ - either fail pure . parseOnly (parseTurnUsername <* endOfInput) + toText :: TurnUsername -> Text + toText = cs . toByteString instance BC.ToByteString TurnUsername where builder tu = @@ -430,7 +439,7 @@ instance BC.ToByteString TurnUsername where <> shortByteString ".r=" <> byteString (view (re utf8) (_tuRandom tu)) -parseTurnUsername :: Parser TurnUsername +parseTurnUsername :: Text.Parser TurnUsername parseTurnUsername = TurnUsername <$> (string "d=" *> fmap (fromIntegral :: Word64 -> POSIXTime) decimal) diff --git a/libs/wire-api/src/Wire/API/Conversation.hs b/libs/wire-api/src/Wire/API/Conversation.hs index 8bcafda6ed..283c6a0a85 100644 --- a/libs/wire-api/src/Wire/API/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Conversation.hs @@ -25,6 +25,7 @@ module Wire.API.Conversation ConversationMetadata (..), defConversationMetadata, Conversation (..), + conversationMetadataObjectSchema, cnvType, cnvCreator, cnvAccess, @@ -44,6 +45,7 @@ module Wire.API.Conversation pattern ConversationPagingState, ConversationsResponse (..), GroupId (..), + mlsSelfConvId, -- * Conversation properties Access (..), @@ -97,19 +99,22 @@ import Control.Applicative import Control.Lens (at, (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson as A +import qualified Data.ByteString.Lazy as LBS import Data.Id -import Data.List.Extra (disjointOrd) +import Data.List.Extra (disjointOrd, enumerate) import Data.List.NonEmpty (NonEmpty) import Data.List1 import Data.Misc -import Data.Proxy (Proxy (Proxy)) import Data.Qualified (Qualified (qUnqualified), deprecatedSchema) import Data.Range (Range, fromRange, rangedSchema) +import Data.SOP import Data.Schema import qualified Data.Set as Set import Data.String.Conversions (cs) import qualified Data.Swagger as S import qualified Data.Swagger.Build.Api as Doc +import qualified Data.UUID as UUID +import qualified Data.UUID.V5 as UUIDV5 import Imports import System.Random (randomRIO) import Wire.API.Conversation.Member @@ -117,6 +122,7 @@ import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role (RoleName, roleNameWireAdmin) import Wire.API.MLS.Group import Wire.API.Routes.MultiTablePaging +import Wire.API.Routes.MultiVerb import Wire.Arbitrary -------------------------------------------------------------------------------- @@ -431,6 +437,10 @@ data Access LinkAccess | -- | User can join knowing [changeable/revokable] code CodeAccess + | -- | In MLS the user can join the global team conversation with their + -- | clients via an external commit, thereby inviting their own clients to + -- | join. + SelfInviteAccess deriving stock (Eq, Ord, Bounded, Enum, Show, Generic) deriving (Arbitrary) via (GenericUniform Access) deriving (ToJSON, FromJSON, S.ToSchema) via Schema Access @@ -443,7 +453,8 @@ instance ToSchema Access where [ element "private" PrivateAccess, element "invite" InviteAccess, element "link" LinkAccess, - element "code" CodeAccess + element "code" CodeAccess, + element "self_invite" SelfInviteAccess ] typeAccess :: Doc.DataType @@ -491,6 +502,7 @@ defRole = activatedAccessRole maybeRole :: ConvType -> Maybe (Set AccessRoleV2) -> Set AccessRoleV2 maybeRole SelfConv _ = privateAccessRole +maybeRole GlobalTeamConv _ = teamAccessRole maybeRole ConnectConv _ = privateAccessRole maybeRole One2OneConv _ = privateAccessRole maybeRole RegularConv Nothing = defRole @@ -573,7 +585,8 @@ data ConvType | SelfConv | One2OneConv | ConnectConv - deriving stock (Eq, Show, Generic) + | GlobalTeamConv + deriving stock (Eq, Show, Generic, Enum, Bounded) deriving (Arbitrary) via (GenericUniform ConvType) deriving (FromJSON, ToJSON, S.ToSchema) via Schema ConvType @@ -584,11 +597,12 @@ instance ToSchema ConvType where [ element 0 RegularConv, element 1 SelfConv, element 2 One2OneConv, - element 3 ConnectConv + element 3 ConnectConv, + element 4 GlobalTeamConv ] typeConversationType :: Doc.DataType -typeConversationType = Doc.int32 $ Doc.enum [0, 1, 2, 3] +typeConversationType = Doc.int32 $ Doc.enum $ fromIntegral . fromEnum <$> enumerate @ConvType -- | Define whether receipts should be sent in the given conversation -- This datatype is defined as an int32 but the Backend does not @@ -934,3 +948,21 @@ instance ToSchema ConversationMemberUpdate where $ ConversationMemberUpdate <$> cmuTarget .= field "target" schema <*> cmuUpdate .= field "update" schema + +-- | The id of the MLS self conversation for a given user +mlsSelfConvId :: UserId -> ConvId +mlsSelfConvId uid = + let inputBytes = LBS.unpack . UUID.toByteString . toUUID $ uid + in Id (UUIDV5.generateNamed namespaceMLSSelfConv inputBytes) + +namespaceMLSSelfConv :: UUID.UUID +namespaceMLSSelfConv = + -- a V5 uuid created with the nil namespace + fromJust . UUID.fromString $ "3eac2a2c-3850-510b-bd08-8a98e80dd4d9" + +-------------------------------------------------------------------------------- +-- MultiVerb instances + +instance AsHeaders '[ConvId] Conversation Conversation where + toHeaders c = (I (qUnqualified (cnvQualifiedId c)) :* Nil, c) + fromHeaders = snd diff --git a/libs/wire-api/src/Wire/API/Conversation/Action.hs b/libs/wire-api/src/Wire/API/Conversation/Action.hs index 2d92ec4365..815903cb3e 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Action.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Action.hs @@ -53,6 +53,7 @@ import Wire.Arbitrary (Arbitrary (..)) -- individual effects per conversation action. See 'HasConversationActionEffects'. type family ConversationAction (tag :: ConversationActionTag) :: * where ConversationAction 'ConversationJoinTag = ConversationJoin + ConversationAction 'ConversationSelfInviteTag = ConvId ConversationAction 'ConversationLeaveTag = () ConversationAction 'ConversationMemberUpdateTag = ConversationMemberUpdate ConversationAction 'ConversationDeleteTag = () @@ -103,6 +104,7 @@ conversationActionSchema SConversationRenameTag = schema conversationActionSchema SConversationMessageTimerUpdateTag = schema conversationActionSchema SConversationReceiptModeUpdateTag = schema conversationActionSchema SConversationAccessDataTag = schema +conversationActionSchema SConversationSelfInviteTag = schema instance FromJSON SomeConversationAction where parseJSON = A.withObject "SomeConversationAction" $ \ob -> do @@ -150,6 +152,9 @@ conversationActionToEvent tag now quid qcnv action = SConversationJoinTag -> let ConversationJoin newMembers role = action in EdMembersJoin $ SimpleMembers (map (`SimpleMember` role) (toList newMembers)) + SConversationSelfInviteTag -> + -- this event will not be sent anyway so this is a dummy event + EdMembersJoin $ SimpleMembers [] SConversationLeaveTag -> EdMembersLeave (QualifiedUserIdList [quid]) SConversationRemoveMembersTag -> diff --git a/libs/wire-api/src/Wire/API/Conversation/Action/Tag.hs b/libs/wire-api/src/Wire/API/Conversation/Action/Tag.hs index 3445e3794f..3b0c782c37 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Action/Tag.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Action/Tag.hs @@ -30,6 +30,7 @@ import Wire.Arbitrary (Arbitrary (..)) data ConversationActionTag = ConversationJoinTag + | ConversationSelfInviteTag | ConversationLeaveTag | ConversationRemoveMembersTag | ConversationMemberUpdateTag @@ -48,6 +49,7 @@ instance ToSchema ConversationActionTag where enum @Text "ConversationActionTag" $ mconcat [ element "ConversationJoinTag" ConversationJoinTag, + element "ConversationSelfInviteTag" ConversationSelfInviteTag, element "ConversationLeaveTag" ConversationLeaveTag, element "ConversationRemoveMembersTag" ConversationRemoveMembersTag, element "ConversationMemberUpdateTag" ConversationMemberUpdateTag, diff --git a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs index 30ca0b6591..d580e5be88 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs @@ -28,6 +28,7 @@ module Wire.API.Conversation.Protocol _ProtocolMLS, _ProtocolProteus, protocolSchema, + mlsDataSchema, ConversationMLSData (..), ) where diff --git a/libs/wire-api/src/Wire/API/Conversation/Role.hs b/libs/wire-api/src/Wire/API/Conversation/Role.hs index e215b72db8..1878b99b65 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Role.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Role.hs @@ -36,6 +36,7 @@ module Wire.API.Conversation.Role wireConvRoleNames, roleNameWireAdmin, roleNameWireMember, + roleToRoleName, -- * Action Action (..), diff --git a/libs/wire-api/src/Wire/API/Error/Brig.hs b/libs/wire-api/src/Wire/API/Error/Brig.hs index 0903494e5e..aa6cc3ce7f 100644 --- a/libs/wire-api/src/Wire/API/Error/Brig.hs +++ b/libs/wire-api/src/Wire/API/Error/Brig.hs @@ -75,6 +75,10 @@ data BrigError | InvalidPasswordResetKey | InvalidPasswordResetCode | ResetPasswordMustDiffer + | NoEmail + | NotificationNotFound + | PendingInvitationNotFound + | ConflictingInvitations instance KnownError (MapError e) => IsSwaggerError (e :: BrigError) where addToSwagger = addStaticErrorToSwagger @(MapError e) @@ -217,3 +221,11 @@ type instance MapError 'InvalidPasswordResetKey = 'StaticError 400 "invalid-key" 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." + +type instance MapError 'NoEmail = 'StaticError 403 "no-email" "This operation requires the user to have a verified email address." + +type instance MapError 'NotificationNotFound = 'StaticError 404 "not-found" "Notification not found." + +type instance MapError 'PendingInvitationNotFound = 'StaticError 404 "not-found" "No pending invitations exists." + +type instance MapError 'ConflictingInvitations = 'StaticError 409 "conflicting-invitations" "Multiple conflicting invitations to different teams exists." diff --git a/libs/wire-api/src/Wire/API/Error/Galley.hs b/libs/wire-api/src/Wire/API/Error/Galley.hs index 171f894b3d..d714737c03 100644 --- a/libs/wire-api/src/Wire/API/Error/Galley.hs +++ b/libs/wire-api/src/Wire/API/Error/Galley.hs @@ -83,6 +83,8 @@ data GalleyError | MLSClientSenderUserMismatch | MLSWelcomeMismatch | MLSMissingGroupInfo + | MLSMissingSenderClient + | MLSUnexpectedSenderClient | -- NoBindingTeamMembers | NoBindingTeam @@ -202,10 +204,14 @@ type instance MapError 'MLSGroupConversationMismatch = 'StaticError 400 "mls-gro 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 'MLSUnexpectedSenderClient = 'StaticError 422 "mls-unexpected-sender-client-found" "Unexpected creator client set. This is a newly created conversation and it expected exactly one client." + 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 'MLSMissingSenderClient = 'StaticError 403 "mls-missing-sender-client" "The client has to refresh their access token and provide their client ID" + type instance MapError 'NoBindingTeamMembers = 'StaticError 403 "non-binding-team-members" "Both users must be members of the same binding team" type instance MapError 'NoBindingTeam = 'StaticError 403 "no-binding-team" "Operation allowed only on binding teams" diff --git a/libs/wire-api/src/Wire/API/Event/Conversation.hs b/libs/wire-api/src/Wire/API/Event/Conversation.hs index f05018432d..58b50fce8e 100644 --- a/libs/wire-api/src/Wire/API/Event/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Event/Conversation.hs @@ -73,6 +73,7 @@ import qualified Data.Aeson.KeyMap as KeyMap import Data.Id import Data.Json.Util import Data.Qualified +import Data.SOP import Data.Schema import qualified Data.Swagger as S import Data.Time @@ -83,6 +84,7 @@ import Wire.API.Conversation import Wire.API.Conversation.Code (ConversationCode (..)) import Wire.API.Conversation.Role import Wire.API.Conversation.Typing (TypingData (..)) +import Wire.API.Routes.MultiVerb import Wire.API.User (QualifiedUserIdList (..)) import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) @@ -413,3 +415,17 @@ instance ToJSON Event where instance S.ToSchema Event where declareNamedSchema = schemaToSwagger + +-------------------------------------------------------------------------------- +-- MultiVerb instances + +instance + (ResponseType r1 ~ ConversationCode, ResponseType r2 ~ Event) => + AsUnion '[r1, r2] AddCodeResult + where + toUnion (CodeAlreadyExisted c) = Z (I c) + toUnion (CodeAdded e) = S (Z (I e)) + + fromUnion (Z (I c)) = CodeAlreadyExisted c + fromUnion (S (Z (I e))) = CodeAdded e + fromUnion (S (S x)) = case x of {} diff --git a/libs/wire-api/src/Wire/API/Event/FeatureConfig.hs b/libs/wire-api/src/Wire/API/Event/FeatureConfig.hs index 967d5bc864..09f670db6f 100644 --- a/libs/wire-api/src/Wire/API/Event/FeatureConfig.hs +++ b/libs/wire-api/src/Wire/API/Event/FeatureConfig.hs @@ -30,7 +30,9 @@ import Data.Schema import qualified Data.Swagger as S import GHC.TypeLits (KnownSymbol) import Imports +import Test.QuickCheck.Gen (oneof) import Wire.API.Team.Feature +import Wire.Arbitrary (Arbitrary (..), GenericUniform (..)) data Event = Event { _eventType :: EventType, @@ -40,8 +42,34 @@ data Event = Event deriving (Eq, Show, Generic) deriving (A.ToJSON, A.FromJSON) via Schema Event +instance Arbitrary Event where + arbitrary = + do + let arbConfig = + oneof + [ arbitrary @(WithStatus SSOConfig) <&> toJSON, + arbitrary @(WithStatus SearchVisibilityAvailableConfig) <&> toJSON, + arbitrary @(WithStatus ValidateSAMLEmailsConfig) <&> toJSON, + arbitrary @(WithStatus DigitalSignaturesConfig) <&> toJSON, + arbitrary @(WithStatus AppLockConfig) <&> toJSON, + arbitrary @(WithStatus FileSharingConfig) <&> toJSON, + arbitrary @(WithStatus ClassifiedDomainsConfig) <&> toJSON, + arbitrary @(WithStatus ConferenceCallingConfig) <&> toJSON, + arbitrary @(WithStatus SelfDeletingMessagesConfig) <&> toJSON, + arbitrary @(WithStatus GuestLinksConfig) <&> toJSON, + arbitrary @(WithStatus SndFactorPasswordChallengeConfig) <&> toJSON, + arbitrary @(WithStatus SearchVisibilityInboundConfig) <&> toJSON, + arbitrary @(WithStatus MLSConfig) <&> toJSON, + arbitrary @(WithStatus ExposeInvitationURLsToTeamAdminConfig) <&> toJSON + ] + Event + <$> arbitrary + <*> arbitrary + <*> arbConfig + data EventType = Update - deriving (Eq, Show) + deriving (Eq, Show, Generic) + deriving (Arbitrary) via GenericUniform EventType instance ToSchema EventType where schema = diff --git a/libs/wire-api/src/Wire/API/MLS/GlobalTeamConversation.hs b/libs/wire-api/src/Wire/API/MLS/GlobalTeamConversation.hs new file mode 100644 index 0000000000..f9f1096860 --- /dev/null +++ b/libs/wire-api/src/Wire/API/MLS/GlobalTeamConversation.hs @@ -0,0 +1,63 @@ +-- 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.GlobalTeamConversation where + +import Control.Lens ((?~)) +import Data.Aeson (FromJSON, ToJSON) +import Data.Id +import Data.Qualified +import Data.Schema +import qualified Data.Swagger as S +import Imports +import Wire.API.Conversation hiding (Conversation) +import Wire.API.Conversation.Protocol +import Wire.Arbitrary (Arbitrary (..), GenericUniform (..)) + +-- | Public-facing global team conversation. +-- Membership is implicit. Every member of a team is part of it. +-- Protocol is also implicit: it's always MLS. +data GlobalTeamConversation = GlobalTeamConversation + { gtcId :: Qualified ConvId, + gtcMlsMetadata :: ConversationMLSData, + gtcCreator :: Maybe UserId, + gtcAccess :: [Access], + gtcName :: Text, + gtcTeam :: TeamId + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform GlobalTeamConversation) + deriving (FromJSON, ToJSON, S.ToSchema) via Schema GlobalTeamConversation + +instance ToSchema GlobalTeamConversation where + schema = + objectWithDocModifier + "GlobalTeamConversation" + (description ?~ "The global team conversation object as returned from the server") + $ GlobalTeamConversation + <$> gtcId .= field "qualified_id" schema + <*> gtcMlsMetadata .= mlsDataSchema + <*> gtcCreator + .= maybe_ + ( optFieldWithDocModifier + "creator" + (description ?~ "The creator's user ID") + schema + ) + <*> gtcAccess .= field "access" (array schema) + <*> gtcName .= field "name" schema + <*> gtcTeam .= field "team" schema diff --git a/libs/wire-api/src/Wire/API/MLS/Proposal.hs b/libs/wire-api/src/Wire/API/MLS/Proposal.hs index b67ec6223f..1226811c6e 100644 --- a/libs/wire-api/src/Wire/API/MLS/Proposal.hs +++ b/libs/wire-api/src/Wire/API/MLS/Proposal.hs @@ -19,6 +19,7 @@ module Wire.API.MLS.Proposal where +import Cassandra import Control.Arrow import Control.Lens (makePrisms) import Data.Binary @@ -191,3 +192,23 @@ instance ParseMLS ProposalRef where parseMLS = ProposalRef <$> getByteString 16 makePrisms ''ProposalOrRef + +data ProposalOrigin + = ProposalOriginClient + | ProposalOriginBackend + deriving (Eq) + +instance Cql ProposalOrigin where + ctype = Tagged IntColumn + toCql = CqlInt . originToInt + fromCql (CqlInt i) = intToOrigin i + fromCql _ = Left "intToOrigin: unexptected data" + +originToInt :: ProposalOrigin -> Int32 +originToInt ProposalOriginClient = 0 +originToInt ProposalOriginBackend = 1 + +intToOrigin :: Int32 -> Either String ProposalOrigin +intToOrigin 0 = pure ProposalOriginClient +intToOrigin 1 = pure ProposalOriginBackend +intToOrigin n = Left $ "intToOrigin: unexptected int constant: " <> show n diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index c616d1f49c..deff0d727c 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -25,6 +25,7 @@ module Wire.API.Routes.Public ZLocalUser, ZConn, ZOptUser, + ZOptClient, ZOptConn, ZBot, ZConversation, @@ -179,6 +180,8 @@ type ZProvider = ZAuthServant 'ZAuthProvider InternalAuthDefOpts type ZOptUser = ZAuthServant 'ZAuthUser '[Servant.Optional, Servant.Strict] +type ZOptClient = ZAuthServant 'ZAuthClient '[Servant.Optional, Servant.Strict] + type ZOptConn = ZAuthServant 'ZAuthConn '[Servant.Optional, Servant.Strict] instance HasSwagger api => HasSwagger (ZAuthServant 'ZAuthUser _opts :> api) where 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 b48d3e1cb7..5a2999282e 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -41,6 +41,7 @@ import Servant (JSON) import Servant hiding (Handler, JSON, addHeader, respond) import Servant.Swagger (HasSwagger (toSwagger)) import Servant.Swagger.Internal.Orphans () +import Wire.API.Call.Config (RTCConfiguration) import Wire.API.Connection hiding (MissingLegalholdConsent) import Wire.API.Error import Wire.API.Error.Brig @@ -56,6 +57,8 @@ import Wire.API.Routes.Public import Wire.API.Routes.Public.Util import Wire.API.Routes.QualifiedCapture import Wire.API.Routes.Version +import Wire.API.Team.Invitation +import Wire.API.Team.Size import Wire.API.User hiding (NoIdentity) import Wire.API.User.Activation import Wire.API.User.Auth @@ -68,6 +71,28 @@ import Wire.API.User.RichInfo (RichInfoAssocList) import Wire.API.User.Search (Contact, RoleFilter, SearchResult, TeamContact, TeamUserSearchSortBy, TeamUserSearchSortOrder) import Wire.API.UserMap +type BrigAPI = + UserAPI + :<|> SelfAPI + :<|> AccountAPI + :<|> ClientAPI + :<|> PrekeyAPI + :<|> UserClientAPI + :<|> ConnectionAPI + :<|> PropertiesAPI + :<|> MLSAPI + :<|> UserHandleAPI + :<|> SearchAPI + :<|> AuthAPI + :<|> CallingAPI + :<|> TeamsAPI + +brigSwagger :: Swagger +brigSwagger = toSwagger (Proxy @BrigAPI) + +------------------------------------------------------------------------------- +-- User API + type MaxUsersForListClientsBulk = 500 type GetUserVerb = @@ -524,8 +549,10 @@ instance ToSchema DeprecatedMatchingResult where object "DeprecatedMatchingResult" $ DeprecatedMatchingResult - <$ const [] .= field "results" (array (null_ @SwaggerDoc)) - <* const [] .= field "auto-connects" (array (null_ @SwaggerDoc)) + <$ const [] + .= field "results" (array (null_ @SwaggerDoc)) + <* const [] + .= field "auto-connects" (array (null_ @SwaggerDoc)) data ActivationRespWithStatus = ActivationResp ActivationResponse @@ -1254,19 +1281,150 @@ type AuthAPI = :> MultiVerb1 'POST '[JSON] (RespondEmpty 200 "Cookies revoked") ) -type BrigAPI = - UserAPI - :<|> SelfAPI - :<|> AccountAPI - :<|> ClientAPI - :<|> PrekeyAPI - :<|> UserClientAPI - :<|> ConnectionAPI - :<|> PropertiesAPI - :<|> MLSAPI - :<|> UserHandleAPI - :<|> SearchAPI - :<|> AuthAPI +------------------------------------------------------------------------------- +-- Calling API -brigSwagger :: Swagger -brigSwagger = toSwagger (Proxy @BrigAPI) +type CallingAPI = + -- Deprecated endpoint, but still used by old clients. + -- See https://github.com/zinfra/backend-issues/issues/1616 for context + Named + "get-calls-config" + ( Summary + "[deprecated] Retrieve TURN server addresses and credentials for \ + \ IP addresses, scheme `turn` and transport `udp` only" + :> ZUser + :> ZConn + :> "calls" + :> "config" + :> Get '[JSON] RTCConfiguration + ) + :<|> Named + "get-calls-config-v2" + ( Summary + "Retrieve all TURN server addresses and credentials. \ + \Clients are expected to do a DNS lookup to resolve \ + \the IP addresses of the given hostnames " + :> ZUser + :> ZConn + :> "calls" + :> "config" + :> "v2" + :> QueryParam' '[Optional, Strict, Description "Limit resulting list. Allowed values [1..10]"] "limit" (Range 1 10 Int) + :> Get '[JSON] RTCConfiguration + ) + +-- Teams API ----------------------------------------------------- + +type TeamsAPI = + Named + "send-team-invitation" + ( Summary "Create and send a new team invitation." + :> Description + "Invitations are sent by email. The maximum allowed number of \ + \pending team invitations is equal to the team size." + :> CanThrow 'NoEmail + :> CanThrow 'NoIdentity + :> CanThrow 'InvalidEmail + :> CanThrow 'BlacklistedEmail + :> CanThrow 'TooManyTeamInvitations + :> CanThrow 'InsufficientTeamPermissions + :> ZUser + :> "teams" + :> Capture "tid" TeamId + :> "invitations" + :> ReqBody '[JSON] InvitationRequest + :> MultiVerb1 + 'POST + '[JSON] + ( WithHeaders + '[Header "Location" InvitationLocation] + (Invitation, InvitationLocation) + (Respond 201 "Invitation was created and sent." Invitation) + ) + ) + :<|> Named + "get-team-invitations" + ( Summary "List the sent team invitations" + :> CanThrow 'InsufficientTeamPermissions + :> ZUser + :> "teams" + :> Capture "tid" TeamId + :> "invitations" + :> QueryParam' '[Optional, Strict, Description "Invitation id to start from (ascending)."] "start" InvitationId + :> QueryParam' '[Optional, Strict, Description "Number of results to return (default 100, max 500)."] "size" (Range 1 500 Int32) + :> MultiVerb1 + 'GET + '[JSON] + (Respond 200 "List of sent invitations" InvitationList) + ) + :<|> Named + "get-team-invitation" + ( Summary "Get a pending team invitation by ID." + :> CanThrow 'InsufficientTeamPermissions + :> ZUser + :> "teams" + :> Capture "tid" TeamId + :> "invitations" + :> Capture "iid" InvitationId + :> MultiVerb + 'GET + '[JSON] + '[ ErrorResponse 'NotificationNotFound, + Respond 200 "Invitation" Invitation + ] + (Maybe Invitation) + ) + :<|> Named + "delete-team-invitation" + ( Summary "Delete a pending team invitation by ID." + :> CanThrow 'InsufficientTeamPermissions + :> ZUser + :> "teams" + :> Capture "tid" TeamId + :> "invitations" + :> Capture "iid" InvitationId + :> MultiVerb1 'DELETE '[JSON] (RespondEmpty 200 "Invitation deleted") + ) + :<|> Named + "get-team-invitation-info" + ( Summary "Get invitation info given a code." + :> CanThrow 'InvalidInvitationCode + :> "teams" + :> "invitations" + :> "info" + :> QueryParam' '[Required, Strict, Description "Invitation code"] "code" InvitationCode + :> MultiVerb1 + 'GET + '[JSON] + (Respond 200 "Invitation info" Invitation) + ) + -- FUTUREWORK: Add another endpoint to allow resending of invitation codes + :<|> Named + "head-team-invitations" + ( Summary "Check if there is an invitation pending given an email address." + :> "teams" + :> "invitations" + :> "by-email" + :> QueryParam' '[Required, Strict, Description "Email address"] "email" Email + :> MultiVerb + 'HEAD + '[JSON] + HeadInvitationsResponses + HeadInvitationByEmailResult + ) + :<|> Named + "get-team-size" + ( Summary + "Returns the number of team members as an integer. \ + \Can be out of sync by roughly the `refresh_interval` \ + \of the ES index." + :> CanThrow 'InvalidInvitationCode + :> ZUser + :> "teams" + :> Capture "tid" TeamId + :> "size" + :> MultiVerb1 + 'GET + '[JSON] + (Respond 200 "Number of team members" TeamSize) + ) 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 ac000d4dff..a1d786c15a 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs @@ -20,149 +20,21 @@ module Wire.API.Routes.Public.Galley where -import qualified Data.Code as Code -import Data.CommaSeparatedList -import Data.Domain (Domain) -import Data.Id (ConvId, TeamId, UserId) -import Data.Qualified (Qualified (..)) -import Data.Range import Data.SOP import qualified Data.Swagger as Swagger -import GHC.TypeLits (AppendSymbol) -import qualified Generics.SOP as GSOP -import Imports hiding (head) import Servant hiding (WithStatus) import Servant.Swagger.Internal import Servant.Swagger.Internal.Orphans () -import Wire.API.Conversation -import Wire.API.Conversation.Role -import Wire.API.CustomBackend (CustomBackend) -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 -import Wire.API.Message -import Wire.API.Routes.CSV -import Wire.API.Routes.LowLevelStream -import Wire.API.Routes.MultiVerb -import Wire.API.Routes.Named -import Wire.API.Routes.Public -import Wire.API.Routes.Public.Util -import Wire.API.Routes.QualifiedCapture -import Wire.API.Routes.Version -import Wire.API.ServantProto (Proto, RawProto) -import Wire.API.Team -import Wire.API.Team.Conversation -import Wire.API.Team.Feature -import Wire.API.Team.LegalHold -import Wire.API.Team.Member -import Wire.API.Team.Permission (Perm (..)) -import Wire.API.Team.SearchVisibility (TeamSearchVisibilityView) -import qualified Wire.API.User as User - -instance AsHeaders '[ConvId] Conversation Conversation where - toHeaders c = (I (qUnqualified (cnvQualifiedId c)) :* Nil, c) - fromHeaders = snd - -type ConversationResponse = ResponseForExistedCreated Conversation - -type ConversationHeaders = '[DescHeader "Location" "Conversation ID" ConvId] - -type ConversationVerb = - MultiVerb - 'POST - '[JSON] - '[ WithHeaders - ConversationHeaders - Conversation - (Respond 200 "Conversation existed" Conversation), - WithHeaders - ConversationHeaders - Conversation - (Respond 201 "Conversation created" Conversation) - ] - ConversationResponse - -type CreateConversationCodeVerb = - MultiVerb - 'POST - '[JSON] - '[ Respond 200 "Conversation code already exists." ConversationCode, - Respond 201 "Conversation code created." Event - ] - AddCodeResult - -instance - (ResponseType r1 ~ ConversationCode, ResponseType r2 ~ Event) => - AsUnion '[r1, r2] AddCodeResult - where - toUnion (CodeAlreadyExisted c) = Z (I c) - toUnion (CodeAdded e) = S (Z (I e)) - - fromUnion (Z (I c)) = CodeAlreadyExisted c - fromUnion (S (Z (I e))) = CodeAdded e - fromUnion (S (S x)) = case x of {} - -type ConvUpdateResponses = UpdateResponses "Conversation unchanged" "Conversation updated" Event - -type ConvJoinResponses = UpdateResponses "Conversation unchanged" "Conversation joined" Event - -data MessageNotSent a - = MessageNotSentConversationNotFound - | MessageNotSentUnknownClient - | MessageNotSentLegalhold - | MessageNotSentClientMissing a - deriving stock (Eq, Show, Generic, Functor) - deriving - (AsUnion (MessageNotSentResponses a)) - via (GenericAsUnion (MessageNotSentResponses a) (MessageNotSent a)) - -instance GSOP.Generic (MessageNotSent a) - -type RemoveFromConversationVerb = - MultiVerb - 'DELETE - '[JSON] - '[ RespondEmpty 204 "No change", - Respond 200 "Member removed" Event - ] - (Maybe Event) - -type MessageNotSentResponses a = - '[ ErrorResponse 'ConvNotFound, - ErrorResponse 'BrigError.UnknownClient, - ErrorResponse 'BrigError.MissingLegalholdConsent, - Respond 412 "Missing clients" a - ] - -type PostOtrResponses a = - MessageNotSentResponses a - .++ '[Respond 201 "Message sent" a] - -type PostOtrResponse a = Either (MessageNotSent a) a - -instance - ( rs ~ (MessageNotSentResponses a .++ '[r]), - a ~ ResponseType r - ) => - AsUnion rs (PostOtrResponse a) - where - toUnion = - eitherToUnion - (toUnion @(MessageNotSentResponses a)) - (Z . I) - - fromUnion = - eitherFromUnion - (fromUnion @(MessageNotSentResponses a)) - (unI . unZ) +import Wire.API.Routes.Public.Galley.Bot +import Wire.API.Routes.Public.Galley.Conversation +import Wire.API.Routes.Public.Galley.CustomBackend +import Wire.API.Routes.Public.Galley.Feature +import Wire.API.Routes.Public.Galley.LegalHold +import Wire.API.Routes.Public.Galley.MLS +import Wire.API.Routes.Public.Galley.Messaging +import Wire.API.Routes.Public.Galley.Team +import Wire.API.Routes.Public.Galley.TeamConversation +import Wire.API.Routes.Public.Galley.TeamMember type ServantAPI = ConversationAPI @@ -176,1710 +48,5 @@ type ServantAPI = :<|> LegalHoldAPI :<|> TeamMemberAPI -type ConversationAPI = - Named - "get-unqualified-conversation" - ( Summary "Get a conversation by ID" - :> CanThrow 'ConvNotFound - :> CanThrow 'ConvAccessDenied - :> ZLocalUser - :> "conversations" - :> 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)" - :> Until 'V2 - :> CanThrow 'ConvNotFound - :> CanThrow 'ConvAccessDenied - :> ZLocalUser - :> "legalhold" - :> "conversations" - :> Capture "cnv" ConvId - :> Get '[Servant.JSON] Conversation - ) - :<|> Named - "get-conversation" - ( Summary "Get a conversation by ID" - :> CanThrow 'ConvNotFound - :> CanThrow 'ConvAccessDenied - :> ZLocalUser - :> "conversations" - :> QualifiedCapture "cnv" ConvId - :> Get '[Servant.JSON] Conversation - ) - :<|> Named - "get-conversation-roles" - ( Summary "Get existing roles available for the given conversation" - :> CanThrow 'ConvNotFound - :> CanThrow 'ConvAccessDenied - :> ZLocalUser - :> "conversations" - :> Capture "cnv" ConvId - :> "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." - -- FUTUREWORK: add bounds to swagger schema for Range - :> ZLocalUser - :> "conversations" - :> "ids" - :> QueryParam' - [ Optional, - Strict, - Description "Conversation ID to start from (exclusive)" - ] - "start" - ConvId - :> QueryParam' - [ Optional, - Strict, - Description "Maximum number of IDs to return" - ] - "size" - (Range 1 1000 Int32) - :> Get '[Servant.JSON] (ConversationList ConvId) - ) - :<|> Named - "list-conversation-ids" - ( Summary "Get all conversation IDs." - :> Description PaginationDocs - :> ZLocalUser - :> "conversations" - :> "list-ids" - :> ReqBody '[Servant.JSON] GetPaginatedConversationIds - :> Post '[Servant.JSON] ConvIdsPage - ) - :<|> Named - "get-conversations" - ( Summary "Get all *local* conversations." - :> Description - "Will not return remote conversations.\n\n\ - \Use `POST /conversations/list-ids` followed by \ - \`POST /conversations/list` instead." - :> ZLocalUser - :> "conversations" - :> QueryParam' - [ Optional, - Strict, - Description "Mutually exclusive with 'start' (at most 32 IDs per request)" - ] - "ids" - (Range 1 32 (CommaSeparatedList ConvId)) - :> QueryParam' - [ Optional, - Strict, - Description "Conversation ID to start from (exclusive)" - ] - "start" - ConvId - :> QueryParam' - [ Optional, - Strict, - Description "Maximum number of conversations to return" - ] - "size" - (Range 1 500 Int32) - :> Get '[Servant.JSON] (ConversationList Conversation) - ) - :<|> Named - "list-conversations-v1" - ( Summary "Get conversation metadata for a list of conversation ids" - :> Until 'V2 - :> ZLocalUser - :> "conversations" - :> "list" - :> "v2" - :> ReqBody '[Servant.JSON] ListConversations - :> Post '[Servant.JSON] ConversationsResponse - ) - :<|> Named - "list-conversations" - ( Summary "Get conversation metadata for a list of conversation ids" - :> From 'V2 - :> ZLocalUser - :> "conversations" - :> "list" - :> ReqBody '[Servant.JSON] ListConversations - :> Post '[Servant.JSON] ConversationsResponse - ) - -- This endpoint can lead to the following events being sent: - -- - ConvCreate event to members - :<|> Named - "get-conversation-by-reusable-code" - ( Summary "Get limited conversation information by key/code pair" - :> CanThrow 'CodeNotFound - :> CanThrow 'ConvNotFound - :> CanThrow 'ConvAccessDenied - :> CanThrow 'GuestLinksDisabled - :> CanThrow 'NotATeamMember - :> ZLocalUser - :> "conversations" - :> "join" - :> QueryParam' [Required, Strict] "key" Code.Key - :> QueryParam' [Required, Strict] "code" Code.Value - :> Get '[Servant.JSON] ConversationCoverView - ) - :<|> Named - "create-group-conversation" - ( Summary "Create a new conversation" - :> CanThrow 'ConvAccessDenied - :> CanThrow 'MLSNonEmptyMemberList - :> CanThrow 'NotConnected - :> CanThrow 'NotATeamMember - :> CanThrow OperationDenied - :> CanThrow 'MissingLegalholdConsent - :> Description "This returns 201 when a new conversation is created, and 200 when the conversation already existed" - :> ZLocalUser - :> ZConn - :> "conversations" - :> ReqBody '[Servant.JSON] NewConv - :> ConversationVerb - ) - :<|> Named - "create-self-conversation" - ( Summary "Create a self-conversation" - :> ZLocalUser - :> "conversations" - :> "self" - :> ConversationVerb - ) - -- This endpoint can lead to the following events being sent: - -- - ConvCreate event to members - -- TODO: add note: "On 201, the conversation ID is the `Location` header" - :<|> Named - "create-one-to-one-conversation" - ( Summary "Create a 1:1 conversation" - :> CanThrow 'ConvAccessDenied - :> CanThrow 'InvalidOperation - :> CanThrow 'NoBindingTeamMembers - :> CanThrow 'NonBindingTeam - :> CanThrow 'NotATeamMember - :> CanThrow 'NotConnected - :> CanThrow OperationDenied - :> CanThrow 'TeamNotFound - :> CanThrow 'MissingLegalholdConsent - :> ZLocalUser - :> ZConn - :> "conversations" - :> "one2one" - :> ReqBody '[Servant.JSON] NewConv - :> ConversationVerb - ) - -- This endpoint can lead to the following events being sent: - -- - MemberJoin event to members - :<|> Named - "add-members-to-conversation-unqualified" - ( Summary "Add members to an existing conversation (deprecated)" - :> Until 'V2 - :> CanThrow ('ActionDenied 'AddConversationMember) - :> CanThrow ('ActionDenied 'LeaveConversation) - :> CanThrow 'ConvNotFound - :> CanThrow 'InvalidOperation - :> CanThrow 'TooManyMembers - :> CanThrow 'ConvAccessDenied - :> CanThrow 'NotATeamMember - :> CanThrow 'NotConnected - :> CanThrow 'MissingLegalholdConsent - :> ZLocalUser - :> ZConn - :> "conversations" - :> Capture "cnv" ConvId - :> "members" - :> ReqBody '[JSON] Invite - :> MultiVerb 'POST '[JSON] ConvUpdateResponses (UpdateResult Event) - ) - :<|> Named - "add-members-to-conversation-unqualified2" - ( Summary "Add qualified members to an existing conversation." - :> Until 'V2 - :> CanThrow ('ActionDenied 'AddConversationMember) - :> CanThrow ('ActionDenied 'LeaveConversation) - :> CanThrow 'ConvNotFound - :> CanThrow 'InvalidOperation - :> CanThrow 'TooManyMembers - :> CanThrow 'ConvAccessDenied - :> CanThrow 'NotATeamMember - :> CanThrow 'NotConnected - :> CanThrow 'MissingLegalholdConsent - :> ZLocalUser - :> ZConn - :> "conversations" - :> Capture "cnv" ConvId - :> "members" - :> "v2" - :> ReqBody '[Servant.JSON] InviteQualified - :> MultiVerb 'POST '[Servant.JSON] ConvUpdateResponses (UpdateResult Event) - ) - :<|> Named - "add-members-to-conversation" - ( Summary "Add qualified members to an existing conversation." - :> From 'V2 - :> CanThrow ('ActionDenied 'AddConversationMember) - :> CanThrow ('ActionDenied 'LeaveConversation) - :> CanThrow 'ConvNotFound - :> CanThrow 'InvalidOperation - :> CanThrow 'TooManyMembers - :> CanThrow 'ConvAccessDenied - :> CanThrow 'NotATeamMember - :> CanThrow 'NotConnected - :> CanThrow 'MissingLegalholdConsent - :> ZLocalUser - :> ZConn - :> "conversations" - :> QualifiedCapture "cnv" ConvId - :> "members" - :> ReqBody '[Servant.JSON] InviteQualified - :> MultiVerb 'POST '[Servant.JSON] ConvUpdateResponses (UpdateResult Event) - ) - -- This endpoint can lead to the following events being sent: - -- - MemberJoin event to members - :<|> Named - "join-conversation-by-id-unqualified" - ( Summary "Join a conversation by its ID (if link access enabled)" - :> CanThrow 'ConvAccessDenied - :> CanThrow 'ConvNotFound - :> CanThrow 'InvalidOperation - :> CanThrow 'NotATeamMember - :> CanThrow 'TooManyMembers - :> ZLocalUser - :> ZConn - :> "conversations" - :> Capture' '[Description "Conversation ID"] "cnv" ConvId - :> "join" - :> MultiVerb 'POST '[Servant.JSON] ConvJoinResponses (UpdateResult Event) - ) - -- This endpoint can lead to the following events being sent: - -- - MemberJoin event to members - :<|> Named - "join-conversation-by-code-unqualified" - ( Summary - "Join a conversation using a reusable code.\ - \If the guest links team feature is disabled, this will fail with 409 GuestLinksDisabled.\ - \Note that this is currently inconsistent (for backwards compatibility reasons) with `POST /conversations/code-check` which responds with 404 CodeNotFound if guest links are disabled." - :> CanThrow 'CodeNotFound - :> CanThrow 'ConvAccessDenied - :> CanThrow 'ConvNotFound - :> CanThrow 'GuestLinksDisabled - :> CanThrow 'InvalidOperation - :> CanThrow 'NotATeamMember - :> CanThrow 'TooManyMembers - :> ZLocalUser - :> ZConn - :> "conversations" - :> "join" - :> ReqBody '[Servant.JSON] ConversationCode - :> MultiVerb 'POST '[Servant.JSON] ConvJoinResponses (UpdateResult Event) - ) - :<|> Named - "code-check" - ( Summary - "Check validity of a conversation code.\ - \If the guest links team feature is disabled, this will fail with 404 CodeNotFound.\ - \Note that this is currently inconsistent (for backwards compatibility reasons) with `POST /conversations/join` which responds with 409 GuestLinksDisabled if guest links are disabled." - :> CanThrow 'CodeNotFound - :> CanThrow 'ConvNotFound - :> "conversations" - :> "code-check" - :> ReqBody '[Servant.JSON] ConversationCode - :> MultiVerb - 'POST - '[JSON] - '[RespondEmpty 200 "Valid"] - () - ) - -- this endpoint can lead to the following events being sent: - -- - ConvCodeUpdate event to members, if code didn't exist before - :<|> Named - "create-conversation-code-unqualified" - ( Summary "Create or recreate a conversation code" - :> CanThrow 'ConvAccessDenied - :> CanThrow 'ConvNotFound - :> CanThrow 'GuestLinksDisabled - :> ZUser - :> ZConn - :> "conversations" - :> Capture' '[Description "Conversation ID"] "cnv" ConvId - :> "code" - :> CreateConversationCodeVerb - ) - :<|> Named - "get-conversation-guest-links-status" - ( Summary "Get the status of the guest links feature for a conversation that potentially has been created by someone from another team." - :> CanThrow 'ConvAccessDenied - :> CanThrow 'ConvNotFound - :> ZUser - :> "conversations" - :> Capture' '[Description "Conversation ID"] "cnv" ConvId - :> "features" - :> FeatureSymbol GuestLinksConfig - :> Get '[Servant.JSON] (WithStatus GuestLinksConfig) - ) - -- This endpoint can lead to the following events being sent: - -- - ConvCodeDelete event to members - :<|> Named - "remove-code-unqualified" - ( Summary "Delete conversation code" - :> CanThrow 'ConvAccessDenied - :> CanThrow 'ConvNotFound - :> ZLocalUser - :> ZConn - :> "conversations" - :> Capture' '[Description "Conversation ID"] "cnv" ConvId - :> "code" - :> MultiVerb - 'DELETE - '[JSON] - '[Respond 200 "Conversation code deleted." Event] - Event - ) - :<|> Named - "get-code" - ( Summary "Get existing conversation code" - :> CanThrow 'CodeNotFound - :> CanThrow 'ConvAccessDenied - :> CanThrow 'ConvNotFound - :> CanThrow 'GuestLinksDisabled - :> ZLocalUser - :> "conversations" - :> Capture' '[Description "Conversation ID"] "cnv" ConvId - :> "code" - :> MultiVerb - 'GET - '[JSON] - '[Respond 200 "Conversation Code" ConversationCode] - ConversationCode - ) - -- This endpoint can lead to the following events being sent: - -- - Typing event to members - :<|> Named - "member-typing-unqualified" - ( Summary "Sending typing notifications" - :> CanThrow 'ConvNotFound - :> ZLocalUser - :> ZConn - :> "conversations" - :> Capture' '[Description "Conversation ID"] "cnv" ConvId - :> "typing" - :> ReqBody '[JSON] TypingData - :> MultiVerb 'POST '[JSON] '[RespondEmpty 200 "Notification sent"] () - ) - -- This endpoint can lead to the following events being sent: - -- - MemberLeave event to members - :<|> Named - "remove-member-unqualified" - ( Summary "Remove a member from a conversation (deprecated)" - :> Until 'V2 - :> ZLocalUser - :> ZConn - :> CanThrow ('ActionDenied 'RemoveConversationMember) - :> CanThrow 'ConvNotFound - :> CanThrow 'InvalidOperation - :> "conversations" - :> Capture' '[Description "Conversation ID"] "cnv" ConvId - :> "members" - :> Capture' '[Description "Target User ID"] "usr" UserId - :> RemoveFromConversationVerb - ) - -- This endpoint can lead to the following events being sent: - -- - MemberLeave event to members - :<|> Named - "remove-member" - ( Summary "Remove a member from a conversation" - :> ZLocalUser - :> ZConn - :> CanThrow ('ActionDenied 'RemoveConversationMember) - :> CanThrow 'ConvNotFound - :> CanThrow 'InvalidOperation - :> "conversations" - :> QualifiedCapture' '[Description "Conversation ID"] "cnv" ConvId - :> "members" - :> QualifiedCapture' '[Description "Target User ID"] "usr" UserId - :> RemoveFromConversationVerb - ) - -- This endpoint can lead to the following events being sent: - -- - MemberStateUpdate event to members - :<|> Named - "update-other-member-unqualified" - ( Summary "Update membership of the specified user (deprecated)" - :> Description "Use `PUT /conversations/:cnv_domain/:cnv/members/:usr_domain/:usr` instead" - :> ZLocalUser - :> ZConn - :> CanThrow 'ConvNotFound - :> CanThrow 'ConvMemberNotFound - :> CanThrow ('ActionDenied 'ModifyOtherConversationMember) - :> CanThrow 'InvalidTarget - :> CanThrow 'InvalidOperation - :> "conversations" - :> Capture' '[Description "Conversation ID"] "cnv" ConvId - :> "members" - :> Capture' '[Description "Target User ID"] "usr" UserId - :> ReqBody '[JSON] OtherMemberUpdate - :> MultiVerb - 'PUT - '[JSON] - '[RespondEmpty 200 "Membership updated"] - () - ) - :<|> Named - "update-other-member" - ( Summary "Update membership of the specified user" - :> Description "**Note**: at least one field has to be provided." - :> ZLocalUser - :> ZConn - :> CanThrow 'ConvNotFound - :> CanThrow 'ConvMemberNotFound - :> CanThrow ('ActionDenied 'ModifyOtherConversationMember) - :> CanThrow 'InvalidTarget - :> CanThrow 'InvalidOperation - :> "conversations" - :> QualifiedCapture' '[Description "Conversation ID"] "cnv" ConvId - :> "members" - :> QualifiedCapture' '[Description "Target User ID"] "usr" UserId - :> ReqBody '[JSON] OtherMemberUpdate - :> MultiVerb - 'PUT - '[JSON] - '[RespondEmpty 200 "Membership updated"] - () - ) - -- This endpoint can lead to the following events being sent: - -- - ConvRename event to members - :<|> Named - "update-conversation-name-deprecated" - ( Summary "Update conversation name (deprecated)" - :> Description "Use `/conversations/:domain/:conv/name` instead." - :> CanThrow ('ActionDenied 'ModifyConversationName) - :> CanThrow 'ConvNotFound - :> CanThrow 'InvalidOperation - :> ZLocalUser - :> ZConn - :> "conversations" - :> Capture' '[Description "Conversation ID"] "cnv" ConvId - :> ReqBody '[JSON] ConversationRename - :> MultiVerb - 'PUT - '[JSON] - (UpdateResponses "Name unchanged" "Name updated" Event) - (UpdateResult Event) - ) - :<|> Named - "update-conversation-name-unqualified" - ( Summary "Update conversation name (deprecated)" - :> Description "Use `/conversations/:domain/:conv/name` instead." - :> CanThrow ('ActionDenied 'ModifyConversationName) - :> CanThrow 'ConvNotFound - :> CanThrow 'InvalidOperation - :> ZLocalUser - :> ZConn - :> "conversations" - :> Capture' '[Description "Conversation ID"] "cnv" ConvId - :> "name" - :> ReqBody '[JSON] ConversationRename - :> MultiVerb - 'PUT - '[JSON] - (UpdateResponses "Name unchanged" "Name updated" Event) - (UpdateResult Event) - ) - :<|> Named - "update-conversation-name" - ( Summary "Update conversation name" - :> CanThrow ('ActionDenied 'ModifyConversationName) - :> CanThrow 'ConvNotFound - :> CanThrow 'InvalidOperation - :> ZLocalUser - :> ZConn - :> "conversations" - :> QualifiedCapture' '[Description "Conversation ID"] "cnv" ConvId - :> "name" - :> ReqBody '[JSON] ConversationRename - :> MultiVerb - 'PUT - '[JSON] - (UpdateResponses "Name updated" "Name unchanged" Event) - (UpdateResult Event) - ) - -- This endpoint can lead to the following events being sent: - -- - ConvMessageTimerUpdate event to members - :<|> Named - "update-conversation-message-timer-unqualified" - ( Summary "Update the message timer for a conversation (deprecated)" - :> Description "Use `/conversations/:domain/:cnv/message-timer` instead." - :> ZLocalUser - :> ZConn - :> CanThrow ('ActionDenied 'ModifyConversationMessageTimer) - :> CanThrow 'ConvAccessDenied - :> CanThrow 'ConvNotFound - :> CanThrow 'InvalidOperation - :> "conversations" - :> Capture' '[Description "Conversation ID"] "cnv" ConvId - :> "message-timer" - :> ReqBody '[JSON] ConversationMessageTimerUpdate - :> MultiVerb - 'PUT - '[JSON] - (UpdateResponses "Message timer unchanged" "Message timer updated" Event) - (UpdateResult Event) - ) - :<|> Named - "update-conversation-message-timer" - ( Summary "Update the message timer for a conversation" - :> ZLocalUser - :> ZConn - :> CanThrow ('ActionDenied 'ModifyConversationMessageTimer) - :> CanThrow 'ConvAccessDenied - :> CanThrow 'ConvNotFound - :> CanThrow 'InvalidOperation - :> "conversations" - :> QualifiedCapture' '[Description "Conversation ID"] "cnv" ConvId - :> "message-timer" - :> ReqBody '[JSON] ConversationMessageTimerUpdate - :> MultiVerb - 'PUT - '[JSON] - (UpdateResponses "Message timer unchanged" "Message timer updated" Event) - (UpdateResult Event) - ) - -- This endpoint can lead to the following events being sent: - -- - ConvReceiptModeUpdate event to members - :<|> Named - "update-conversation-receipt-mode-unqualified" - ( Summary "Update receipt mode for a conversation (deprecated)" - :> Description "Use `PUT /conversations/:domain/:cnv/receipt-mode` instead." - :> ZLocalUser - :> ZConn - :> CanThrow ('ActionDenied 'ModifyConversationReceiptMode) - :> CanThrow 'ConvAccessDenied - :> CanThrow 'ConvNotFound - :> CanThrow 'InvalidOperation - :> "conversations" - :> Capture' '[Description "Conversation ID"] "cnv" ConvId - :> "receipt-mode" - :> ReqBody '[JSON] ConversationReceiptModeUpdate - :> MultiVerb - 'PUT - '[JSON] - (UpdateResponses "Receipt mode unchanged" "Receipt mode updated" Event) - (UpdateResult Event) - ) - :<|> Named - "update-conversation-receipt-mode" - ( Summary "Update receipt mode for a conversation" - :> ZLocalUser - :> ZConn - :> CanThrow ('ActionDenied 'ModifyConversationReceiptMode) - :> CanThrow 'ConvAccessDenied - :> CanThrow 'ConvNotFound - :> CanThrow 'InvalidOperation - :> "conversations" - :> QualifiedCapture' '[Description "Conversation ID"] "cnv" ConvId - :> "receipt-mode" - :> ReqBody '[JSON] ConversationReceiptModeUpdate - :> MultiVerb - 'PUT - '[JSON] - (UpdateResponses "Receipt mode unchanged" "Receipt mode updated" Event) - (UpdateResult Event) - ) - -- This endpoint can lead to the following events being sent: - -- - MemberLeave event to members, if members get removed - -- - ConvAccessUpdate event to members - :<|> Named - "update-conversation-access-unqualified" - ( Summary "Update access modes for a conversation (deprecated)" - :> Description "Use PUT `/conversations/:domain/:cnv/access` instead." - :> ZLocalUser - :> ZConn - :> CanThrow ('ActionDenied 'ModifyConversationAccess) - :> CanThrow ('ActionDenied 'RemoveConversationMember) - :> CanThrow 'ConvAccessDenied - :> CanThrow 'ConvNotFound - :> CanThrow 'InvalidOperation - :> CanThrow 'InvalidTargetAccess - :> "conversations" - :> Capture' '[Description "Conversation ID"] "cnv" ConvId - :> "access" - :> ReqBody '[JSON] ConversationAccessData - :> MultiVerb - 'PUT - '[JSON] - (UpdateResponses "Access unchanged" "Access updated" Event) - (UpdateResult Event) - ) - :<|> Named - "update-conversation-access" - ( Summary "Update access modes for a conversation" - :> ZLocalUser - :> ZConn - :> CanThrow ('ActionDenied 'ModifyConversationAccess) - :> CanThrow ('ActionDenied 'RemoveConversationMember) - :> CanThrow 'ConvAccessDenied - :> CanThrow 'ConvNotFound - :> CanThrow 'InvalidOperation - :> CanThrow 'InvalidTargetAccess - :> "conversations" - :> QualifiedCapture' '[Description "Conversation ID"] "cnv" ConvId - :> "access" - :> ReqBody '[JSON] ConversationAccessData - :> MultiVerb - 'PUT - '[JSON] - (UpdateResponses "Access unchanged" "Access updated" Event) - (UpdateResult Event) - ) - :<|> Named - "get-conversation-self-unqualified" - ( Summary "Get self membership properties (deprecated)" - :> ZLocalUser - :> "conversations" - :> Capture' '[Description "Conversation ID"] "cnv" ConvId - :> "self" - :> Get '[JSON] (Maybe Member) - ) - :<|> Named - "update-conversation-self-unqualified" - ( Summary "Update self membership properties (deprecated)" - :> Description "Use `/conversations/:domain/:conv/self` instead." - :> CanThrow 'ConvNotFound - :> ZLocalUser - :> ZConn - :> "conversations" - :> Capture' '[Description "Conversation ID"] "cnv" ConvId - :> "self" - :> ReqBody '[JSON] MemberUpdate - :> MultiVerb - 'PUT - '[JSON] - '[RespondEmpty 200 "Update successful"] - () - ) - :<|> Named - "update-conversation-self" - ( Summary "Update self membership properties" - :> Description "**Note**: at least one field has to be provided." - :> CanThrow 'ConvNotFound - :> ZLocalUser - :> ZConn - :> "conversations" - :> QualifiedCapture' '[Description "Conversation ID"] "cnv" ConvId - :> "self" - :> ReqBody '[JSON] MemberUpdate - :> MultiVerb - 'PUT - '[JSON] - '[RespondEmpty 200 "Update successful"] - () - ) - -type TeamConversationAPI = - Named - "get-team-conversation-roles" - ( Summary "Get existing roles available for the given team" - :> CanThrow 'NotATeamMember - :> ZUser - :> "teams" - :> Capture "tid" TeamId - :> "conversations" - :> "roles" - :> Get '[Servant.JSON] ConversationRolesList - ) - :<|> Named - "get-team-conversations" - ( Summary "Get team conversations" - :> CanThrow OperationDenied - :> CanThrow 'NotATeamMember - :> ZUser - :> "teams" - :> Capture "tid" TeamId - :> "conversations" - :> Get '[Servant.JSON] TeamConversationList - ) - :<|> Named - "get-team-conversation" - ( Summary "Get one team conversation" - :> CanThrow 'ConvNotFound - :> CanThrow OperationDenied - :> CanThrow 'NotATeamMember - :> ZUser - :> "teams" - :> Capture "tid" TeamId - :> "conversations" - :> Capture "cid" ConvId - :> Get '[Servant.JSON] TeamConversation - ) - :<|> Named - "delete-team-conversation" - ( Summary "Remove a team conversation" - :> CanThrow ('ActionDenied 'DeleteConversation) - :> CanThrow 'ConvNotFound - :> CanThrow 'InvalidOperation - :> CanThrow 'NotATeamMember - :> ZLocalUser - :> ZConn - :> "teams" - :> Capture "tid" TeamId - :> "conversations" - :> Capture "cid" ConvId - :> MultiVerb 'DELETE '[JSON] '[RespondEmpty 200 "Conversation deleted"] () - ) - -type TeamAPI = - Named - "create-non-binding-team" - ( Summary "Create a new non binding team" - -- FUTUREWORK: deprecated in https://github.com/wireapp/wire-server/pull/2607 - :> ZUser - :> ZConn - :> CanThrow 'NotConnected - :> CanThrow 'UserBindingExists - :> "teams" - :> ReqBody '[Servant.JSON] NonBindingNewTeam - :> MultiVerb - 'POST - '[JSON] - '[ WithHeaders - '[DescHeader "Location" "Team ID" TeamId] - TeamId - (RespondEmpty 201 "Team ID as `Location` header value") - ] - TeamId - ) - :<|> Named - "update-team" - ( Summary "Update team properties" - :> ZUser - :> ZConn - :> CanThrow 'NotATeamMember - :> CanThrow ('MissingPermission ('Just 'SetTeamData)) - :> "teams" - :> Capture "tid" TeamId - :> ReqBody '[JSON] TeamUpdateData - :> MultiVerb - 'PUT - '[JSON] - '[RespondEmpty 200 "Team updated"] - () - ) - :<|> Named - "get-teams" - ( Summary "Get teams (deprecated); use `GET /teams/:tid`" - -- FUTUREWORK: deprecated in https://github.com/wireapp/wire-server/pull/2607 - :> ZUser - :> "teams" - :> Get '[JSON] TeamList - ) - :<|> Named - "get-team" - ( Summary "Get a team by ID" - :> ZUser - :> CanThrow 'TeamNotFound - :> "teams" - :> Capture "tid" TeamId - :> Get '[JSON] Team - ) - :<|> Named - "delete-team" - ( Summary "Delete a team" - :> ZUser - :> ZConn - :> CanThrow 'TeamNotFound - :> CanThrow ('MissingPermission ('Just 'DeleteTeam)) - :> CanThrow 'NotATeamMember - :> CanThrow OperationDenied - :> CanThrow 'DeleteQueueFull - :> CanThrow AuthenticationError - :> "teams" - :> Capture "tid" TeamId - :> ReqBody '[Servant.JSON] TeamDeleteData - :> MultiVerb 'DELETE '[JSON] '[RespondEmpty 202 "Team is scheduled for removal"] () - ) - -type MessagingAPI = - Named - "post-otr-message-unqualified" - ( Summary "Post an encrypted message to a conversation (accepts JSON or Protobuf)" - :> Description PostOtrDescriptionUnqualified - :> ZLocalUser - :> ZConn - :> "conversations" - :> Capture "cnv" ConvId - :> "otr" - :> "messages" - :> QueryParam "ignore_missing" IgnoreMissing - :> QueryParam "report_missing" ReportMissing - :> ReqBody '[JSON, Proto] NewOtrMessage - :> MultiVerb - 'POST - '[Servant.JSON] - (PostOtrResponses ClientMismatch) - (PostOtrResponse ClientMismatch) - ) - :<|> Named - "post-otr-broadcast-unqualified" - ( Summary "Broadcast an encrypted message to all team members and all contacts (accepts JSON or Protobuf)" - :> Description PostOtrDescriptionUnqualified - :> ZLocalUser - :> ZConn - :> CanThrow 'TeamNotFound - :> CanThrow 'BroadcastLimitExceeded - :> CanThrow 'NonBindingTeam - :> "broadcast" - :> "otr" - :> "messages" - :> QueryParam "ignore_missing" IgnoreMissing - :> QueryParam "report_missing" ReportMissing - :> ReqBody '[JSON, Proto] NewOtrMessage - :> MultiVerb - 'POST - '[JSON] - (PostOtrResponses ClientMismatch) - (PostOtrResponse ClientMismatch) - ) - :<|> Named - "post-proteus-message" - ( Summary "Post an encrypted message to a conversation (accepts only Protobuf)" - :> Description PostOtrDescription - :> ZLocalUser - :> ZConn - :> "conversations" - :> QualifiedCapture "cnv" ConvId - :> "proteus" - :> "messages" - :> ReqBody '[Proto] (RawProto QualifiedNewOtrMessage) - :> MultiVerb - 'POST - '[Servant.JSON] - (PostOtrResponses MessageSendingStatus) - (Either (MessageNotSent MessageSendingStatus) MessageSendingStatus) - ) - :<|> Named - "post-proteus-broadcast" - ( Summary "Post an encrypted message to all team members and all contacts (accepts only Protobuf)" - :> Description PostOtrDescription - :> ZLocalUser - :> ZConn - :> CanThrow 'TeamNotFound - :> CanThrow 'BroadcastLimitExceeded - :> CanThrow 'NonBindingTeam - :> "broadcast" - :> "proteus" - :> "messages" - :> ReqBody '[Proto] QualifiedNewOtrMessage - :> MultiVerb - 'POST - '[JSON] - (PostOtrResponses MessageSendingStatus) - (Either (MessageNotSent MessageSendingStatus) MessageSendingStatus) - ) - -type BotAPI = - Named - "post-bot-message-unqualified" - ( ZBot - :> ZConversation - :> CanThrow 'ConvNotFound - :> "bot" - :> "messages" - :> QueryParam "ignore_missing" IgnoreMissing - :> QueryParam "report_missing" ReportMissing - :> ReqBody '[JSON] NewOtrMessage - :> MultiVerb - 'POST - '[Servant.JSON] - (PostOtrResponses ClientMismatch) - (PostOtrResponse ClientMismatch) - ) - -type FeatureAPI = - FeatureStatusGet SSOConfig - :<|> FeatureStatusGet LegalholdConfig - :<|> FeatureStatusPut - '( 'ActionDenied 'RemoveConversationMember, - '( AuthenticationError, - '( 'CannotEnableLegalHoldServiceLargeTeam, - '( 'LegalHoldNotEnabled, - '( 'LegalHoldDisableUnimplemented, - '( 'LegalHoldServiceNotRegistered, - '( 'UserLegalHoldIllegalOperation, - '( 'LegalHoldCouldNotBlockConnections, '()) - ) - ) - ) - ) - ) - ) - ) - LegalholdConfig - :<|> FeatureStatusGet SearchVisibilityAvailableConfig - :<|> FeatureStatusPut '() SearchVisibilityAvailableConfig - :<|> FeatureStatusDeprecatedGet "This endpoint is potentially used by the old Android client. It is not used by iOS, team management, or webapp as of June 2022" SearchVisibilityAvailableConfig - :<|> FeatureStatusDeprecatedPut "This endpoint is potentially used by the old Android client. It is not used by iOS, team management, or webapp as of June 2022" SearchVisibilityAvailableConfig - :<|> SearchVisibilityGet - :<|> SearchVisibilitySet - :<|> FeatureStatusGet ValidateSAMLEmailsConfig - :<|> FeatureStatusDeprecatedGet "This endpoint is potentially used by the old Android client. It is not used by iOS, team management, or webapp as of June 2022" ValidateSAMLEmailsConfig - :<|> FeatureStatusGet DigitalSignaturesConfig - :<|> FeatureStatusDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is potentially used by the old Android client. It is not used by team management, or webapp as of June 2022" DigitalSignaturesConfig - :<|> FeatureStatusGet AppLockConfig - :<|> FeatureStatusPut '() AppLockConfig - :<|> FeatureStatusGet FileSharingConfig - :<|> FeatureStatusPut '() FileSharingConfig - :<|> FeatureStatusGet ClassifiedDomainsConfig - :<|> FeatureStatusGet ConferenceCallingConfig - :<|> FeatureStatusGet SelfDeletingMessagesConfig - :<|> FeatureStatusPut '() SelfDeletingMessagesConfig - :<|> FeatureStatusGet GuestLinksConfig - :<|> FeatureStatusPut '() GuestLinksConfig - :<|> FeatureStatusGet SndFactorPasswordChallengeConfig - :<|> FeatureStatusPut '() SndFactorPasswordChallengeConfig - :<|> FeatureStatusGet MLSConfig - :<|> FeatureStatusPut '() MLSConfig - :<|> FeatureStatusGet ExposeInvitationURLsToTeamAdminConfig - :<|> FeatureStatusPut '() ExposeInvitationURLsToTeamAdminConfig - :<|> FeatureStatusGet SearchVisibilityInboundConfig - :<|> FeatureStatusPut '() SearchVisibilityInboundConfig - :<|> AllFeatureConfigsUserGet - :<|> AllFeatureConfigsTeamGet - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" LegalholdConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" SSOConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" SearchVisibilityAvailableConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" ValidateSAMLEmailsConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" DigitalSignaturesConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" AppLockConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" FileSharingConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" ClassifiedDomainsConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" ConferenceCallingConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" SelfDeletingMessagesConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" GuestLinksConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" SndFactorPasswordChallengeConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" MLSConfig - -type FeatureStatusGet f = - Named - '("get", f) - (ZUser :> FeatureStatusBaseGet f) - -type FeatureStatusPut errs f = - Named - '("put", f) - (ZUser :> FeatureStatusBasePutPublic errs f) - -type FeatureStatusDeprecatedGet d f = - Named - '("get-deprecated", f) - (ZUser :> FeatureStatusBaseDeprecatedGet d f) - -type FeatureStatusDeprecatedPut d f = - Named - '("put-deprecated", f) - (ZUser :> FeatureStatusBaseDeprecatedPut d f) - -type FeatureStatusBaseGet featureConfig = - Summary (AppendSymbol "Get config for " (FeatureSymbol featureConfig)) - :> CanThrow OperationDenied - :> CanThrow 'NotATeamMember - :> CanThrow 'TeamNotFound - :> "teams" - :> Capture "tid" TeamId - :> "features" - :> FeatureSymbol featureConfig - :> Get '[Servant.JSON] (WithStatus featureConfig) - -type FeatureStatusBasePutPublic errs featureConfig = - Summary (AppendSymbol "Put config for " (FeatureSymbol featureConfig)) - :> CanThrow OperationDenied - :> CanThrow 'NotATeamMember - :> CanThrow 'TeamNotFound - :> CanThrow TeamFeatureError - :> CanThrowMany errs - :> "teams" - :> Capture "tid" TeamId - :> "features" - :> FeatureSymbol featureConfig - :> ReqBody '[Servant.JSON] (WithStatusNoLock featureConfig) - :> Put '[Servant.JSON] (WithStatus featureConfig) - --- | A type for a GET endpoint for a feature with a deprecated path -type FeatureStatusBaseDeprecatedGet desc featureConfig = - ( Summary - (AppendSymbol "[deprecated] Get config for " (FeatureSymbol featureConfig)) - :> Until 'V2 - :> Description - ( "Deprecated. Please use `GET /teams/:tid/features/" - `AppendSymbol` FeatureSymbol featureConfig - `AppendSymbol` "` instead.\n" - `AppendSymbol` desc - ) - :> CanThrow 'NotATeamMember - :> CanThrow OperationDenied - :> CanThrow 'TeamNotFound - :> "teams" - :> Capture "tid" TeamId - :> "features" - :> DeprecatedFeatureName featureConfig - :> Get '[Servant.JSON] (WithStatus featureConfig) - ) - --- | A type for a PUT endpoint for a feature with a deprecated path -type FeatureStatusBaseDeprecatedPut desc featureConfig = - Summary - (AppendSymbol "[deprecated] Get config for " (FeatureSymbol featureConfig)) - :> Until 'V2 - :> Description - ( "Deprecated. Please use `PUT /teams/:tid/features/" - `AppendSymbol` FeatureSymbol featureConfig - `AppendSymbol` "` instead.\n" - `AppendSymbol` desc - ) - :> CanThrow 'NotATeamMember - :> CanThrow OperationDenied - :> CanThrow 'TeamNotFound - :> CanThrow TeamFeatureError - :> "teams" - :> Capture "tid" TeamId - :> "features" - :> DeprecatedFeatureName featureConfig - :> ReqBody '[Servant.JSON] (WithStatusNoLock featureConfig) - :> Put '[Servant.JSON] (WithStatus featureConfig) - -type FeatureConfigDeprecatedGet desc featureConfig = - Named - '("get-config", featureConfig) - ( Summary (AppendSymbol "[deprecated] Get feature config for feature " (FeatureSymbol featureConfig)) - :> Until 'V2 - :> Description ("Deprecated. Please use `GET /feature-configs` instead.\n" `AppendSymbol` desc) - :> ZUser - :> CanThrow 'NotATeamMember - :> CanThrow OperationDenied - :> CanThrow 'TeamNotFound - :> "feature-configs" - :> FeatureSymbol featureConfig - :> Get '[Servant.JSON] (WithStatus featureConfig) - ) - -type AllFeatureConfigsUserGet = - Named - "get-all-feature-configs-for-user" - ( Summary - "Gets feature configs for a user" - :> Description - "Gets feature configs for a user. If the user is a member of a team and has the required permissions, this will return the team's feature configs.\ - \If the user is not a member of a team, this will return the personal feature configs (the server defaults)." - :> ZUser - :> CanThrow 'NotATeamMember - :> CanThrow OperationDenied - :> CanThrow 'TeamNotFound - :> "feature-configs" - :> Get '[Servant.JSON] AllFeatureConfigs - ) - -type AllFeatureConfigsTeamGet = - Named - "get-all-feature-configs-for-team" - ( Summary "Gets feature configs for a team" - :> Description "Gets feature configs for a team. User must be a member of the team and have permission to view team features." - :> CanThrow 'NotATeamMember - :> CanThrow OperationDenied - :> CanThrow 'TeamNotFound - :> ZLocalUser - :> "teams" - :> Capture "tid" TeamId - :> "features" - :> Get '[JSON] AllFeatureConfigs - ) - -type SearchVisibilityGet = - Named - "get-search-visibility" - ( Summary "Shows the value for search visibility" - :> CanThrow 'NotATeamMember - :> CanThrow OperationDenied - :> ZLocalUser - :> "teams" - :> Capture "tid" TeamId - :> "search-visibility" - :> Get '[JSON] TeamSearchVisibilityView - ) - -type SearchVisibilitySet = - Named - "set-search-visibility" - ( Summary "Sets the search visibility for the whole team" - :> CanThrow 'NotATeamMember - :> CanThrow OperationDenied - :> CanThrow 'TeamSearchVisibilityNotEnabled - :> CanThrow 'TeamNotFound - :> CanThrow TeamFeatureError - :> ZLocalUser - :> "teams" - :> Capture "tid" TeamId - :> "search-visibility" - :> ReqBody '[JSON] TeamSearchVisibilityView - :> MultiVerb 'PUT '[JSON] '[RespondEmpty 204 "Search visibility set"] () - ) - -type MLSMessagingAPI = - Named - "mls-welcome-message" - ( Summary "Post an MLS welcome message" - :> CanThrow 'MLSKeyPackageRefNotFound - :> "welcome" - :> ZConn - :> ReqBody '[MLS] (RawMLS Welcome) - :> MultiVerb1 'POST '[JSON] (RespondEmpty 201 "Welcome message sent") - ) - :<|> Named - "mls-message-v1" - ( Summary "Post an MLS message" - :> Until '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 'MissingLegalholdConsent - :> CanThrow MLSProposalFailure - :> "messages" - :> ZConn - :> ReqBody '[MLS] (RawMLS SomeMessage) - :> MultiVerb1 'POST '[JSON] (Respond 201 "Message sent" [Event]) - ) - :<|> Named - "mls-message" - ( Summary "Post an MLS message" - :> 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 'MissingLegalholdConsent - :> CanThrow MLSProposalFailure - :> "messages" - :> ZConn - :> ReqBody '[MLS] (RawMLS SomeMessage) - :> MultiVerb1 'POST '[JSON] (Respond 201 "Message sent" MLSMessageSendingStatus) - ) - :<|> Named - "mls-commit-bundle" - ( Summary "Post a MLS CommitBundle" - :> From 'V3 - :> 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 '[CommitBundleMimeType] 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" - :> "public-keys" - :> MultiVerb1 'GET '[JSON] (Respond 200 "Public keys" MLSPublicKeys) - ) - -type MLSAPI = LiftNamed (ZLocalUser :> "mls" :> MLSMessagingAPI) - -type CustomBackendAPI = - Named - "get-custom-backend-by-domain" - ( Summary "Shows information about custom backends related to a given email domain" - :> CanThrow 'CustomBackendNotFound - :> "custom-backend" - :> "by-domain" - :> Capture' '[Description "URL-encoded email domain"] "domain" Domain - :> Get '[JSON] CustomBackend - ) - -type LegalHoldAPI = - Named - "create-legal-hold-settings" - ( Summary "Create legal hold service settings" - :> CanThrow 'NotATeamMember - :> CanThrow OperationDenied - :> CanThrow 'LegalHoldNotEnabled - :> CanThrow 'LegalHoldServiceInvalidKey - :> CanThrow 'LegalHoldServiceBadResponse - :> ZLocalUser - :> "teams" - :> Capture "tid" TeamId - :> "legalhold" - :> "settings" - :> ReqBody '[JSON] NewLegalHoldService - :> MultiVerb1 'POST '[JSON] (Respond 201 "Legal hold service settings created" ViewLegalHoldService) - ) - :<|> Named - "get-legal-hold-settings" - ( Summary "Get legal hold service settings" - :> CanThrow 'NotATeamMember - :> CanThrow OperationDenied - :> ZLocalUser - :> "teams" - :> Capture "tid" TeamId - :> "legalhold" - :> "settings" - :> Get '[JSON] ViewLegalHoldService - ) - :<|> Named - "delete-legal-hold-settings" - ( Summary "Delete legal hold service settings" - :> CanThrow AuthenticationError - :> CanThrow OperationDenied - :> CanThrow 'NotATeamMember - :> CanThrow ('ActionDenied 'RemoveConversationMember) - :> CanThrow 'InvalidOperation - :> CanThrow 'LegalHoldNotEnabled - :> CanThrow 'LegalHoldDisableUnimplemented - :> CanThrow 'LegalHoldServiceNotRegistered - :> CanThrow 'UserLegalHoldIllegalOperation - :> CanThrow 'LegalHoldCouldNotBlockConnections - :> Description - "This endpoint can lead to the following events being sent:\n\ - \- ClientRemoved event to members with a legalhold client (via brig)\n\ - \- UserLegalHoldDisabled event to contacts of members with a legalhold client (via brig)" - :> ZLocalUser - :> "teams" - :> Capture "tid" TeamId - :> "legalhold" - :> "settings" - :> ReqBody '[JSON] RemoveLegalHoldSettingsRequest - :> MultiVerb1 'DELETE '[JSON] (RespondEmpty 204 "Legal hold service settings deleted") - ) - :<|> Named - "get-legal-hold" - ( Summary "Get legal hold status" - :> CanThrow 'TeamMemberNotFound - :> ZLocalUser - :> "teams" - :> Capture "tid" TeamId - :> "legalhold" - :> Capture "uid" UserId - :> Get '[JSON] UserLegalHoldStatusResponse - ) - :<|> Named - "consent-to-legal-hold" - ( Summary "Consent to legal hold" - :> CanThrow ('ActionDenied 'RemoveConversationMember) - :> CanThrow 'InvalidOperation - :> CanThrow 'TeamMemberNotFound - :> CanThrow 'UserLegalHoldIllegalOperation - :> CanThrow 'LegalHoldCouldNotBlockConnections - :> ZLocalUser - :> "teams" - :> Capture "tid" TeamId - :> "legalhold" - :> "consent" - :> MultiVerb 'POST '[JSON] GrantConsentResultResponseTypes GrantConsentResult - ) - :<|> Named - "request-legal-hold-device" - ( Summary "Request legal hold device" - :> CanThrow ('ActionDenied 'RemoveConversationMember) - :> CanThrow 'NotATeamMember - :> CanThrow OperationDenied - :> CanThrow 'TeamMemberNotFound - :> CanThrow 'LegalHoldNotEnabled - :> CanThrow 'UserLegalHoldAlreadyEnabled - :> CanThrow 'NoUserLegalHoldConsent - :> CanThrow 'LegalHoldServiceBadResponse - :> CanThrow 'LegalHoldServiceNotRegistered - :> CanThrow 'LegalHoldCouldNotBlockConnections - :> CanThrow 'UserLegalHoldIllegalOperation - :> Description - "This endpoint can lead to the following events being sent:\n\ - \- LegalHoldClientRequested event to contacts of the user the device is requested for, if they didn't already have a legalhold client (via brig)" - :> ZLocalUser - :> "teams" - :> Capture "tid" TeamId - :> "legalhold" - :> Capture "uid" UserId - :> MultiVerb - 'POST - '[JSON] - RequestDeviceResultResponseType - RequestDeviceResult - ) - :<|> Named - "disable-legal-hold-for-user" - ( Summary "Disable legal hold for user" - :> CanThrow AuthenticationError - :> CanThrow ('ActionDenied 'RemoveConversationMember) - :> CanThrow 'NotATeamMember - :> CanThrow OperationDenied - :> CanThrow 'LegalHoldServiceNotRegistered - :> CanThrow 'UserLegalHoldIllegalOperation - :> CanThrow 'LegalHoldCouldNotBlockConnections - :> Description - "This endpoint can lead to the following events being sent:\n\ - \- ClientRemoved event to the user owning the client (via brig)\n\ - \- UserLegalHoldDisabled event to contacts of the user owning the client (via brig)" - :> ZLocalUser - :> "teams" - :> Capture "tid" TeamId - :> "legalhold" - :> Capture "uid" UserId - :> ReqBody '[JSON] DisableLegalHoldForUserRequest - :> MultiVerb - 'DELETE - '[JSON] - DisableLegalHoldForUserResponseType - DisableLegalHoldForUserResponse - ) - :<|> Named - "approve-legal-hold-device" - ( Summary "Approve legal hold device" - :> CanThrow AuthenticationError - :> CanThrow 'AccessDenied - :> CanThrow ('ActionDenied 'RemoveConversationMember) - :> CanThrow 'NotATeamMember - :> CanThrow 'LegalHoldNotEnabled - :> CanThrow 'UserLegalHoldNotPending - :> CanThrow 'NoLegalHoldDeviceAllocated - :> CanThrow 'LegalHoldServiceNotRegistered - :> CanThrow 'UserLegalHoldAlreadyEnabled - :> CanThrow 'UserLegalHoldIllegalOperation - :> CanThrow 'LegalHoldCouldNotBlockConnections - :> Description - "This endpoint can lead to the following events being sent:\n\ - \- ClientAdded event to the user owning the client (via brig)\n\ - \- UserLegalHoldEnabled event to contacts of the user owning the client (via brig)\n\ - \- ClientRemoved event to the user, if removing old client due to max number (via brig)" - :> ZLocalUser - :> ZConn - :> "teams" - :> Capture "tid" TeamId - :> "legalhold" - :> Capture "uid" UserId - :> "approve" - :> ReqBody '[JSON] ApproveLegalHoldForUserRequest - :> MultiVerb1 'PUT '[JSON] (RespondEmpty 200 "Legal hold approved") - ) - -type RequestDeviceResultResponseType = - '[ RespondEmpty 201 "Request device successful", - RespondEmpty 204 "Request device already pending" - ] - -data RequestDeviceResult - = RequestDeviceSuccess - | RequestDeviceAlreadyPending - deriving (Generic) - deriving (AsUnion RequestDeviceResultResponseType) via GenericAsUnion RequestDeviceResultResponseType RequestDeviceResult - -instance GSOP.Generic RequestDeviceResult - -type DisableLegalHoldForUserResponseType = - '[ RespondEmpty 200 "Disable legal hold successful", - RespondEmpty 204 "Legal hold was not enabled" - ] - -data DisableLegalHoldForUserResponse - = DisableLegalHoldSuccess - | DisableLegalHoldWasNotEnabled - deriving (Generic) - deriving (AsUnion DisableLegalHoldForUserResponseType) via GenericAsUnion DisableLegalHoldForUserResponseType DisableLegalHoldForUserResponse - -instance GSOP.Generic DisableLegalHoldForUserResponse - -type GrantConsentResultResponseTypes = - '[ RespondEmpty 201 "Grant consent successful", - RespondEmpty 204 "Consent already granted" - ] - -data GrantConsentResult - = GrantConsentSuccess - | GrantConsentAlreadyGranted - deriving (Generic) - deriving (AsUnion GrantConsentResultResponseTypes) via GenericAsUnion GrantConsentResultResponseTypes GrantConsentResult - -instance GSOP.Generic GrantConsentResult - -type TeamMemberAPI = - Named - "get-team-members" - ( Summary "Get team members" - :> CanThrow 'NotATeamMember - :> ZLocalUser - :> "teams" - :> Capture "tid" TeamId - :> "members" - :> QueryParam' - [ Optional, - Strict, - Description "Maximum results to be returned" - ] - "maxResults" - (Range 1 HardTruncationLimit Int32) - :> QueryParam' - [ Optional, - Strict, - Description - "Optional, when not specified, the first page will be returned.\ - \Every returned page contains a `pagingState`, this should be supplied to retrieve the next page." - ] - "pagingState" - TeamMembersPagingState - :> Get '[JSON] TeamMembersPage - ) - :<|> Named - "get-team-member" - ( Summary "Get single team member" - :> CanThrow 'NotATeamMember - :> CanThrow 'TeamMemberNotFound - :> ZLocalUser - :> "teams" - :> Capture "tid" TeamId - :> "members" - :> Capture "uid" UserId - :> Get '[JSON] TeamMemberOptPerms - ) - :<|> Named - "get-team-members-by-ids" - ( Summary "Get team members by user id list" - :> Description "The `has_more` field in the response body is always `false`." - :> CanThrow 'NotATeamMember - :> CanThrow 'BulkGetMemberLimitExceeded - :> ZLocalUser - :> "teams" - :> Capture "tid" TeamId - :> "get-members-by-ids-using-post" - :> QueryParam' - [ Optional, - Strict, - Description "Maximum results to be returned" - ] - "maxResults" - (Range 1 HardTruncationLimit Int32) - :> ReqBody '[JSON] User.UserIdList - :> Post '[JSON] TeamMemberListOptPerms - ) - :<|> Named - "add-team-member" - ( Summary "Add a new team member" - -- FUTUREWORK: deprecated in https://github.com/wireapp/wire-server/pull/2607 - :> CanThrow 'InvalidPermissions - :> CanThrow 'NoAddToBinding - :> CanThrow 'NotATeamMember - :> CanThrow 'NotConnected - :> CanThrow OperationDenied - :> CanThrow 'TeamNotFound - :> CanThrow 'TooManyTeamMembers - :> CanThrow 'UserBindingExists - :> CanThrow 'TooManyTeamMembersOnTeamWithLegalhold - :> ZLocalUser - :> ZConn - :> "teams" - :> Capture "tid" TeamId - :> "members" - :> ReqBody '[JSON] NewTeamMember - :> MultiVerb1 - 'POST - '[JSON] - (RespondEmpty 200 "") - ) - :<|> Named - "delete-team-member" - ( Summary "Remove an existing team member" - :> CanThrow AuthenticationError - :> CanThrow 'AccessDenied - :> CanThrow 'TeamMemberNotFound - :> CanThrow 'TeamNotFound - :> CanThrow 'NotATeamMember - :> CanThrow OperationDenied - :> ZLocalUser - :> ZConn - :> "teams" - :> Capture "tid" TeamId - :> "members" - :> Capture "uid" UserId - :> ReqBody '[JSON] TeamMemberDeleteData - :> MultiVerb - 'DELETE - '[JSON] - TeamMemberDeleteResultResponseType - TeamMemberDeleteResult - ) - :<|> Named - "delete-non-binding-team-member" - ( Summary "Remove an existing team member" - -- FUTUREWORK: deprecated in https://github.com/wireapp/wire-server/pull/2607 - :> CanThrow AuthenticationError - :> CanThrow 'AccessDenied - :> CanThrow 'TeamMemberNotFound - :> CanThrow 'TeamNotFound - :> CanThrow 'NotATeamMember - :> CanThrow OperationDenied - :> ZLocalUser - :> ZConn - :> "teams" - :> Capture "tid" TeamId - :> "members" - :> Capture "uid" UserId - :> MultiVerb - 'DELETE - '[JSON] - TeamMemberDeleteResultResponseType - TeamMemberDeleteResult - ) - :<|> Named - "update-team-member" - ( Summary "Update an existing team member" - :> CanThrow 'AccessDenied - :> CanThrow 'InvalidPermissions - :> CanThrow 'TeamNotFound - :> CanThrow 'TeamMemberNotFound - :> CanThrow 'NotATeamMember - :> CanThrow OperationDenied - :> ZLocalUser - :> ZConn - :> "teams" - :> Capture "tid" TeamId - :> "members" - :> ReqBody '[JSON] NewTeamMember - :> MultiVerb1 - 'PUT - '[JSON] - (RespondEmpty 200 "") - ) - :<|> Named - "get-team-members-csv" - ( Summary "Get all members of the team as a CSV file" - :> CanThrow 'AccessDenied - :> Description - "The endpoint returns data in chunked transfer encoding.\ - \ Internal server errors might result in a failed transfer\ - \ instead of a 500 response." - :> ZLocalUser - :> "teams" - :> Capture "tid" TeamId - :> "members" - :> "csv" - :> LowLevelStream - 'GET - 200 - '[ '( "Content-Disposition", - "attachment; filename=\"wire_team_members.csv\"" - ) - ] - "CSV of team members" - CSV - ) - -type TeamMemberDeleteResultResponseType = - '[ RespondEmpty 202 "Team member scheduled for deletion", - RespondEmpty 200 "" - ] - -data TeamMemberDeleteResult - = TeamMemberDeleteAccepted - | TeamMemberDeleteCompleted - deriving (Generic) - deriving (AsUnion TeamMemberDeleteResultResponseType) via GenericAsUnion TeamMemberDeleteResultResponseType TeamMemberDeleteResult - -instance GSOP.Generic TeamMemberDeleteResult - --- This is a work-around for the fact that we sometimes want to send larger lists of user ids --- in the filter query than fits the url length limit. For details, see --- https://github.com/zinfra/backend-issues/issues/1248 -type PostOtrDescriptionUnqualified = - "This endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\n\ - \To override this, the endpoint accepts two query params:\n\ - \- `ignore_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n\ - \ - When 'true' all missing clients are ignored.\n\ - \ - When 'false' all missing clients are reported.\n\ - \ - When comma separated list of user-ids, only clients for listed users are ignored.\n\ - \- `report_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n\ - \ - When 'true' all missing clients are reported.\n\ - \ - When 'false' all missing clients are ignored.\n\ - \ - When comma separated list of user-ids, only clients for listed users are reported.\n\ - \\n\ - \Apart from these, the request body also accepts `report_missing` which can only be a list of user ids and behaves the same way as the query parameter.\n\ - \\n\ - \All three of these should be considered mutually exclusive. The server however does not error if more than one is specified, it reads them in this order of precedence:\n\ - \- `report_missing` in the request body has highest precedence.\n\ - \- `ignore_missing` in the query param is the next.\n\ - \- `report_missing` in the query param has the lowest precedence.\n\ - \\n\ - \This endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\ - \\n\ - \**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto." - -type PostOtrDescription = - "This endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\n\ - \To override this, the endpoint accepts `client_mismatch_strategy` in the body. It can have these values:\n\ - \- `report_all`: When set, the message is not sent if any clients are missing. The missing clients are reported in the response.\n\ - \- `ignore_all`: When set, no checks about missing clients are carried out.\n\ - \- `report_only`: Takes a list of qualified UserIDs. If any clients of the listed users are missing, the message is not sent. The missing clients are reported in the response.\n\ - \- `ignore_only`: Takes a list of qualified UserIDs. If any clients of the non-listed users are missing, the message is not sent. The missing clients are reported in the response.\n\ - \\n\ - \The sending of messages in a federated conversation could theoretically fail partially. \ - \To make this case unlikely, the backend first gets a list of clients from all the involved backends and then tries to send a message. \ - \So, if any backend is down, the message is not propagated to anyone. \ - \But the actual message fan out to multiple backends could still fail partially. This type of failure is reported as a 201, \ - \the clients for which the message sending failed are part of the response body.\n\ - \\n\ - \This endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\ - \\n\ - \**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto." - swaggerDoc :: Swagger.Swagger swaggerDoc = toSwagger (Proxy @ServantAPI) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Bot.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Bot.hs new file mode 100644 index 0000000000..fddc356beb --- /dev/null +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Bot.hs @@ -0,0 +1,46 @@ +-- 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.Routes.Public.Galley.Bot where + +import Servant hiding (WithStatus) +import Servant.Swagger.Internal.Orphans () +import Wire.API.Error +import Wire.API.Error.Galley +import Wire.API.Message +import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Named +import Wire.API.Routes.Public +import Wire.API.Routes.Public.Galley.Messaging + +type BotAPI = + Named + "post-bot-message-unqualified" + ( ZBot + :> ZConversation + :> CanThrow 'ConvNotFound + :> "bot" + :> "messages" + :> QueryParam "ignore_missing" IgnoreMissing + :> QueryParam "report_missing" ReportMissing + :> ReqBody '[JSON] NewOtrMessage + :> MultiVerb + 'POST + '[Servant.JSON] + (PostOtrResponses ClientMismatch) + (PostOtrResponse ClientMismatch) + ) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs new file mode 100644 index 0000000000..8d996a3b98 --- /dev/null +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -0,0 +1,840 @@ +-- 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.Routes.Public.Galley.Conversation where + +import qualified Data.Code as Code +import Data.CommaSeparatedList +import Data.Id +import Data.Range +import Imports hiding (head) +import Servant hiding (WithStatus) +import Servant.Swagger.Internal.Orphans () +import Wire.API.Conversation +import Wire.API.Conversation.Role +import Wire.API.Error +import Wire.API.Error.Galley +import Wire.API.Event.Conversation +-- import Wire.API.MLS.GlobalTeamConversation +import Wire.API.MLS.PublicGroupState +import Wire.API.MLS.Servant +import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Named +import Wire.API.Routes.Public +import Wire.API.Routes.Public.Util +import Wire.API.Routes.QualifiedCapture +import Wire.API.Routes.Version +import Wire.API.Team.Feature + +type ConversationResponse = ResponseForExistedCreated Conversation + +type ConversationHeaders = '[DescHeader "Location" "Conversation ID" ConvId] + +type ConversationVerb = + MultiVerb + 'POST + '[JSON] + '[ WithHeaders + ConversationHeaders + Conversation + (Respond 200 "Conversation existed" Conversation), + WithHeaders + ConversationHeaders + Conversation + (Respond 201 "Conversation created" Conversation) + ] + ConversationResponse + +type CreateConversationCodeVerb = + MultiVerb + 'POST + '[JSON] + '[ Respond 200 "Conversation code already exists." ConversationCode, + Respond 201 "Conversation code created." Event + ] + AddCodeResult + +type ConvUpdateResponses = UpdateResponses "Conversation unchanged" "Conversation updated" Event + +type ConvJoinResponses = UpdateResponses "Conversation unchanged" "Conversation joined" Event + +type RemoveFromConversationVerb = + MultiVerb + 'DELETE + '[JSON] + '[ RespondEmpty 204 "No change", + Respond 200 "Member removed" Event + ] + (Maybe Event) + +type ConversationAPI = + Named + "get-unqualified-conversation" + ( Summary "Get a conversation by ID" + :> CanThrow 'ConvNotFound + :> CanThrow 'ConvAccessDenied + :> ZLocalUser + :> "conversations" + :> 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)" + :> Until 'V2 + :> CanThrow 'ConvNotFound + :> CanThrow 'ConvAccessDenied + :> ZLocalUser + :> "legalhold" + :> "conversations" + :> Capture "cnv" ConvId + :> Get '[Servant.JSON] Conversation + ) + :<|> Named + "get-conversation" + ( Summary "Get a conversation by ID" + :> CanThrow 'ConvNotFound + :> CanThrow 'ConvAccessDenied + :> ZLocalUser + :> "conversations" + :> QualifiedCapture "cnv" ConvId + :> Get '[Servant.JSON] Conversation + ) + -- :<|> Named + -- "get-global-team-conversation" + -- ( Summary "Get the global conversation for a given team ID" + -- :> CanThrow 'ConvNotFound + -- :> CanThrow 'NotATeamMember + -- :> ZLocalUser + -- :> "teams" + -- :> Capture "tid" TeamId + -- :> "conversations" + -- :> "global" + -- :> Get '[Servant.JSON] GlobalTeamConversation + -- ) + :<|> Named + "get-conversation-roles" + ( Summary "Get existing roles available for the given conversation" + :> CanThrow 'ConvNotFound + :> CanThrow 'ConvAccessDenied + :> ZLocalUser + :> "conversations" + :> Capture "cnv" ConvId + :> "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." + -- FUTUREWORK: add bounds to swagger schema for Range + :> Until 'V3 + :> ZLocalUser + :> "conversations" + :> "ids" + :> QueryParam' + [ Optional, + Strict, + Description "Conversation ID to start from (exclusive)" + ] + "start" + ConvId + :> QueryParam' + [ Optional, + Strict, + Description "Maximum number of IDs to return" + ] + "size" + (Range 1 1000 Int32) + :> Get '[Servant.JSON] (ConversationList ConvId) + ) + :<|> Named + "list-conversation-ids-v2" + ( Summary "Get all conversation IDs." + :> Until 'V3 + :> Description PaginationDocs + :> ZLocalUser + :> "conversations" + :> "list-ids" + :> ReqBody '[Servant.JSON] GetPaginatedConversationIds + :> Post '[Servant.JSON] ConvIdsPage + ) + :<|> Named + "list-conversation-ids" + ( Summary "Get all conversation IDs." + :> From 'V3 + :> Description PaginationDocs + :> ZLocalUser + :> "conversations" + :> "list-ids" + :> ReqBody '[Servant.JSON] GetPaginatedConversationIds + :> Post '[Servant.JSON] ConvIdsPage + ) + :<|> Named + "get-conversations" + ( Summary "Get all *local* conversations." + :> Description + "Will not return remote conversations.\n\n\ + \Use `POST /conversations/list-ids` followed by \ + \`POST /conversations/list` instead." + :> ZLocalUser + :> "conversations" + :> QueryParam' + [ Optional, + Strict, + Description "Mutually exclusive with 'start' (at most 32 IDs per request)" + ] + "ids" + (Range 1 32 (CommaSeparatedList ConvId)) + :> QueryParam' + [ Optional, + Strict, + Description "Conversation ID to start from (exclusive)" + ] + "start" + ConvId + :> QueryParam' + [ Optional, + Strict, + Description "Maximum number of conversations to return" + ] + "size" + (Range 1 500 Int32) + :> Get '[Servant.JSON] (ConversationList Conversation) + ) + :<|> Named + "list-conversations-v1" + ( Summary "Get conversation metadata for a list of conversation ids" + :> Until 'V2 + :> ZLocalUser + :> "conversations" + :> "list" + :> "v2" + :> ReqBody '[Servant.JSON] ListConversations + :> Post '[Servant.JSON] ConversationsResponse + ) + :<|> Named + "list-conversations" + ( Summary "Get conversation metadata for a list of conversation ids" + :> From 'V2 + :> ZLocalUser + :> "conversations" + :> "list" + :> ReqBody '[Servant.JSON] ListConversations + :> Post '[Servant.JSON] ConversationsResponse + ) + -- This endpoint can lead to the following events being sent: + -- - ConvCreate event to members + :<|> Named + "get-conversation-by-reusable-code" + ( Summary "Get limited conversation information by key/code pair" + :> CanThrow 'CodeNotFound + :> CanThrow 'ConvNotFound + :> CanThrow 'ConvAccessDenied + :> CanThrow 'GuestLinksDisabled + :> CanThrow 'NotATeamMember + :> ZLocalUser + :> "conversations" + :> "join" + :> QueryParam' [Required, Strict] "key" Code.Key + :> QueryParam' [Required, Strict] "code" Code.Value + :> Get '[Servant.JSON] ConversationCoverView + ) + :<|> Named + "create-group-conversation" + ( Summary "Create a new conversation" + :> CanThrow 'ConvAccessDenied + :> CanThrow 'MLSNonEmptyMemberList + :> CanThrow 'NotConnected + :> CanThrow 'NotATeamMember + :> CanThrow OperationDenied + :> CanThrow 'MissingLegalholdConsent + :> Description "This returns 201 when a new conversation is created, and 200 when the conversation already existed" + :> ZLocalUser + :> ZConn + :> "conversations" + :> ReqBody '[Servant.JSON] NewConv + :> ConversationVerb + ) + :<|> Named + "create-self-conversation" + ( Summary "Create a self-conversation" + :> ZLocalUser + :> "conversations" + :> "self" + :> ConversationVerb + ) + :<|> Named + "get-mls-self-conversation" + ( Summary "Get the user's MLS self-conversation" + :> ZLocalUser + :> "conversations" + :> "mls-self" + :> MultiVerb1 + 'GET + '[JSON] + ( Respond + 200 + "The MLS self-conversation" + Conversation + ) + ) + -- This endpoint can lead to the following events being sent: + -- - ConvCreate event to members + -- TODO: add note: "On 201, the conversation ID is the `Location` header" + :<|> Named + "create-one-to-one-conversation" + ( Summary "Create a 1:1 conversation" + :> CanThrow 'ConvAccessDenied + :> CanThrow 'InvalidOperation + :> CanThrow 'NoBindingTeamMembers + :> CanThrow 'NonBindingTeam + :> CanThrow 'NotATeamMember + :> CanThrow 'NotConnected + :> CanThrow OperationDenied + :> CanThrow 'TeamNotFound + :> CanThrow 'MissingLegalholdConsent + :> ZLocalUser + :> ZConn + :> "conversations" + :> "one2one" + :> ReqBody '[Servant.JSON] NewConv + :> ConversationVerb + ) + -- This endpoint can lead to the following events being sent: + -- - MemberJoin event to members + :<|> Named + "add-members-to-conversation-unqualified" + ( Summary "Add members to an existing conversation (deprecated)" + :> Until 'V2 + :> CanThrow ('ActionDenied 'AddConversationMember) + :> CanThrow ('ActionDenied 'LeaveConversation) + :> CanThrow 'ConvNotFound + :> CanThrow 'InvalidOperation + :> CanThrow 'TooManyMembers + :> CanThrow 'ConvAccessDenied + :> CanThrow 'NotATeamMember + :> CanThrow 'NotConnected + :> CanThrow 'MissingLegalholdConsent + :> ZLocalUser + :> ZConn + :> "conversations" + :> Capture "cnv" ConvId + :> "members" + :> ReqBody '[JSON] Invite + :> MultiVerb 'POST '[JSON] ConvUpdateResponses (UpdateResult Event) + ) + :<|> Named + "add-members-to-conversation-unqualified2" + ( Summary "Add qualified members to an existing conversation." + :> Until 'V2 + :> CanThrow ('ActionDenied 'AddConversationMember) + :> CanThrow ('ActionDenied 'LeaveConversation) + :> CanThrow 'ConvNotFound + :> CanThrow 'InvalidOperation + :> CanThrow 'TooManyMembers + :> CanThrow 'ConvAccessDenied + :> CanThrow 'NotATeamMember + :> CanThrow 'NotConnected + :> CanThrow 'MissingLegalholdConsent + :> ZLocalUser + :> ZConn + :> "conversations" + :> Capture "cnv" ConvId + :> "members" + :> "v2" + :> ReqBody '[Servant.JSON] InviteQualified + :> MultiVerb 'POST '[Servant.JSON] ConvUpdateResponses (UpdateResult Event) + ) + :<|> Named + "add-members-to-conversation" + ( Summary "Add qualified members to an existing conversation." + :> From 'V2 + :> CanThrow ('ActionDenied 'AddConversationMember) + :> CanThrow ('ActionDenied 'LeaveConversation) + :> CanThrow 'ConvNotFound + :> CanThrow 'InvalidOperation + :> CanThrow 'TooManyMembers + :> CanThrow 'ConvAccessDenied + :> CanThrow 'NotATeamMember + :> CanThrow 'NotConnected + :> CanThrow 'MissingLegalholdConsent + :> ZLocalUser + :> ZConn + :> "conversations" + :> QualifiedCapture "cnv" ConvId + :> "members" + :> ReqBody '[Servant.JSON] InviteQualified + :> MultiVerb 'POST '[Servant.JSON] ConvUpdateResponses (UpdateResult Event) + ) + -- This endpoint can lead to the following events being sent: + -- - MemberJoin event to members + :<|> Named + "join-conversation-by-id-unqualified" + ( Summary "Join a conversation by its ID (if link access enabled)" + :> CanThrow 'ConvAccessDenied + :> CanThrow 'ConvNotFound + :> CanThrow 'InvalidOperation + :> CanThrow 'NotATeamMember + :> CanThrow 'TooManyMembers + :> ZLocalUser + :> ZConn + :> "conversations" + :> Capture' '[Description "Conversation ID"] "cnv" ConvId + :> "join" + :> MultiVerb 'POST '[Servant.JSON] ConvJoinResponses (UpdateResult Event) + ) + -- This endpoint can lead to the following events being sent: + -- - MemberJoin event to members + :<|> Named + "join-conversation-by-code-unqualified" + ( Summary + "Join a conversation using a reusable code.\ + \If the guest links team feature is disabled, this will fail with 409 GuestLinksDisabled.\ + \Note that this is currently inconsistent (for backwards compatibility reasons) with `POST /conversations/code-check` which responds with 404 CodeNotFound if guest links are disabled." + :> CanThrow 'CodeNotFound + :> CanThrow 'ConvAccessDenied + :> CanThrow 'ConvNotFound + :> CanThrow 'GuestLinksDisabled + :> CanThrow 'InvalidOperation + :> CanThrow 'NotATeamMember + :> CanThrow 'TooManyMembers + :> ZLocalUser + :> ZConn + :> "conversations" + :> "join" + :> ReqBody '[Servant.JSON] ConversationCode + :> MultiVerb 'POST '[Servant.JSON] ConvJoinResponses (UpdateResult Event) + ) + :<|> Named + "code-check" + ( Summary + "Check validity of a conversation code.\ + \If the guest links team feature is disabled, this will fail with 404 CodeNotFound.\ + \Note that this is currently inconsistent (for backwards compatibility reasons) with `POST /conversations/join` which responds with 409 GuestLinksDisabled if guest links are disabled." + :> CanThrow 'CodeNotFound + :> CanThrow 'ConvNotFound + :> "conversations" + :> "code-check" + :> ReqBody '[Servant.JSON] ConversationCode + :> MultiVerb + 'POST + '[JSON] + '[RespondEmpty 200 "Valid"] + () + ) + -- this endpoint can lead to the following events being sent: + -- - ConvCodeUpdate event to members, if code didn't exist before + :<|> Named + "create-conversation-code-unqualified" + ( Summary "Create or recreate a conversation code" + :> CanThrow 'ConvAccessDenied + :> CanThrow 'ConvNotFound + :> CanThrow 'GuestLinksDisabled + :> ZUser + :> ZConn + :> "conversations" + :> Capture' '[Description "Conversation ID"] "cnv" ConvId + :> "code" + :> CreateConversationCodeVerb + ) + :<|> Named + "get-conversation-guest-links-status" + ( Summary "Get the status of the guest links feature for a conversation that potentially has been created by someone from another team." + :> CanThrow 'ConvAccessDenied + :> CanThrow 'ConvNotFound + :> ZUser + :> "conversations" + :> Capture' '[Description "Conversation ID"] "cnv" ConvId + :> "features" + :> FeatureSymbol GuestLinksConfig + :> Get '[Servant.JSON] (WithStatus GuestLinksConfig) + ) + -- This endpoint can lead to the following events being sent: + -- - ConvCodeDelete event to members + :<|> Named + "remove-code-unqualified" + ( Summary "Delete conversation code" + :> CanThrow 'ConvAccessDenied + :> CanThrow 'ConvNotFound + :> ZLocalUser + :> ZConn + :> "conversations" + :> Capture' '[Description "Conversation ID"] "cnv" ConvId + :> "code" + :> MultiVerb + 'DELETE + '[JSON] + '[Respond 200 "Conversation code deleted." Event] + Event + ) + :<|> Named + "get-code" + ( Summary "Get existing conversation code" + :> CanThrow 'CodeNotFound + :> CanThrow 'ConvAccessDenied + :> CanThrow 'ConvNotFound + :> CanThrow 'GuestLinksDisabled + :> ZLocalUser + :> "conversations" + :> Capture' '[Description "Conversation ID"] "cnv" ConvId + :> "code" + :> MultiVerb + 'GET + '[JSON] + '[Respond 200 "Conversation Code" ConversationCode] + ConversationCode + ) + -- This endpoint can lead to the following events being sent: + -- - Typing event to members + :<|> Named + "member-typing-unqualified" + ( Summary "Sending typing notifications" + :> CanThrow 'ConvNotFound + :> ZLocalUser + :> ZConn + :> "conversations" + :> Capture' '[Description "Conversation ID"] "cnv" ConvId + :> "typing" + :> ReqBody '[JSON] TypingData + :> MultiVerb 'POST '[JSON] '[RespondEmpty 200 "Notification sent"] () + ) + -- This endpoint can lead to the following events being sent: + -- - MemberLeave event to members + :<|> Named + "remove-member-unqualified" + ( Summary "Remove a member from a conversation (deprecated)" + :> Until 'V2 + :> ZLocalUser + :> ZConn + :> CanThrow ('ActionDenied 'RemoveConversationMember) + :> CanThrow 'ConvNotFound + :> CanThrow 'InvalidOperation + :> "conversations" + :> Capture' '[Description "Conversation ID"] "cnv" ConvId + :> "members" + :> Capture' '[Description "Target User ID"] "usr" UserId + :> RemoveFromConversationVerb + ) + -- This endpoint can lead to the following events being sent: + -- - MemberLeave event to members + :<|> Named + "remove-member" + ( Summary "Remove a member from a conversation" + :> ZLocalUser + :> ZConn + :> CanThrow ('ActionDenied 'RemoveConversationMember) + :> CanThrow 'ConvNotFound + :> CanThrow 'InvalidOperation + :> "conversations" + :> QualifiedCapture' '[Description "Conversation ID"] "cnv" ConvId + :> "members" + :> QualifiedCapture' '[Description "Target User ID"] "usr" UserId + :> RemoveFromConversationVerb + ) + -- This endpoint can lead to the following events being sent: + -- - MemberStateUpdate event to members + :<|> Named + "update-other-member-unqualified" + ( Summary "Update membership of the specified user (deprecated)" + :> Description "Use `PUT /conversations/:cnv_domain/:cnv/members/:usr_domain/:usr` instead" + :> ZLocalUser + :> ZConn + :> CanThrow 'ConvNotFound + :> CanThrow 'ConvMemberNotFound + :> CanThrow ('ActionDenied 'ModifyOtherConversationMember) + :> CanThrow 'InvalidTarget + :> CanThrow 'InvalidOperation + :> "conversations" + :> Capture' '[Description "Conversation ID"] "cnv" ConvId + :> "members" + :> Capture' '[Description "Target User ID"] "usr" UserId + :> ReqBody '[JSON] OtherMemberUpdate + :> MultiVerb + 'PUT + '[JSON] + '[RespondEmpty 200 "Membership updated"] + () + ) + :<|> Named + "update-other-member" + ( Summary "Update membership of the specified user" + :> Description "**Note**: at least one field has to be provided." + :> ZLocalUser + :> ZConn + :> CanThrow 'ConvNotFound + :> CanThrow 'ConvMemberNotFound + :> CanThrow ('ActionDenied 'ModifyOtherConversationMember) + :> CanThrow 'InvalidTarget + :> CanThrow 'InvalidOperation + :> "conversations" + :> QualifiedCapture' '[Description "Conversation ID"] "cnv" ConvId + :> "members" + :> QualifiedCapture' '[Description "Target User ID"] "usr" UserId + :> ReqBody '[JSON] OtherMemberUpdate + :> MultiVerb + 'PUT + '[JSON] + '[RespondEmpty 200 "Membership updated"] + () + ) + -- This endpoint can lead to the following events being sent: + -- - ConvRename event to members + :<|> Named + "update-conversation-name-deprecated" + ( Summary "Update conversation name (deprecated)" + :> Description "Use `/conversations/:domain/:conv/name` instead." + :> CanThrow ('ActionDenied 'ModifyConversationName) + :> CanThrow 'ConvNotFound + :> CanThrow 'InvalidOperation + :> ZLocalUser + :> ZConn + :> "conversations" + :> Capture' '[Description "Conversation ID"] "cnv" ConvId + :> ReqBody '[JSON] ConversationRename + :> MultiVerb + 'PUT + '[JSON] + (UpdateResponses "Name unchanged" "Name updated" Event) + (UpdateResult Event) + ) + :<|> Named + "update-conversation-name-unqualified" + ( Summary "Update conversation name (deprecated)" + :> Description "Use `/conversations/:domain/:conv/name` instead." + :> CanThrow ('ActionDenied 'ModifyConversationName) + :> CanThrow 'ConvNotFound + :> CanThrow 'InvalidOperation + :> ZLocalUser + :> ZConn + :> "conversations" + :> Capture' '[Description "Conversation ID"] "cnv" ConvId + :> "name" + :> ReqBody '[JSON] ConversationRename + :> MultiVerb + 'PUT + '[JSON] + (UpdateResponses "Name unchanged" "Name updated" Event) + (UpdateResult Event) + ) + :<|> Named + "update-conversation-name" + ( Summary "Update conversation name" + :> CanThrow ('ActionDenied 'ModifyConversationName) + :> CanThrow 'ConvNotFound + :> CanThrow 'InvalidOperation + :> ZLocalUser + :> ZConn + :> "conversations" + :> QualifiedCapture' '[Description "Conversation ID"] "cnv" ConvId + :> "name" + :> ReqBody '[JSON] ConversationRename + :> MultiVerb + 'PUT + '[JSON] + (UpdateResponses "Name updated" "Name unchanged" Event) + (UpdateResult Event) + ) + -- This endpoint can lead to the following events being sent: + -- - ConvMessageTimerUpdate event to members + :<|> Named + "update-conversation-message-timer-unqualified" + ( Summary "Update the message timer for a conversation (deprecated)" + :> Description "Use `/conversations/:domain/:cnv/message-timer` instead." + :> ZLocalUser + :> ZConn + :> CanThrow ('ActionDenied 'ModifyConversationMessageTimer) + :> CanThrow 'ConvAccessDenied + :> CanThrow 'ConvNotFound + :> CanThrow 'InvalidOperation + :> "conversations" + :> Capture' '[Description "Conversation ID"] "cnv" ConvId + :> "message-timer" + :> ReqBody '[JSON] ConversationMessageTimerUpdate + :> MultiVerb + 'PUT + '[JSON] + (UpdateResponses "Message timer unchanged" "Message timer updated" Event) + (UpdateResult Event) + ) + :<|> Named + "update-conversation-message-timer" + ( Summary "Update the message timer for a conversation" + :> ZLocalUser + :> ZConn + :> CanThrow ('ActionDenied 'ModifyConversationMessageTimer) + :> CanThrow 'ConvAccessDenied + :> CanThrow 'ConvNotFound + :> CanThrow 'InvalidOperation + :> "conversations" + :> QualifiedCapture' '[Description "Conversation ID"] "cnv" ConvId + :> "message-timer" + :> ReqBody '[JSON] ConversationMessageTimerUpdate + :> MultiVerb + 'PUT + '[JSON] + (UpdateResponses "Message timer unchanged" "Message timer updated" Event) + (UpdateResult Event) + ) + -- This endpoint can lead to the following events being sent: + -- - ConvReceiptModeUpdate event to members + :<|> Named + "update-conversation-receipt-mode-unqualified" + ( Summary "Update receipt mode for a conversation (deprecated)" + :> Description "Use `PUT /conversations/:domain/:cnv/receipt-mode` instead." + :> ZLocalUser + :> ZConn + :> CanThrow ('ActionDenied 'ModifyConversationReceiptMode) + :> CanThrow 'ConvAccessDenied + :> CanThrow 'ConvNotFound + :> CanThrow 'InvalidOperation + :> "conversations" + :> Capture' '[Description "Conversation ID"] "cnv" ConvId + :> "receipt-mode" + :> ReqBody '[JSON] ConversationReceiptModeUpdate + :> MultiVerb + 'PUT + '[JSON] + (UpdateResponses "Receipt mode unchanged" "Receipt mode updated" Event) + (UpdateResult Event) + ) + :<|> Named + "update-conversation-receipt-mode" + ( Summary "Update receipt mode for a conversation" + :> ZLocalUser + :> ZConn + :> CanThrow ('ActionDenied 'ModifyConversationReceiptMode) + :> CanThrow 'ConvAccessDenied + :> CanThrow 'ConvNotFound + :> CanThrow 'InvalidOperation + :> "conversations" + :> QualifiedCapture' '[Description "Conversation ID"] "cnv" ConvId + :> "receipt-mode" + :> ReqBody '[JSON] ConversationReceiptModeUpdate + :> MultiVerb + 'PUT + '[JSON] + (UpdateResponses "Receipt mode unchanged" "Receipt mode updated" Event) + (UpdateResult Event) + ) + -- This endpoint can lead to the following events being sent: + -- - MemberLeave event to members, if members get removed + -- - ConvAccessUpdate event to members + :<|> Named + "update-conversation-access-unqualified" + ( Summary "Update access modes for a conversation (deprecated)" + :> Description "Use PUT `/conversations/:domain/:cnv/access` instead." + :> ZLocalUser + :> ZConn + :> CanThrow ('ActionDenied 'ModifyConversationAccess) + :> CanThrow ('ActionDenied 'RemoveConversationMember) + :> CanThrow 'ConvAccessDenied + :> CanThrow 'ConvNotFound + :> CanThrow 'InvalidOperation + :> CanThrow 'InvalidTargetAccess + :> "conversations" + :> Capture' '[Description "Conversation ID"] "cnv" ConvId + :> "access" + :> ReqBody '[JSON] ConversationAccessData + :> MultiVerb + 'PUT + '[JSON] + (UpdateResponses "Access unchanged" "Access updated" Event) + (UpdateResult Event) + ) + :<|> Named + "update-conversation-access" + ( Summary "Update access modes for a conversation" + :> ZLocalUser + :> ZConn + :> CanThrow ('ActionDenied 'ModifyConversationAccess) + :> CanThrow ('ActionDenied 'RemoveConversationMember) + :> CanThrow 'ConvAccessDenied + :> CanThrow 'ConvNotFound + :> CanThrow 'InvalidOperation + :> CanThrow 'InvalidTargetAccess + :> "conversations" + :> QualifiedCapture' '[Description "Conversation ID"] "cnv" ConvId + :> "access" + :> ReqBody '[JSON] ConversationAccessData + :> MultiVerb + 'PUT + '[JSON] + (UpdateResponses "Access unchanged" "Access updated" Event) + (UpdateResult Event) + ) + :<|> Named + "get-conversation-self-unqualified" + ( Summary "Get self membership properties (deprecated)" + :> ZLocalUser + :> "conversations" + :> Capture' '[Description "Conversation ID"] "cnv" ConvId + :> "self" + :> Get '[JSON] (Maybe Member) + ) + :<|> Named + "update-conversation-self-unqualified" + ( Summary "Update self membership properties (deprecated)" + :> Description "Use `/conversations/:domain/:conv/self` instead." + :> CanThrow 'ConvNotFound + :> ZLocalUser + :> ZConn + :> "conversations" + :> Capture' '[Description "Conversation ID"] "cnv" ConvId + :> "self" + :> ReqBody '[JSON] MemberUpdate + :> MultiVerb + 'PUT + '[JSON] + '[RespondEmpty 200 "Update successful"] + () + ) + :<|> Named + "update-conversation-self" + ( Summary "Update self membership properties" + :> Description "**Note**: at least one field has to be provided." + :> CanThrow 'ConvNotFound + :> ZLocalUser + :> ZConn + :> "conversations" + :> QualifiedCapture' '[Description "Conversation ID"] "cnv" ConvId + :> "self" + :> ReqBody '[JSON] MemberUpdate + :> MultiVerb + 'PUT + '[JSON] + '[RespondEmpty 200 "Update successful"] + () + ) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/CustomBackend.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/CustomBackend.hs new file mode 100644 index 0000000000..079858baa0 --- /dev/null +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/CustomBackend.hs @@ -0,0 +1,37 @@ +-- 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.Routes.Public.Galley.CustomBackend where + +import Data.Domain +import Servant hiding (WithStatus) +import Servant.Swagger.Internal.Orphans () +import Wire.API.CustomBackend +import Wire.API.Error +import Wire.API.Error.Galley +import Wire.API.Routes.Named + +type CustomBackendAPI = + Named + "get-custom-backend-by-domain" + ( Summary "Shows information about custom backends related to a given email domain" + :> CanThrow 'CustomBackendNotFound + :> "custom-backend" + :> "by-domain" + :> Capture' '[Description "URL-encoded email domain"] "domain" Domain + :> Get '[JSON] CustomBackend + ) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs new file mode 100644 index 0000000000..f52fd7b183 --- /dev/null +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs @@ -0,0 +1,260 @@ +-- 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.Routes.Public.Galley.Feature where + +import Data.Id +import GHC.TypeLits +import Servant hiding (WithStatus) +import Servant.Swagger.Internal.Orphans () +import Wire.API.Conversation.Role +import Wire.API.Error +import Wire.API.Error.Galley +import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Named +import Wire.API.Routes.Public +import Wire.API.Routes.Version +import Wire.API.Team.Feature +import Wire.API.Team.SearchVisibility (TeamSearchVisibilityView) + +type FeatureAPI = + FeatureStatusGet SSOConfig + :<|> FeatureStatusGet LegalholdConfig + :<|> FeatureStatusPut + '( 'ActionDenied 'RemoveConversationMember, + '( AuthenticationError, + '( 'CannotEnableLegalHoldServiceLargeTeam, + '( 'LegalHoldNotEnabled, + '( 'LegalHoldDisableUnimplemented, + '( 'LegalHoldServiceNotRegistered, + '( 'UserLegalHoldIllegalOperation, + '( 'LegalHoldCouldNotBlockConnections, '()) + ) + ) + ) + ) + ) + ) + ) + LegalholdConfig + :<|> FeatureStatusGet SearchVisibilityAvailableConfig + :<|> FeatureStatusPut '() SearchVisibilityAvailableConfig + :<|> FeatureStatusDeprecatedGet "This endpoint is potentially used by the old Android client. It is not used by iOS, team management, or webapp as of June 2022" SearchVisibilityAvailableConfig + :<|> FeatureStatusDeprecatedPut "This endpoint is potentially used by the old Android client. It is not used by iOS, team management, or webapp as of June 2022" SearchVisibilityAvailableConfig + :<|> SearchVisibilityGet + :<|> SearchVisibilitySet + :<|> FeatureStatusGet ValidateSAMLEmailsConfig + :<|> FeatureStatusDeprecatedGet "This endpoint is potentially used by the old Android client. It is not used by iOS, team management, or webapp as of June 2022" ValidateSAMLEmailsConfig + :<|> FeatureStatusGet DigitalSignaturesConfig + :<|> FeatureStatusDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is potentially used by the old Android client. It is not used by team management, or webapp as of June 2022" DigitalSignaturesConfig + :<|> FeatureStatusGet AppLockConfig + :<|> FeatureStatusPut '() AppLockConfig + :<|> FeatureStatusGet FileSharingConfig + :<|> FeatureStatusPut '() FileSharingConfig + :<|> FeatureStatusGet ClassifiedDomainsConfig + :<|> FeatureStatusGet ConferenceCallingConfig + :<|> FeatureStatusGet SelfDeletingMessagesConfig + :<|> FeatureStatusPut '() SelfDeletingMessagesConfig + :<|> FeatureStatusGet GuestLinksConfig + :<|> FeatureStatusPut '() GuestLinksConfig + :<|> FeatureStatusGet SndFactorPasswordChallengeConfig + :<|> FeatureStatusPut '() SndFactorPasswordChallengeConfig + :<|> FeatureStatusGet MLSConfig + :<|> FeatureStatusPut '() MLSConfig + :<|> FeatureStatusGet ExposeInvitationURLsToTeamAdminConfig + :<|> FeatureStatusPut '() ExposeInvitationURLsToTeamAdminConfig + :<|> FeatureStatusGet SearchVisibilityInboundConfig + :<|> FeatureStatusPut '() SearchVisibilityInboundConfig + :<|> AllFeatureConfigsUserGet + :<|> AllFeatureConfigsTeamGet + :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" LegalholdConfig + :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" SSOConfig + :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" SearchVisibilityAvailableConfig + :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" ValidateSAMLEmailsConfig + :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" DigitalSignaturesConfig + :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" AppLockConfig + :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" FileSharingConfig + :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" ClassifiedDomainsConfig + :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" ConferenceCallingConfig + :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" SelfDeletingMessagesConfig + :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" GuestLinksConfig + :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" SndFactorPasswordChallengeConfig + :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" MLSConfig + +type FeatureStatusGet f = + Named + '("get", f) + (ZUser :> FeatureStatusBaseGet f) + +type FeatureStatusPut errs f = + Named + '("put", f) + (ZUser :> FeatureStatusBasePutPublic errs f) + +type FeatureStatusDeprecatedGet d f = + Named + '("get-deprecated", f) + (ZUser :> FeatureStatusBaseDeprecatedGet d f) + +type FeatureStatusDeprecatedPut d f = + Named + '("put-deprecated", f) + (ZUser :> FeatureStatusBaseDeprecatedPut d f) + +type FeatureStatusBaseGet featureConfig = + Summary (AppendSymbol "Get config for " (FeatureSymbol featureConfig)) + :> CanThrow OperationDenied + :> CanThrow 'NotATeamMember + :> CanThrow 'TeamNotFound + :> "teams" + :> Capture "tid" TeamId + :> "features" + :> FeatureSymbol featureConfig + :> Get '[Servant.JSON] (WithStatus featureConfig) + +type FeatureStatusBasePutPublic errs featureConfig = + Summary (AppendSymbol "Put config for " (FeatureSymbol featureConfig)) + :> CanThrow OperationDenied + :> CanThrow 'NotATeamMember + :> CanThrow 'TeamNotFound + :> CanThrow TeamFeatureError + :> CanThrowMany errs + :> "teams" + :> Capture "tid" TeamId + :> "features" + :> FeatureSymbol featureConfig + :> ReqBody '[Servant.JSON] (WithStatusNoLock featureConfig) + :> Put '[Servant.JSON] (WithStatus featureConfig) + +-- | A type for a GET endpoint for a feature with a deprecated path +type FeatureStatusBaseDeprecatedGet desc featureConfig = + ( Summary + (AppendSymbol "[deprecated] Get config for " (FeatureSymbol featureConfig)) + :> Until 'V2 + :> Description + ( "Deprecated. Please use `GET /teams/:tid/features/" + `AppendSymbol` FeatureSymbol featureConfig + `AppendSymbol` "` instead.\n" + `AppendSymbol` desc + ) + :> CanThrow 'NotATeamMember + :> CanThrow OperationDenied + :> CanThrow 'TeamNotFound + :> "teams" + :> Capture "tid" TeamId + :> "features" + :> DeprecatedFeatureName featureConfig + :> Get '[Servant.JSON] (WithStatus featureConfig) + ) + +-- | A type for a PUT endpoint for a feature with a deprecated path +type FeatureStatusBaseDeprecatedPut desc featureConfig = + Summary + (AppendSymbol "[deprecated] Get config for " (FeatureSymbol featureConfig)) + :> Until 'V2 + :> Description + ( "Deprecated. Please use `PUT /teams/:tid/features/" + `AppendSymbol` FeatureSymbol featureConfig + `AppendSymbol` "` instead.\n" + `AppendSymbol` desc + ) + :> CanThrow 'NotATeamMember + :> CanThrow OperationDenied + :> CanThrow 'TeamNotFound + :> CanThrow TeamFeatureError + :> "teams" + :> Capture "tid" TeamId + :> "features" + :> DeprecatedFeatureName featureConfig + :> ReqBody '[Servant.JSON] (WithStatusNoLock featureConfig) + :> Put '[Servant.JSON] (WithStatus featureConfig) + +type FeatureConfigDeprecatedGet desc featureConfig = + Named + '("get-config", featureConfig) + ( Summary (AppendSymbol "[deprecated] Get feature config for feature " (FeatureSymbol featureConfig)) + :> Until 'V2 + :> Description ("Deprecated. Please use `GET /feature-configs` instead.\n" `AppendSymbol` desc) + :> ZUser + :> CanThrow 'NotATeamMember + :> CanThrow OperationDenied + :> CanThrow 'TeamNotFound + :> "feature-configs" + :> FeatureSymbol featureConfig + :> Get '[Servant.JSON] (WithStatus featureConfig) + ) + +type AllFeatureConfigsUserGet = + Named + "get-all-feature-configs-for-user" + ( Summary + "Gets feature configs for a user" + :> Description + "Gets feature configs for a user. If the user is a member of a team and has the required permissions, this will return the team's feature configs.\ + \If the user is not a member of a team, this will return the personal feature configs (the server defaults)." + :> ZUser + :> CanThrow 'NotATeamMember + :> CanThrow OperationDenied + :> CanThrow 'TeamNotFound + :> "feature-configs" + :> Get '[Servant.JSON] AllFeatureConfigs + ) + +type AllFeatureConfigsTeamGet = + Named + "get-all-feature-configs-for-team" + ( Summary "Gets feature configs for a team" + :> Description "Gets feature configs for a team. User must be a member of the team and have permission to view team features." + :> CanThrow 'NotATeamMember + :> CanThrow OperationDenied + :> CanThrow 'TeamNotFound + :> ZLocalUser + :> "teams" + :> Capture "tid" TeamId + :> "features" + :> Get '[JSON] AllFeatureConfigs + ) + +type SearchVisibilityGet = + Named + "get-search-visibility" + ( Summary "Shows the value for search visibility" + :> CanThrow 'NotATeamMember + :> CanThrow OperationDenied + :> ZLocalUser + :> "teams" + :> Capture "tid" TeamId + :> "search-visibility" + :> Get '[JSON] TeamSearchVisibilityView + ) + +type SearchVisibilitySet = + Named + "set-search-visibility" + ( Summary "Sets the search visibility for the whole team" + :> CanThrow 'NotATeamMember + :> CanThrow OperationDenied + :> CanThrow 'TeamSearchVisibilityNotEnabled + :> CanThrow 'TeamNotFound + :> CanThrow TeamFeatureError + :> ZLocalUser + :> "teams" + :> Capture "tid" TeamId + :> "search-visibility" + :> ReqBody '[JSON] TeamSearchVisibilityView + :> MultiVerb 'PUT '[JSON] '[RespondEmpty 204 "Search visibility set"] () + ) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs new file mode 100644 index 0000000000..0c1ae5b2f1 --- /dev/null +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs @@ -0,0 +1,234 @@ +-- 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.Routes.Public.Galley.LegalHold where + +import Data.Id +import GHC.Generics +import qualified Generics.SOP as GSOP +import Servant hiding (WithStatus) +import Servant.Swagger.Internal.Orphans () +import Wire.API.Conversation.Role +import Wire.API.Error +import Wire.API.Error.Galley +import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Named +import Wire.API.Routes.Public +import Wire.API.Team.LegalHold + +type LegalHoldAPI = + Named + "create-legal-hold-settings" + ( Summary "Create legal hold service settings" + :> CanThrow 'NotATeamMember + :> CanThrow OperationDenied + :> CanThrow 'LegalHoldNotEnabled + :> CanThrow 'LegalHoldServiceInvalidKey + :> CanThrow 'LegalHoldServiceBadResponse + :> ZLocalUser + :> "teams" + :> Capture "tid" TeamId + :> "legalhold" + :> "settings" + :> ReqBody '[JSON] NewLegalHoldService + :> MultiVerb1 'POST '[JSON] (Respond 201 "Legal hold service settings created" ViewLegalHoldService) + ) + :<|> Named + "get-legal-hold-settings" + ( Summary "Get legal hold service settings" + :> CanThrow 'NotATeamMember + :> CanThrow OperationDenied + :> ZLocalUser + :> "teams" + :> Capture "tid" TeamId + :> "legalhold" + :> "settings" + :> Get '[JSON] ViewLegalHoldService + ) + :<|> Named + "delete-legal-hold-settings" + ( Summary "Delete legal hold service settings" + :> CanThrow AuthenticationError + :> CanThrow OperationDenied + :> CanThrow 'NotATeamMember + :> CanThrow ('ActionDenied 'RemoveConversationMember) + :> CanThrow 'InvalidOperation + :> CanThrow 'LegalHoldNotEnabled + :> CanThrow 'LegalHoldDisableUnimplemented + :> CanThrow 'LegalHoldServiceNotRegistered + :> CanThrow 'UserLegalHoldIllegalOperation + :> CanThrow 'LegalHoldCouldNotBlockConnections + :> Description + "This endpoint can lead to the following events being sent:\n\ + \- ClientRemoved event to members with a legalhold client (via brig)\n\ + \- UserLegalHoldDisabled event to contacts of members with a legalhold client (via brig)" + :> ZLocalUser + :> "teams" + :> Capture "tid" TeamId + :> "legalhold" + :> "settings" + :> ReqBody '[JSON] RemoveLegalHoldSettingsRequest + :> MultiVerb1 'DELETE '[JSON] (RespondEmpty 204 "Legal hold service settings deleted") + ) + :<|> Named + "get-legal-hold" + ( Summary "Get legal hold status" + :> CanThrow 'TeamMemberNotFound + :> ZLocalUser + :> "teams" + :> Capture "tid" TeamId + :> "legalhold" + :> Capture "uid" UserId + :> Get '[JSON] UserLegalHoldStatusResponse + ) + :<|> Named + "consent-to-legal-hold" + ( Summary "Consent to legal hold" + :> CanThrow ('ActionDenied 'RemoveConversationMember) + :> CanThrow 'InvalidOperation + :> CanThrow 'TeamMemberNotFound + :> CanThrow 'UserLegalHoldIllegalOperation + :> CanThrow 'LegalHoldCouldNotBlockConnections + :> ZLocalUser + :> "teams" + :> Capture "tid" TeamId + :> "legalhold" + :> "consent" + :> MultiVerb 'POST '[JSON] GrantConsentResultResponseTypes GrantConsentResult + ) + :<|> Named + "request-legal-hold-device" + ( Summary "Request legal hold device" + :> CanThrow ('ActionDenied 'RemoveConversationMember) + :> CanThrow 'NotATeamMember + :> CanThrow OperationDenied + :> CanThrow 'TeamMemberNotFound + :> CanThrow 'LegalHoldNotEnabled + :> CanThrow 'UserLegalHoldAlreadyEnabled + :> CanThrow 'NoUserLegalHoldConsent + :> CanThrow 'LegalHoldServiceBadResponse + :> CanThrow 'LegalHoldServiceNotRegistered + :> CanThrow 'LegalHoldCouldNotBlockConnections + :> CanThrow 'UserLegalHoldIllegalOperation + :> Description + "This endpoint can lead to the following events being sent:\n\ + \- LegalHoldClientRequested event to contacts of the user the device is requested for, if they didn't already have a legalhold client (via brig)" + :> ZLocalUser + :> "teams" + :> Capture "tid" TeamId + :> "legalhold" + :> Capture "uid" UserId + :> MultiVerb + 'POST + '[JSON] + RequestDeviceResultResponseType + RequestDeviceResult + ) + :<|> Named + "disable-legal-hold-for-user" + ( Summary "Disable legal hold for user" + :> CanThrow AuthenticationError + :> CanThrow ('ActionDenied 'RemoveConversationMember) + :> CanThrow 'NotATeamMember + :> CanThrow OperationDenied + :> CanThrow 'LegalHoldServiceNotRegistered + :> CanThrow 'UserLegalHoldIllegalOperation + :> CanThrow 'LegalHoldCouldNotBlockConnections + :> Description + "This endpoint can lead to the following events being sent:\n\ + \- ClientRemoved event to the user owning the client (via brig)\n\ + \- UserLegalHoldDisabled event to contacts of the user owning the client (via brig)" + :> ZLocalUser + :> "teams" + :> Capture "tid" TeamId + :> "legalhold" + :> Capture "uid" UserId + :> ReqBody '[JSON] DisableLegalHoldForUserRequest + :> MultiVerb + 'DELETE + '[JSON] + DisableLegalHoldForUserResponseType + DisableLegalHoldForUserResponse + ) + :<|> Named + "approve-legal-hold-device" + ( Summary "Approve legal hold device" + :> CanThrow AuthenticationError + :> CanThrow 'AccessDenied + :> CanThrow ('ActionDenied 'RemoveConversationMember) + :> CanThrow 'NotATeamMember + :> CanThrow 'LegalHoldNotEnabled + :> CanThrow 'UserLegalHoldNotPending + :> CanThrow 'NoLegalHoldDeviceAllocated + :> CanThrow 'LegalHoldServiceNotRegistered + :> CanThrow 'UserLegalHoldAlreadyEnabled + :> CanThrow 'UserLegalHoldIllegalOperation + :> CanThrow 'LegalHoldCouldNotBlockConnections + :> Description + "This endpoint can lead to the following events being sent:\n\ + \- ClientAdded event to the user owning the client (via brig)\n\ + \- UserLegalHoldEnabled event to contacts of the user owning the client (via brig)\n\ + \- ClientRemoved event to the user, if removing old client due to max number (via brig)" + :> ZLocalUser + :> ZConn + :> "teams" + :> Capture "tid" TeamId + :> "legalhold" + :> Capture "uid" UserId + :> "approve" + :> ReqBody '[JSON] ApproveLegalHoldForUserRequest + :> MultiVerb1 'PUT '[JSON] (RespondEmpty 200 "Legal hold approved") + ) + +type RequestDeviceResultResponseType = + '[ RespondEmpty 201 "Request device successful", + RespondEmpty 204 "Request device already pending" + ] + +data RequestDeviceResult + = RequestDeviceSuccess + | RequestDeviceAlreadyPending + deriving (Generic) + deriving (AsUnion RequestDeviceResultResponseType) via GenericAsUnion RequestDeviceResultResponseType RequestDeviceResult + +instance GSOP.Generic RequestDeviceResult + +type DisableLegalHoldForUserResponseType = + '[ RespondEmpty 200 "Disable legal hold successful", + RespondEmpty 204 "Legal hold was not enabled" + ] + +data DisableLegalHoldForUserResponse + = DisableLegalHoldSuccess + | DisableLegalHoldWasNotEnabled + deriving (Generic) + deriving (AsUnion DisableLegalHoldForUserResponseType) via GenericAsUnion DisableLegalHoldForUserResponseType DisableLegalHoldForUserResponse + +instance GSOP.Generic DisableLegalHoldForUserResponse + +type GrantConsentResultResponseTypes = + '[ RespondEmpty 201 "Grant consent successful", + RespondEmpty 204 "Consent already granted" + ] + +data GrantConsentResult + = GrantConsentSuccess + | GrantConsentAlreadyGranted + deriving (Generic) + deriving (AsUnion GrantConsentResultResponseTypes) via GenericAsUnion GrantConsentResultResponseTypes GrantConsentResult + +instance GSOP.Generic GrantConsentResult diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs new file mode 100644 index 0000000000..03421544b0 --- /dev/null +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs @@ -0,0 +1,141 @@ +-- 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.Routes.Public.Galley.MLS where + +import Servant hiding (WithStatus) +import Servant.Swagger.Internal.Orphans () +import Wire.API.Error +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.Serialisation +import Wire.API.MLS.Servant +import Wire.API.MLS.Welcome +import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Named +import Wire.API.Routes.Public +import Wire.API.Routes.Version + +type MLSMessagingAPI = + Named + "mls-welcome-message" + ( Summary "Post an MLS welcome message" + :> CanThrow 'MLSKeyPackageRefNotFound + :> "welcome" + :> ZConn + :> ReqBody '[MLS] (RawMLS Welcome) + :> MultiVerb1 'POST '[JSON] (RespondEmpty 201 "Welcome message sent") + ) + :<|> Named + "mls-message-v1" + ( Summary "Post an MLS message" + :> Until '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 'MLSUnexpectedSenderClient + :> CanThrow 'MLSClientSenderUserMismatch + :> CanThrow 'MLSGroupConversationMismatch + :> CanThrow 'MLSMissingSenderClient + :> CanThrow 'MissingLegalholdConsent + :> CanThrow MLSProposalFailure + :> "messages" + :> ZOptClient + :> ZConn + :> ReqBody '[MLS] (RawMLS SomeMessage) + :> MultiVerb1 'POST '[JSON] (Respond 201 "Message sent" [Event]) + ) + :<|> Named + "mls-message" + ( Summary "Post an MLS message" + :> 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 'MLSUnexpectedSenderClient + :> CanThrow 'MLSClientSenderUserMismatch + :> CanThrow 'MLSGroupConversationMismatch + :> CanThrow 'MLSMissingSenderClient + :> CanThrow 'MissingLegalholdConsent + :> CanThrow MLSProposalFailure + :> "messages" + :> ZOptClient + :> ZConn + :> ReqBody '[MLS] (RawMLS SomeMessage) + :> MultiVerb1 'POST '[JSON] (Respond 201 "Message sent" MLSMessageSendingStatus) + ) + :<|> Named + "mls-commit-bundle" + ( Summary "Post a MLS CommitBundle" + :> From 'V3 + :> 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 'MLSUnexpectedSenderClient + :> CanThrow 'MLSClientSenderUserMismatch + :> CanThrow 'MLSGroupConversationMismatch + :> CanThrow 'MLSMissingSenderClient + :> CanThrow 'MLSWelcomeMismatch + :> CanThrow 'MissingLegalholdConsent + :> CanThrow MLSProposalFailure + :> "commit-bundles" + :> ZOptClient + :> ZConn + :> ReqBody '[CommitBundleMimeType] 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" + :> "public-keys" + :> MultiVerb1 'GET '[JSON] (Respond 200 "Public keys" MLSPublicKeys) + ) + +type MLSAPI = LiftNamed (ZLocalUser :> "mls" :> MLSMessagingAPI) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs new file mode 100644 index 0000000000..1e982f96e6 --- /dev/null +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs @@ -0,0 +1,197 @@ +-- 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.Routes.Public.Galley.Messaging where + +import Data.Id +import Data.SOP +import qualified Generics.SOP as GSOP +import Imports +import Servant hiding (WithStatus) +import Servant.Swagger.Internal.Orphans () +import Wire.API.Error +import qualified Wire.API.Error.Brig as BrigError +import Wire.API.Error.Galley +import Wire.API.Message +import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Named +import Wire.API.Routes.Public +import Wire.API.Routes.QualifiedCapture +import Wire.API.ServantProto + +type MessagingAPI = + Named + "post-otr-message-unqualified" + ( Summary "Post an encrypted message to a conversation (accepts JSON or Protobuf)" + :> Description PostOtrDescriptionUnqualified + :> ZLocalUser + :> ZConn + :> "conversations" + :> Capture "cnv" ConvId + :> "otr" + :> "messages" + :> QueryParam "ignore_missing" IgnoreMissing + :> QueryParam "report_missing" ReportMissing + :> ReqBody '[JSON, Proto] NewOtrMessage + :> MultiVerb + 'POST + '[Servant.JSON] + (PostOtrResponses ClientMismatch) + (PostOtrResponse ClientMismatch) + ) + :<|> Named + "post-otr-broadcast-unqualified" + ( Summary "Broadcast an encrypted message to all team members and all contacts (accepts JSON or Protobuf)" + :> Description PostOtrDescriptionUnqualified + :> ZLocalUser + :> ZConn + :> CanThrow 'TeamNotFound + :> CanThrow 'BroadcastLimitExceeded + :> CanThrow 'NonBindingTeam + :> "broadcast" + :> "otr" + :> "messages" + :> QueryParam "ignore_missing" IgnoreMissing + :> QueryParam "report_missing" ReportMissing + :> ReqBody '[JSON, Proto] NewOtrMessage + :> MultiVerb + 'POST + '[JSON] + (PostOtrResponses ClientMismatch) + (PostOtrResponse ClientMismatch) + ) + :<|> Named + "post-proteus-message" + ( Summary "Post an encrypted message to a conversation (accepts only Protobuf)" + :> Description PostOtrDescription + :> ZLocalUser + :> ZConn + :> "conversations" + :> QualifiedCapture "cnv" ConvId + :> "proteus" + :> "messages" + :> ReqBody '[Proto] (RawProto QualifiedNewOtrMessage) + :> MultiVerb + 'POST + '[Servant.JSON] + (PostOtrResponses MessageSendingStatus) + (Either (MessageNotSent MessageSendingStatus) MessageSendingStatus) + ) + :<|> Named + "post-proteus-broadcast" + ( Summary "Post an encrypted message to all team members and all contacts (accepts only Protobuf)" + :> Description PostOtrDescription + :> ZLocalUser + :> ZConn + :> CanThrow 'TeamNotFound + :> CanThrow 'BroadcastLimitExceeded + :> CanThrow 'NonBindingTeam + :> "broadcast" + :> "proteus" + :> "messages" + :> ReqBody '[Proto] QualifiedNewOtrMessage + :> MultiVerb + 'POST + '[JSON] + (PostOtrResponses MessageSendingStatus) + (Either (MessageNotSent MessageSendingStatus) MessageSendingStatus) + ) + +data MessageNotSent a + = MessageNotSentConversationNotFound + | MessageNotSentUnknownClient + | MessageNotSentLegalhold + | MessageNotSentClientMissing a + deriving stock (Eq, Show, Generic, Functor) + deriving + (AsUnion (MessageNotSentResponses a)) + via (GenericAsUnion (MessageNotSentResponses a) (MessageNotSent a)) + +instance GSOP.Generic (MessageNotSent a) + +type MessageNotSentResponses a = + '[ ErrorResponse 'ConvNotFound, + ErrorResponse 'BrigError.UnknownClient, + ErrorResponse 'BrigError.MissingLegalholdConsent, + Respond 412 "Missing clients" a + ] + +type PostOtrResponses a = + MessageNotSentResponses a + .++ '[Respond 201 "Message sent" a] + +type PostOtrResponse a = Either (MessageNotSent a) a + +instance + ( rs ~ (MessageNotSentResponses a .++ '[r]), + a ~ ResponseType r + ) => + AsUnion rs (PostOtrResponse a) + where + toUnion = + eitherToUnion + (toUnion @(MessageNotSentResponses a)) + (Z . I) + + fromUnion = + eitherFromUnion + (fromUnion @(MessageNotSentResponses a)) + (unI . unZ) + +-- This is a work-around for the fact that we sometimes want to send larger lists of user ids +-- in the filter query than fits the url length limit. For details, see +-- https://github.com/zinfra/backend-issues/issues/1248 +type PostOtrDescriptionUnqualified = + "This endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\n\ + \To override this, the endpoint accepts two query params:\n\ + \- `ignore_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n\ + \ - When 'true' all missing clients are ignored.\n\ + \ - When 'false' all missing clients are reported.\n\ + \ - When comma separated list of user-ids, only clients for listed users are ignored.\n\ + \- `report_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n\ + \ - When 'true' all missing clients are reported.\n\ + \ - When 'false' all missing clients are ignored.\n\ + \ - When comma separated list of user-ids, only clients for listed users are reported.\n\ + \\n\ + \Apart from these, the request body also accepts `report_missing` which can only be a list of user ids and behaves the same way as the query parameter.\n\ + \\n\ + \All three of these should be considered mutually exclusive. The server however does not error if more than one is specified, it reads them in this order of precedence:\n\ + \- `report_missing` in the request body has highest precedence.\n\ + \- `ignore_missing` in the query param is the next.\n\ + \- `report_missing` in the query param has the lowest precedence.\n\ + \\n\ + \This endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\ + \\n\ + \**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto." + +type PostOtrDescription = + "This endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\n\ + \To override this, the endpoint accepts `client_mismatch_strategy` in the body. It can have these values:\n\ + \- `report_all`: When set, the message is not sent if any clients are missing. The missing clients are reported in the response.\n\ + \- `ignore_all`: When set, no checks about missing clients are carried out.\n\ + \- `report_only`: Takes a list of qualified UserIDs. If any clients of the listed users are missing, the message is not sent. The missing clients are reported in the response.\n\ + \- `ignore_only`: Takes a list of qualified UserIDs. If any clients of the non-listed users are missing, the message is not sent. The missing clients are reported in the response.\n\ + \\n\ + \The sending of messages in a federated conversation could theoretically fail partially. \ + \To make this case unlikely, the backend first gets a list of clients from all the involved backends and then tries to send a message. \ + \So, if any backend is down, the message is not propagated to anyone. \ + \But the actual message fan out to multiple backends could still fail partially. This type of failure is reported as a 201, \ + \the clients for which the message sending failed are part of the response body.\n\ + \\n\ + \This endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\ + \\n\ + \**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto." diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Team.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Team.hs new file mode 100644 index 0000000000..4a8f3924f0 --- /dev/null +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Team.hs @@ -0,0 +1,101 @@ +-- 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.Routes.Public.Galley.Team where + +import Data.Id +import Imports +import Servant hiding (WithStatus) +import Servant.Swagger.Internal.Orphans () +import Wire.API.Error +import Wire.API.Error.Galley +import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Named +import Wire.API.Routes.Public +import Wire.API.Team +import Wire.API.Team.Permission + +type TeamAPI = + Named + "create-non-binding-team" + ( Summary "Create a new non binding team" + -- FUTUREWORK: deprecated in https://github.com/wireapp/wire-server/pull/2607 + :> ZLocalUser + :> ZConn + :> CanThrow 'NotConnected + :> CanThrow 'UserBindingExists + :> "teams" + :> ReqBody '[Servant.JSON] NonBindingNewTeam + :> MultiVerb + 'POST + '[JSON] + '[ WithHeaders + '[DescHeader "Location" "Team ID" TeamId] + TeamId + (RespondEmpty 201 "Team ID as `Location` header value") + ] + TeamId + ) + :<|> Named + "update-team" + ( Summary "Update team properties" + :> ZUser + :> ZConn + :> CanThrow 'NotATeamMember + :> CanThrow ('MissingPermission ('Just 'SetTeamData)) + :> "teams" + :> Capture "tid" TeamId + :> ReqBody '[JSON] TeamUpdateData + :> MultiVerb + 'PUT + '[JSON] + '[RespondEmpty 200 "Team updated"] + () + ) + :<|> Named + "get-teams" + ( Summary "Get teams (deprecated); use `GET /teams/:tid`" + -- FUTUREWORK: deprecated in https://github.com/wireapp/wire-server/pull/2607 + :> ZUser + :> "teams" + :> Get '[JSON] TeamList + ) + :<|> Named + "get-team" + ( Summary "Get a team by ID" + :> ZUser + :> CanThrow 'TeamNotFound + :> "teams" + :> Capture "tid" TeamId + :> Get '[JSON] Team + ) + :<|> Named + "delete-team" + ( Summary "Delete a team" + :> ZUser + :> ZConn + :> CanThrow 'TeamNotFound + :> CanThrow ('MissingPermission ('Just 'DeleteTeam)) + :> CanThrow 'NotATeamMember + :> CanThrow OperationDenied + :> CanThrow 'DeleteQueueFull + :> CanThrow AuthenticationError + :> "teams" + :> Capture "tid" TeamId + :> ReqBody '[Servant.JSON] TeamDeleteData + :> MultiVerb 'DELETE '[JSON] '[RespondEmpty 202 "Team is scheduled for removal"] () + ) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs new file mode 100644 index 0000000000..ce00269f8a --- /dev/null +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs @@ -0,0 +1,81 @@ +-- 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.Routes.Public.Galley.TeamConversation where + +import Data.Id +import Servant hiding (WithStatus) +import Servant.Swagger.Internal.Orphans () +import Wire.API.Conversation.Role +import Wire.API.Error +import Wire.API.Error.Galley +import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Named +import Wire.API.Routes.Public +import Wire.API.Team.Conversation + +type TeamConversationAPI = + Named + "get-team-conversation-roles" + ( Summary "Get existing roles available for the given team" + :> CanThrow 'NotATeamMember + :> ZUser + :> "teams" + :> Capture "tid" TeamId + :> "conversations" + :> "roles" + :> Get '[Servant.JSON] ConversationRolesList + ) + :<|> Named + "get-team-conversations" + ( Summary "Get team conversations" + :> CanThrow OperationDenied + :> CanThrow 'NotATeamMember + :> ZUser + :> "teams" + :> Capture "tid" TeamId + :> "conversations" + :> Get '[Servant.JSON] TeamConversationList + ) + :<|> Named + "get-team-conversation" + ( Summary "Get one team conversation" + :> CanThrow 'ConvNotFound + :> CanThrow OperationDenied + :> CanThrow 'NotATeamMember + :> ZUser + :> "teams" + :> Capture "tid" TeamId + :> "conversations" + :> Capture "cid" ConvId + :> Get '[Servant.JSON] TeamConversation + ) + :<|> Named + "delete-team-conversation" + ( Summary "Remove a team conversation" + :> CanThrow ('ActionDenied 'DeleteConversation) + :> CanThrow 'ConvNotFound + :> CanThrow 'InvalidOperation + :> CanThrow 'NotATeamMember + :> ZLocalUser + :> ZConn + :> "teams" + :> Capture "tid" TeamId + :> "conversations" + :> Capture "cid" ConvId + :> MultiVerb 'DELETE '[JSON] '[RespondEmpty 200 "Conversation deleted"] () + ) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamMember.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamMember.hs new file mode 100644 index 0000000000..7f9e99dcaa --- /dev/null +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamMember.hs @@ -0,0 +1,219 @@ +-- 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.Routes.Public.Galley.TeamMember where + +import Data.Id +import Data.Int +import Data.Range +import GHC.Generics +import qualified Generics.SOP as GSOP +import Servant hiding (WithStatus) +import Servant.Swagger.Internal.Orphans () +import Wire.API.Error +import Wire.API.Error.Galley +import Wire.API.Routes.CSV +import Wire.API.Routes.LowLevelStream +import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Named +import Wire.API.Routes.Public +import Wire.API.Team.Member +import qualified Wire.API.User as User + +type TeamMemberAPI = + Named + "get-team-members" + ( Summary "Get team members" + :> CanThrow 'NotATeamMember + :> ZLocalUser + :> "teams" + :> Capture "tid" TeamId + :> "members" + :> QueryParam' + [ Optional, + Strict, + Description "Maximum results to be returned" + ] + "maxResults" + (Range 1 HardTruncationLimit Int32) + :> QueryParam' + [ Optional, + Strict, + Description + "Optional, when not specified, the first page will be returned.\ + \Every returned page contains a `pagingState`, this should be supplied to retrieve the next page." + ] + "pagingState" + TeamMembersPagingState + :> Get '[JSON] TeamMembersPage + ) + :<|> Named + "get-team-member" + ( Summary "Get single team member" + :> CanThrow 'NotATeamMember + :> CanThrow 'TeamMemberNotFound + :> ZLocalUser + :> "teams" + :> Capture "tid" TeamId + :> "members" + :> Capture "uid" UserId + :> Get '[JSON] TeamMemberOptPerms + ) + :<|> Named + "get-team-members-by-ids" + ( Summary "Get team members by user id list" + :> Description "The `has_more` field in the response body is always `false`." + :> CanThrow 'NotATeamMember + :> CanThrow 'BulkGetMemberLimitExceeded + :> ZLocalUser + :> "teams" + :> Capture "tid" TeamId + :> "get-members-by-ids-using-post" + :> QueryParam' + [ Optional, + Strict, + Description "Maximum results to be returned" + ] + "maxResults" + (Range 1 HardTruncationLimit Int32) + :> ReqBody '[JSON] User.UserIdList + :> Post '[JSON] TeamMemberListOptPerms + ) + :<|> Named + "add-team-member" + ( Summary "Add a new team member" + -- FUTUREWORK: deprecated in https://github.com/wireapp/wire-server/pull/2607 + :> CanThrow 'InvalidPermissions + :> CanThrow 'NoAddToBinding + :> CanThrow 'NotATeamMember + :> CanThrow 'NotConnected + :> CanThrow OperationDenied + :> CanThrow 'TeamNotFound + :> CanThrow 'TooManyTeamMembers + :> CanThrow 'UserBindingExists + :> CanThrow 'TooManyTeamMembersOnTeamWithLegalhold + :> ZLocalUser + :> ZConn + :> "teams" + :> Capture "tid" TeamId + :> "members" + :> ReqBody '[JSON] NewTeamMember + :> MultiVerb1 + 'POST + '[JSON] + (RespondEmpty 200 "") + ) + :<|> Named + "delete-team-member" + ( Summary "Remove an existing team member" + :> CanThrow AuthenticationError + :> CanThrow 'AccessDenied + :> CanThrow 'TeamMemberNotFound + :> CanThrow 'TeamNotFound + :> CanThrow 'NotATeamMember + :> CanThrow OperationDenied + :> ZLocalUser + :> ZConn + :> "teams" + :> Capture "tid" TeamId + :> "members" + :> Capture "uid" UserId + :> ReqBody '[JSON] TeamMemberDeleteData + :> MultiVerb + 'DELETE + '[JSON] + TeamMemberDeleteResultResponseType + TeamMemberDeleteResult + ) + :<|> Named + "delete-non-binding-team-member" + ( Summary "Remove an existing team member" + -- FUTUREWORK: deprecated in https://github.com/wireapp/wire-server/pull/2607 + :> CanThrow AuthenticationError + :> CanThrow 'AccessDenied + :> CanThrow 'TeamMemberNotFound + :> CanThrow 'TeamNotFound + :> CanThrow 'NotATeamMember + :> CanThrow OperationDenied + :> ZLocalUser + :> ZConn + :> "teams" + :> Capture "tid" TeamId + :> "members" + :> Capture "uid" UserId + :> MultiVerb + 'DELETE + '[JSON] + TeamMemberDeleteResultResponseType + TeamMemberDeleteResult + ) + :<|> Named + "update-team-member" + ( Summary "Update an existing team member" + :> CanThrow 'AccessDenied + :> CanThrow 'InvalidPermissions + :> CanThrow 'TeamNotFound + :> CanThrow 'TeamMemberNotFound + :> CanThrow 'NotATeamMember + :> CanThrow OperationDenied + :> ZLocalUser + :> ZConn + :> "teams" + :> Capture "tid" TeamId + :> "members" + :> ReqBody '[JSON] NewTeamMember + :> MultiVerb1 + 'PUT + '[JSON] + (RespondEmpty 200 "") + ) + :<|> Named + "get-team-members-csv" + ( Summary "Get all members of the team as a CSV file" + :> CanThrow 'AccessDenied + :> Description + "The endpoint returns data in chunked transfer encoding.\ + \ Internal server errors might result in a failed transfer\ + \ instead of a 500 response." + :> ZLocalUser + :> "teams" + :> Capture "tid" TeamId + :> "members" + :> "csv" + :> LowLevelStream + 'GET + 200 + '[ '( "Content-Disposition", + "attachment; filename=\"wire_team_members.csv\"" + ) + ] + "CSV of team members" + CSV + ) + +type TeamMemberDeleteResultResponseType = + '[ RespondEmpty 202 "Team member scheduled for deletion", + RespondEmpty 200 "" + ] + +data TeamMemberDeleteResult + = TeamMemberDeleteAccepted + | TeamMemberDeleteCompleted + deriving (Generic) + deriving (AsUnion TeamMemberDeleteResultResponseType) via GenericAsUnion TeamMemberDeleteResultResponseType TeamMemberDeleteResult + +instance GSOP.Generic TeamMemberDeleteResult diff --git a/libs/wire-api/src/Wire/API/Swagger.hs b/libs/wire-api/src/Wire/API/Swagger.hs index 9a133e98ea..3b757e1548 100644 --- a/libs/wire-api/src/Wire/API/Swagger.hs +++ b/libs/wire-api/src/Wire/API/Swagger.hs @@ -18,7 +18,6 @@ module Wire.API.Swagger where import Data.Swagger.Build.Api (Model) -import qualified Wire.API.Call.Config as Call.Config import qualified Wire.API.Connection as Connection import qualified Wire.API.Conversation as Conversation import qualified Wire.API.Conversation.Code as Conversation.Code @@ -32,7 +31,6 @@ import qualified Wire.API.Properties as Properties import qualified Wire.API.Provider.Service as Provider.Service import qualified Wire.API.Team as Team 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.Client as User.Client @@ -44,9 +42,7 @@ import qualified Wire.API.User.Search as User.Search models :: [Model] models = - [ Call.Config.modelRtcConfiguration, - Call.Config.modelRtcIceServer, - Connection.modelConnectionList, + [ Connection.modelConnectionList, Connection.modelConnection, Connection.modelConnectionUpdate, Conversation.modelConversation, @@ -88,9 +84,6 @@ models = Team.modelTeamDelete, Team.Conversation.modelTeamConversation, Team.Conversation.modelTeamConversationList, - Team.Invitation.modelTeamInvitation, - Team.Invitation.modelTeamInvitationList, - Team.Invitation.modelTeamInvitationRequest, Team.Permission.modelPermissions, User.modelUserIdList, User.modelUser, diff --git a/libs/wire-api/src/Wire/API/Team/Invitation.hs b/libs/wire-api/src/Wire/API/Team/Invitation.hs index efcc60de35..20eea89c53 100644 --- a/libs/wire-api/src/Wire/API/Team/Invitation.hs +++ b/libs/wire-api/src/Wire/API/Team/Invitation.hs @@ -21,21 +21,28 @@ module Wire.API.Team.Invitation ( InvitationRequest (..), Invitation (..), InvitationList (..), - - -- * Swagger - modelTeamInvitation, - modelTeamInvitationList, - modelTeamInvitationRequest, + InvitationLocation (..), + HeadInvitationByEmailResult (..), + HeadInvitationsResponses, ) where -import Data.Aeson +import Control.Lens ((?~)) +import qualified Data.Aeson as A +import Data.ByteString.Conversion import Data.Id import Data.Json.Util -import qualified Data.Swagger.Build.Api as Doc +import Data.SOP +import Data.Schema +import qualified Data.Swagger as S +import qualified Data.Text.Encoding as TE import Imports +import Servant (FromHttpApiData (..), ToHttpApiData (..)) import URI.ByteString -import Wire.API.Team.Role (Role, defaultRole, typeRole) +import Wire.API.Error +import Wire.API.Error.Brig +import Wire.API.Routes.MultiVerb +import Wire.API.Team.Role (Role, defaultRole) import Wire.API.User.Identity (Email, Phone) import Wire.API.User.Profile (Locale, Name) import Wire.Arbitrary (Arbitrary, GenericUniform (..)) @@ -52,45 +59,22 @@ data InvitationRequest = InvitationRequest } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform InvitationRequest) - -modelTeamInvitationRequest :: Doc.Model -modelTeamInvitationRequest = Doc.defineModel "TeamInvitationRequest" $ do - Doc.description "A request to join a team on Wire." - Doc.property "locale" Doc.string' $ do - Doc.description "Locale to use for the invitation." - Doc.optional - Doc.property "role" typeRole $ do - Doc.description "Role of the invitee (invited user)." - Doc.optional - Doc.property "name" Doc.string' $ do - Doc.description "Name of the invitee (1 - 128 characters)." - Doc.optional - Doc.property "email" Doc.string' $ - Doc.description "Email of the invitee." - Doc.property "phone" Doc.string' $ do - Doc.description "Phone number of the invitee, in the E.164 format." - Doc.optional - Doc.property "inviter_name" Doc.string' $ - Doc.description "DEPRECATED - WILL BE IGNORED IN FAVOR OF REQ AUTH DATA - Name of the inviter (1 - 128 characters)." - -instance ToJSON InvitationRequest where - toJSON i = - object - [ "locale" .= irLocale i, - "role" .= irRole i, - "name" .= irInviteeName i, - "email" .= irInviteeEmail i, - "phone" .= irInviteePhone i - ] - -instance FromJSON InvitationRequest where - parseJSON = withObject "invitation-request" $ \o -> - InvitationRequest - <$> o .:? "locale" - <*> o .:? "role" - <*> o .:? "name" - <*> o .: "email" - <*> o .:? "phone" + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via (Schema InvitationRequest) + +instance ToSchema InvitationRequest where + schema = + objectWithDocModifier "InvitationRequest" (description ?~ "A request to join a team on Wire.") $ + InvitationRequest + <$> irLocale + .= optFieldWithDocModifier "locale" (description ?~ "Locale to use for the invitation.") (maybeWithDefault A.Null schema) + <*> irRole + .= optFieldWithDocModifier "role" (description ?~ "Role of the invitee (invited user).") (maybeWithDefault A.Null schema) + <*> irInviteeName + .= optFieldWithDocModifier "name" (description ?~ "Name of the invitee (1 - 128 characters).") (maybeWithDefault A.Null schema) + <*> irInviteeEmail + .= fieldWithDocModifier "email" (description ?~ "Email of the invitee.") schema + <*> irInviteePhone + .= optFieldWithDocModifier "phone" (description ?~ "Phone number of the invitee, in the E.164 format.") (maybeWithDefault A.Null schema) -------------------------------------------------------------------------------- -- Invitation @@ -110,63 +94,73 @@ data Invitation = Invitation } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform Invitation) + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via (Schema Invitation) + +instance ToSchema Invitation where + schema = + objectWithDocModifier "Invitation" (description ?~ "An invitation to join a team on Wire") $ + Invitation + <$> inTeam + .= fieldWithDocModifier "team" (description ?~ "Team ID of the inviting team") schema + <*> inRole + -- clients, when leaving "role" empty, can leave the default role choice to us + .= (fromMaybe defaultRole <$> optFieldWithDocModifier "role" (description ?~ "Role of the invited user") schema) + <*> inInvitation + .= fieldWithDocModifier "id" (description ?~ "UUID used to refer the invitation") schema + <*> inCreatedAt + .= fieldWithDocModifier "created_at" (description ?~ "Timestamp of invitation creation") schema + <*> inCreatedBy + .= optFieldWithDocModifier "created_by" (description ?~ "ID of the inviting user") (maybeWithDefault A.Null schema) + <*> inInviteeEmail + .= fieldWithDocModifier "email" (description ?~ "Email of the invitee") schema + <*> inInviteeName + .= optFieldWithDocModifier "name" (description ?~ "Name of the invitee (1 - 128 characters)") (maybeWithDefault A.Null schema) + <*> inInviteePhone + .= optFieldWithDocModifier "phone" (description ?~ "Phone number of the invitee, in the E.164 format") (maybeWithDefault A.Null schema) + <*> (fmap (TE.decodeUtf8 . serializeURIRef') . inInviteeUrl) + .= optFieldWithDocModifier "url" (description ?~ "URL of the invitation link to be sent to the invitee") (maybeWithDefault A.Null urlSchema) + where + urlSchema = parsedText "URIRef Absolute" (runParser (uriParser strictURIParserOptions) . TE.encodeUtf8) + +newtype InvitationLocation = InvitationLocation + { unInvitationLocation :: ByteString + } + deriving stock (Eq, Show, Generic) --- | (This is *not* the swagger model for the 'TeamInvitation' type (which does not exist), --- but for the use of 'Invitation' under @/teams/{tid}/invitations@.) -modelTeamInvitation :: Doc.Model -modelTeamInvitation = Doc.defineModel "TeamInvitation" $ do - Doc.description "An invitation to join a team on Wire" - Doc.property "team" Doc.bytes' $ - Doc.description "Team ID of the inviting team" - Doc.property "role" typeRole $ do - Doc.description "Role of the invited user" - Doc.optional - Doc.property "id" Doc.bytes' $ - Doc.description "UUID used to refer the invitation" - Doc.property "created_at" Doc.dateTime' $ - Doc.description "Timestamp of invitation creation" - Doc.property "created_by" Doc.bytes' $ do - Doc.description "ID of the inviting user" - Doc.optional - Doc.property "email" Doc.string' $ - Doc.description "Email of the invitee" - Doc.property "name" Doc.string' $ do - Doc.description "Name of the invitee (1 - 128 characters)" - Doc.optional - 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 = - object - [ "team" .= inTeam i, - "role" .= inRole i, - "id" .= inInvitation i, - "created_at" .= inCreatedAt i, - "created_by" .= inCreatedBy i, - "email" .= inInviteeEmail i, - "name" .= inInviteeName i, - "phone" .= inInviteePhone i, - "url" .= inInviteeUrl i - ] - -instance FromJSON Invitation where - parseJSON = withObject "invitation" $ \o -> - Invitation - <$> o .: "team" - -- clients, when leaving "role" empty, can leave the default role choice to us - <*> o .:? "role" .!= defaultRole - <*> o .: "id" - <*> o .: "created_at" - <*> o .:? "created_by" - <*> o .: "email" - <*> o .:? "name" - <*> o .:? "phone" - <*> o .:? "url" +instance S.ToParamSchema InvitationLocation where + toParamSchema _ = + mempty + & S.type_ ?~ S.SwaggerString + & S.format ?~ "url" + +instance FromHttpApiData InvitationLocation where + parseUrlPiece = parseHeader . TE.encodeUtf8 + parseHeader = pure . InvitationLocation + +instance ToHttpApiData InvitationLocation where + toUrlPiece = TE.decodeUtf8 . toHeader + toHeader = unInvitationLocation + +data HeadInvitationByEmailResult + = InvitationByEmail + | InvitationByEmailNotFound + | InvitationByEmailMoreThanOne + +type HeadInvitationsResponses = + '[ ErrorResponse 'PendingInvitationNotFound, + ErrorResponse 'ConflictingInvitations, + RespondEmpty 200 "Pending invitation exists." + ] + +instance AsUnion HeadInvitationsResponses HeadInvitationByEmailResult where + toUnion InvitationByEmailNotFound = Z (I (dynError @(MapError 'PendingInvitationNotFound))) + toUnion InvitationByEmailMoreThanOne = S (Z (I (dynError @(MapError 'ConflictingInvitations)))) + toUnion InvitationByEmail = S (S (Z (I ()))) + + fromUnion (Z (I _)) = InvitationByEmailNotFound + fromUnion (S (Z (I _))) = InvitationByEmailMoreThanOne + fromUnion (S (S (Z (I ())))) = InvitationByEmail + fromUnion (S (S (S x))) = case x of {} -------------------------------------------------------------------------------- -- InvitationList @@ -177,23 +171,13 @@ data InvitationList = InvitationList } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform InvitationList) - -modelTeamInvitationList :: Doc.Model -modelTeamInvitationList = Doc.defineModel "TeamInvitationList" $ do - Doc.description "A list of sent team invitations." - Doc.property "invitations" (Doc.unique $ Doc.array (Doc.ref modelTeamInvitation)) Doc.end - Doc.property "has_more" Doc.bool' $ - Doc.description "Indicator that the server has more invitations than returned." - -instance ToJSON InvitationList where - toJSON (InvitationList l m) = - object - [ "invitations" .= l, - "has_more" .= m - ] - -instance FromJSON InvitationList where - parseJSON = withObject "InvitationList" $ \o -> - InvitationList - <$> o .: "invitations" - <*> o .: "has_more" + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via (Schema InvitationList) + +instance ToSchema InvitationList where + schema = + objectWithDocModifier "InvitationList" (description ?~ "A list of sent team invitations.") $ + InvitationList + <$> ilInvitations + .= field "invitations" (array schema) + <*> ilHasMore + .= fieldWithDocModifier "has_more" (description ?~ "Indicator that the server has more invitations than returned.") schema diff --git a/libs/wire-api/src/Wire/API/Team/Role.hs b/libs/wire-api/src/Wire/API/Team/Role.hs index 2879766d4b..c79f191fa6 100644 --- a/libs/wire-api/src/Wire/API/Team/Role.hs +++ b/libs/wire-api/src/Wire/API/Team/Role.hs @@ -20,9 +20,6 @@ module Wire.API.Team.Role ( Role (..), defaultRole, - - -- * Swagger - typeRole, ) where @@ -34,7 +31,6 @@ import Data.Attoparsec.ByteString.Char8 (string) import Data.ByteString.Conversion (FromByteString (..), ToByteString (..)) import Data.Schema import qualified Data.Swagger as S -import qualified Data.Swagger.Model.Api as Doc import qualified Data.Text as T import Imports import Servant.API (FromHttpApiData, parseQueryParam) @@ -101,17 +97,6 @@ instance FromHttpApiData Role where flip foldMap [minBound .. maxBound] $ \s -> guard (T.pack (show s) == name) $> s -typeRole :: Doc.DataType -typeRole = - Doc.Prim $ - Doc.Primitive - { Doc.primType = Doc.PrimString, - Doc.defaultValue = Just defaultRole, - Doc.enum = Just [minBound ..], - Doc.minVal = Just minBound, - Doc.maxVal = Just maxBound - } - roleName :: IsString a => Role -> a roleName RoleOwner = "owner" roleName RoleAdmin = "admin" diff --git a/libs/wire-api/src/Wire/API/Team/Size.hs b/libs/wire-api/src/Wire/API/Team/Size.hs index 44cad608ba..75768861cd 100644 --- a/libs/wire-api/src/Wire/API/Team/Size.hs +++ b/libs/wire-api/src/Wire/API/Team/Size.hs @@ -17,27 +17,24 @@ module Wire.API.Team.Size ( TeamSize (TeamSize), - modelTeamSize, ) where -import Data.Aeson -import qualified Data.Swagger.Build.Api as Doc +import Control.Lens ((?~)) +import qualified Data.Aeson as A +import Data.Schema +import qualified Data.Swagger as S import Imports import Numeric.Natural newtype TeamSize = TeamSize Natural deriving (Show, Eq) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema TeamSize) -instance ToJSON TeamSize where - toJSON (TeamSize s) = object ["teamSize" .= s] - -instance FromJSON TeamSize where - parseJSON = - withObject "TeamSize" $ \o -> TeamSize <$> o .: "teamSize" - -modelTeamSize :: Doc.Model -modelTeamSize = Doc.defineModel "TeamSize" $ do - Doc.description "A simple object with a total number of team members." - Doc.property "teamSize" Doc.int32' $ do - Doc.description "Team size." +instance ToSchema TeamSize where + schema = + objectWithDocModifier "TeamSize" (description ?~ "A simple object with a total number of team members.") $ + TeamSize <$> (unTeamSize .= fieldWithDocModifier "teamSize" (description ?~ "Team size.") schema) + where + unTeamSize :: TeamSize -> Natural + unTeamSize (TeamSize n) = n diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index c9eeca6cd0..08af13b115 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -145,7 +145,7 @@ import Data.String.Conversions (cs) import qualified Data.Swagger as S import qualified Data.Swagger.Build.Api as Doc import qualified Data.Text as T -import Data.Text.Ascii (AsciiBase64Url) +import Data.Text.Ascii import qualified Data.Text.Encoding as T import Data.UUID (UUID, nil) import qualified Data.UUID as UUID @@ -165,6 +165,7 @@ import qualified Wire.API.Error.Brig as E import Wire.API.Provider.Service (ServiceRef, modelServiceRef) import Wire.API.Routes.MultiVerb import Wire.API.Team (BindingNewTeam, bindingNewTeamObjectSchema) +import Wire.API.Team.Role import Wire.API.User.Activation (ActivationCode) import Wire.API.User.Auth (CookieLabel) import Wire.API.User.Identity @@ -189,7 +190,8 @@ instance ToSchema UserIdList where schema = object "UserIdList" $ UserIdList - <$> mUsers .= field "user_ids" (array schema) + <$> mUsers + .= field "user_ids" (array schema) modelUserIdList :: Doc.Model modelUserIdList = Doc.defineModel "UserIdList" $ do @@ -209,8 +211,10 @@ instance ToSchema QualifiedUserIdList where schema = object "QualifiedUserIdList" $ QualifiedUserIdList - <$> qualifiedUserIdList .= field "qualified_user_ids" (array schema) - <* (fmap qUnqualified . qualifiedUserIdList) .= field "user_ids" (deprecatedSchema "qualified_user_ids" (array schema)) + <$> qualifiedUserIdList + .= field "qualified_user_ids" (array schema) + <* (fmap qUnqualified . qualifiedUserIdList) + .= field "user_ids" (deprecatedSchema "qualified_user_ids" (array schema)) -------------------------------------------------------------------------------- -- LimitedQualifiedUserIdList @@ -263,21 +267,32 @@ instance ToSchema UserProfile where schema = object "UserProfile" $ UserProfile - <$> profileQualifiedId .= field "qualified_id" schema + <$> profileQualifiedId + .= field "qualified_id" schema <* (qUnqualified . profileQualifiedId) .= optional (field "id" (deprecatedSchema "qualified_id" schema)) - <*> profileName .= field "name" schema - <*> profilePict .= (field "picture" schema <|> pure noPict) - <*> profileAssets .= (field "assets" (array schema) <|> pure []) - <*> profileAccentId .= field "accent_id" schema + <*> profileName + .= field "name" schema + <*> profilePict + .= (field "picture" schema <|> pure noPict) + <*> profileAssets + .= (field "assets" (array schema) <|> pure []) + <*> profileAccentId + .= field "accent_id" schema <*> ((\del -> if del then Just True else Nothing) . profileDeleted) .= maybe_ (fromMaybe False <$> optField "deleted" schema) - <*> profileService .= maybe_ (optField "service" schema) - <*> profileHandle .= maybe_ (optField "handle" schema) - <*> profileExpire .= maybe_ (optField "expires_at" schema) - <*> profileTeam .= maybe_ (optField "team" schema) - <*> profileEmail .= maybe_ (optField "email" schema) - <*> profileLegalholdStatus .= field "legalhold_status" schema + <*> profileService + .= maybe_ (optField "service" schema) + <*> profileHandle + .= maybe_ (optField "handle" schema) + <*> profileExpire + .= maybe_ (optField "expires_at" schema) + <*> profileTeam + .= maybe_ (optField "team" schema) + <*> profileEmail + .= maybe_ (optField "email" schema) + <*> profileLegalholdStatus + .= field "legalhold_status" schema modelUser :: Doc.Model modelUser = Doc.defineModel "User" $ do @@ -371,20 +386,33 @@ instance ToSchema User where userObjectSchema :: ObjectSchema SwaggerDoc User userObjectSchema = User - <$> userId .= field "id" schema - <*> userQualifiedId .= field "qualified_id" schema - <*> userIdentity .= maybeUserIdentityObjectSchema - <*> userDisplayName .= field "name" schema - <*> userPict .= (fromMaybe noPict <$> optField "picture" schema) - <*> userAssets .= (fromMaybe [] <$> optField "assets" (array schema)) - <*> userAccentId .= field "accent_id" schema + <$> userId + .= field "id" schema + <*> userQualifiedId + .= field "qualified_id" schema + <*> userIdentity + .= maybeUserIdentityObjectSchema + <*> userDisplayName + .= field "name" schema + <*> userPict + .= (fromMaybe noPict <$> optField "picture" schema) + <*> userAssets + .= (fromMaybe [] <$> optField "assets" (array schema)) + <*> userAccentId + .= field "accent_id" schema <*> (fromMaybe False <$> (\u -> if userDeleted u then Just True else Nothing) .= maybe_ (optField "deleted" schema)) - <*> userLocale .= field "locale" schema - <*> userService .= maybe_ (optField "service" schema) - <*> userHandle .= maybe_ (optField "handle" schema) - <*> userExpire .= maybe_ (optField "expires_at" schema) - <*> userTeam .= maybe_ (optField "team" schema) - <*> userManagedBy .= (fromMaybe ManagedByWire <$> optField "managed_by" schema) + <*> userLocale + .= field "locale" schema + <*> userService + .= maybe_ (optField "service" schema) + <*> userHandle + .= maybe_ (optField "handle" schema) + <*> userExpire + .= maybe_ (optField "expires_at" schema) + <*> userTeam + .= maybe_ (optField "team" schema) + <*> userManagedBy + .= (fromMaybe ManagedByWire <$> optField "managed_by" schema) userEmail :: User -> Maybe Email userEmail = emailIdentity <=< userIdentity @@ -662,7 +690,8 @@ data NewUserSpar = NewUserSpar newUserSparManagedBy :: ManagedBy, newUserSparHandle :: Maybe Handle, newUserSparRichInfo :: Maybe RichInfo, - newUserSparLocale :: Maybe Locale + newUserSparLocale :: Maybe Locale, + newUserSparRole :: Role } deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON, S.ToSchema) via (Schema NewUserSpar) @@ -671,14 +700,24 @@ instance ToSchema NewUserSpar where schema = object "NewUserSpar" $ NewUserSpar - <$> newUserSparUUID .= field "newUserSparUUID" genericToSchema - <*> newUserSparSSOId .= field "newUserSparSSOId" genericToSchema - <*> newUserSparDisplayName .= field "newUserSparDisplayName" schema - <*> newUserSparTeamId .= field "newUserSparTeamId" schema - <*> newUserSparManagedBy .= field "newUserSparManagedBy" schema - <*> newUserSparHandle .= maybe_ (optField "newUserSparHandle" schema) - <*> newUserSparRichInfo .= maybe_ (optField "newUserSparRichInfo" schema) - <*> newUserSparLocale .= maybe_ (optField "newUserSparLocale" schema) + <$> newUserSparUUID + .= field "newUserSparUUID" genericToSchema + <*> newUserSparSSOId + .= field "newUserSparSSOId" genericToSchema + <*> newUserSparDisplayName + .= field "newUserSparDisplayName" schema + <*> newUserSparTeamId + .= field "newUserSparTeamId" schema + <*> newUserSparManagedBy + .= field "newUserSparManagedBy" schema + <*> newUserSparHandle + .= maybe_ (optField "newUserSparHandle" schema) + <*> newUserSparRichInfo + .= maybe_ (optField "newUserSparRichInfo" schema) + <*> newUserSparLocale + .= maybe_ (optField "newUserSparLocale" schema) + <*> newUserSparRole + .= field "newUserSparRole" schema newUserFromSpar :: NewUserSpar -> NewUser newUserFromSpar new = @@ -769,25 +808,44 @@ data NewUserRaw = NewUserRaw newUserRawObjectSchema :: ObjectSchema SwaggerDoc NewUserRaw newUserRawObjectSchema = NewUserRaw - <$> newUserRawDisplayName .= field "name" schema - <*> newUserRawUUID .= maybe_ (optField "uuid" genericToSchema) - <*> newUserRawEmail .= maybe_ (optField "email" schema) - <*> newUserRawPhone .= maybe_ (optField "phone" schema) - <*> newUserRawSSOId .= maybe_ (optField "sso_id" genericToSchema) - <*> newUserRawPict .= maybe_ (optField "picture" schema) - <*> newUserRawAssets .= (fromMaybe [] <$> optField "assets" (array schema)) - <*> newUserRawAccentId .= maybe_ (optField "accent_id" schema) - <*> newUserRawEmailCode .= maybe_ (optField "email_code" schema) - <*> newUserRawPhoneCode .= maybe_ (optField "phone_code" schema) - <*> newUserRawInvitationCode .= maybe_ (optField "invitation_code" schema) - <*> newUserRawTeamCode .= maybe_ (optField "team_code" schema) - <*> newUserRawTeam .= maybe_ (optField "team" schema) - <*> newUserRawTeamId .= maybe_ (optField "team_id" schema) - <*> newUserRawLabel .= maybe_ (optField "label" schema) - <*> newUserRawLocale .= maybe_ (optField "locale" schema) - <*> newUserRawPassword .= maybe_ (optField "password" schema) - <*> newUserRawExpiresIn .= maybe_ (optField "expires_in" schema) - <*> newUserRawManagedBy .= maybe_ (optField "managed_by" schema) + <$> newUserRawDisplayName + .= field "name" schema + <*> newUserRawUUID + .= maybe_ (optField "uuid" genericToSchema) + <*> newUserRawEmail + .= maybe_ (optField "email" schema) + <*> newUserRawPhone + .= maybe_ (optField "phone" schema) + <*> newUserRawSSOId + .= maybe_ (optField "sso_id" genericToSchema) + <*> newUserRawPict + .= maybe_ (optField "picture" schema) + <*> newUserRawAssets + .= (fromMaybe [] <$> optField "assets" (array schema)) + <*> newUserRawAccentId + .= maybe_ (optField "accent_id" schema) + <*> newUserRawEmailCode + .= maybe_ (optField "email_code" schema) + <*> newUserRawPhoneCode + .= maybe_ (optField "phone_code" schema) + <*> newUserRawInvitationCode + .= maybe_ (optField "invitation_code" schema) + <*> newUserRawTeamCode + .= maybe_ (optField "team_code" schema) + <*> newUserRawTeam + .= maybe_ (optField "team" schema) + <*> newUserRawTeamId + .= maybe_ (optField "team_id" schema) + <*> newUserRawLabel + .= maybe_ (optField "label" schema) + <*> newUserRawLocale + .= maybe_ (optField "locale" schema) + <*> newUserRawPassword + .= maybe_ (optField "password" schema) + <*> newUserRawExpiresIn + .= maybe_ (optField "expires_in" schema) + <*> newUserRawManagedBy + .= maybe_ (optField "managed_by" schema) instance ToSchema NewUser where schema = @@ -959,6 +1017,15 @@ newtype InvitationCode = InvitationCode deriving newtype (ToSchema, ToByteString, FromByteString, Arbitrary) deriving (FromJSON, ToJSON, S.ToSchema) via Schema InvitationCode +instance S.ToParamSchema InvitationCode where + toParamSchema _ = S.toParamSchema (Proxy @Text) + +instance FromHttpApiData InvitationCode where + parseQueryParam = bimap cs InvitationCode . validateBase64Url + +instance ToHttpApiData InvitationCode where + toQueryParam = cs . toByteString . fromInvitationCode + -------------------------------------------------------------------------------- -- NewTeamUser @@ -1003,8 +1070,10 @@ instance ToSchema BindingNewTeamUser where schema = object "BindingNewTeamUser" $ BindingNewTeamUser - <$> bnuTeam .= bindingNewTeamObjectSchema - <*> bnuCurrency .= maybe_ (optField "currency" genericToSchema) + <$> bnuTeam + .= bindingNewTeamObjectSchema + <*> bnuCurrency + .= maybe_ (optField "currency" genericToSchema) -------------------------------------------------------------------------------- -- SCIM User Info @@ -1021,8 +1090,10 @@ instance ToSchema ScimUserInfo where schema = object "ScimUserInfo" $ ScimUserInfo - <$> suiUserId .= field "id" schema - <*> suiCreatedOn .= maybe_ (optField "created_on" schema) + <$> suiUserId + .= field "id" schema + <*> suiCreatedOn + .= maybe_ (optField "created_on" schema) newtype ScimUserInfos = ScimUserInfos {scimUserInfos :: [ScimUserInfo]} deriving stock (Eq, Show, Generic) @@ -1033,7 +1104,8 @@ instance ToSchema ScimUserInfos where schema = object "ScimUserInfos" $ ScimUserInfos - <$> scimUserInfos .= field "scim_user_infos" (array schema) + <$> scimUserInfos + .= field "scim_user_infos" (array schema) ------------------------------------------------------------------------------- -- UserSet @@ -1051,7 +1123,8 @@ instance ToSchema UserSet where schema = object "UserSet" $ UserSet - <$> usUsrs .= field "users" (set schema) + <$> usUsrs + .= field "users" (set schema) -------------------------------------------------------------------------------- -- Profile Updates @@ -1071,10 +1144,14 @@ instance ToSchema UserUpdate where schema = object "UserUpdate" $ UserUpdate - <$> uupName .= maybe_ (optField "name" schema) - <*> uupPict .= maybe_ (optField "picture" schema) - <*> uupAssets .= maybe_ (optField "assets" (array schema)) - <*> uupAccentId .= maybe_ (optField "accent_id" schema) + <$> uupName + .= maybe_ (optField "name" schema) + <*> uupPict + .= maybe_ (optField "picture" schema) + <*> uupAssets + .= maybe_ (optField "assets" (array schema)) + <*> uupAccentId + .= maybe_ (optField "accent_id" schema) data UpdateProfileError = DisplayNameManagedByScim @@ -1111,8 +1188,10 @@ instance ToSchema PasswordChange where ) . object "PasswordChange" $ PasswordChange - <$> cpOldPassword .= maybe_ (optField "old_password" schema) - <*> cpNewPassword .= field "new_password" schema + <$> cpOldPassword + .= maybe_ (optField "old_password" schema) + <*> cpNewPassword + .= field "new_password" schema data ChangePasswordError = InvalidCurrentPassword @@ -1145,7 +1224,8 @@ instance ToSchema LocaleUpdate where schema = object "LocaleUpdate" $ LocaleUpdate - <$> luLocale .= field "locale" schema + <$> luLocale + .= field "locale" schema newtype EmailUpdate = EmailUpdate {euEmail :: Email} deriving stock (Eq, Show, Generic) @@ -1156,7 +1236,8 @@ instance ToSchema EmailUpdate where schema = object "EmailUpdate" $ EmailUpdate - <$> euEmail .= field "email" schema + <$> euEmail + .= field "email" schema modelEmailUpdate :: Doc.Model modelEmailUpdate = Doc.defineModel "EmailUpdate" $ do @@ -1180,7 +1261,8 @@ instance ToSchema PhoneUpdate where schema = object "PhoneUpdate" $ PhoneUpdate - <$> puPhone .= field "phone" schema + <$> puPhone + .= field "phone" schema data ChangePhoneError = PhoneExists @@ -1301,7 +1383,8 @@ instance ToSchema DeleteUser where schema = object "DeleteUser" $ DeleteUser - <$> deleteUserPassword .= maybe_ (optField "password" schema) + <$> deleteUserPassword + .= maybe_ (optField "password" schema) mkDeleteUser :: Maybe PlainTextPassword -> DeleteUser mkDeleteUser = DeleteUser @@ -1316,7 +1399,8 @@ modelDelete = Doc.defineModel "Delete" $ do instance ToJSON DeleteUser where toJSON d = A.object $ - "password" A..= deleteUserPassword d + "password" + A..= deleteUserPassword d # [] instance FromJSON DeleteUser where @@ -1339,8 +1423,10 @@ 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 + <$> 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 @@ -1353,7 +1439,8 @@ instance ToSchema DeletionCodeTimeout where schema = object "DeletionCodeTimeout" $ DeletionCodeTimeout - <$> fromDeletionCodeTimeout .= field "expires_in" schema + <$> fromDeletionCodeTimeout + .= field "expires_in" schema instance ToJSON DeletionCodeTimeout where toJSON (DeletionCodeTimeout t) = A.object ["expires_in" A..= t] @@ -1466,5 +1553,7 @@ instance ToSchema SendVerificationCode where schema = object "SendVerificationCode" $ SendVerificationCode - <$> svcAction .= field "action" schema - <*> svcEmail .= field "email" schema + <$> svcAction + .= field "action" schema + <*> svcEmail + .= field "email" schema diff --git a/libs/wire-api/src/Wire/API/User/Scim.hs b/libs/wire-api/src/Wire/API/User/Scim.hs index 1576c0b311..27ce75cd20 100644 --- a/libs/wire-api/src/Wire/API/User/Scim.hs +++ b/libs/wire-api/src/Wire/API/User/Scim.hs @@ -84,6 +84,7 @@ import Web.Scim.Schema.Schema (Schema (CustomSchema)) import qualified Web.Scim.Schema.Schema as Scim import qualified Web.Scim.Schema.User as Scim import qualified Web.Scim.Schema.User as Scim.User +import Wire.API.Team.Role (Role) import Wire.API.User.Identity (Email) import Wire.API.User.Profile as BT import qualified Wire.API.User.RichInfo as RI @@ -326,7 +327,8 @@ data ValidScimUser = ValidScimUser _vsuName :: BT.Name, _vsuRichInfo :: RI.RichInfo, _vsuActive :: Bool, - _vsuLocale :: Maybe Locale + _vsuLocale :: Maybe Locale, + _vsuRole :: Role } deriving (Eq, Show) 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 ccff53d727..32471911e4 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 @@ -83,6 +83,8 @@ import qualified Test.Wire.API.Golden.Generated.EmailUpdate_user import qualified Test.Wire.API.Golden.Generated.Email_user import qualified Test.Wire.API.Golden.Generated.EventType_team import qualified Test.Wire.API.Golden.Generated.EventType_user +import qualified Test.Wire.API.Golden.Generated.Event_conversation +import qualified Test.Wire.API.Golden.Generated.Event_featureConfig import qualified Test.Wire.API.Golden.Generated.Event_team import qualified Test.Wire.API.Golden.Generated.Event_user import qualified Test.Wire.API.Golden.Generated.HandleUpdate_user @@ -1389,5 +1391,34 @@ tests = testGroup "Golden: WithStatusPatch_team 19" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_19, "testObject_WithStatusPatch_team_19.json")] + [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_19, "testObject_WithStatusPatch_team_19.json")], + testGroup + "Golden: Event_FeatureConfig" + $ testObjects + [ (Test.Wire.API.Golden.Generated.Event_featureConfig.testObject_Event_featureConfig_1, "testObject_Event_featureConfig_1.json"), + (Test.Wire.API.Golden.Generated.Event_featureConfig.testObject_Event_featureConfig_2, "testObject_Event_featureConfig_2.json"), + (Test.Wire.API.Golden.Generated.Event_featureConfig.testObject_Event_featureConfig_3, "testObject_Event_featureConfig_3.json"), + (Test.Wire.API.Golden.Generated.Event_featureConfig.testObject_Event_featureConfig_4, "testObject_Event_featureConfig_4.json"), + (Test.Wire.API.Golden.Generated.Event_featureConfig.testObject_Event_featureConfig_5, "testObject_Event_featureConfig_5.json"), + (Test.Wire.API.Golden.Generated.Event_featureConfig.testObject_Event_featureConfig_6, "testObject_Event_featureConfig_6.json"), + (Test.Wire.API.Golden.Generated.Event_featureConfig.testObject_Event_featureConfig_7, "testObject_Event_featureConfig_7.json"), + (Test.Wire.API.Golden.Generated.Event_featureConfig.testObject_Event_featureConfig_8, "testObject_Event_featureConfig_8.json"), + (Test.Wire.API.Golden.Generated.Event_featureConfig.testObject_Event_featureConfig_9, "testObject_Event_featureConfig_9.json"), + (Test.Wire.API.Golden.Generated.Event_featureConfig.testObject_Event_featureConfig_10, "testObject_Event_featureConfig_10.json") + ], + testGroup + "Golden: Event_Conversation" + $ testObjects + [ (Test.Wire.API.Golden.Generated.Event_conversation.testObject_Event_conversation_1, "testObject_Event_conversation_1.json"), + (Test.Wire.API.Golden.Generated.Event_conversation.testObject_Event_conversation_2, "testObject_Event_conversation_2.json"), + (Test.Wire.API.Golden.Generated.Event_conversation.testObject_Event_conversation_3, "testObject_Event_conversation_3.json"), + (Test.Wire.API.Golden.Generated.Event_conversation.testObject_Event_conversation_4, "testObject_Event_conversation_4.json"), + (Test.Wire.API.Golden.Generated.Event_conversation.testObject_Event_conversation_5, "testObject_Event_conversation_5.json"), + (Test.Wire.API.Golden.Generated.Event_conversation.testObject_Event_conversation_6, "testObject_Event_conversation_6.json"), + (Test.Wire.API.Golden.Generated.Event_conversation.testObject_Event_conversation_7, "testObject_Event_conversation_7.json"), + (Test.Wire.API.Golden.Generated.Event_conversation.testObject_Event_conversation_8, "testObject_Event_conversation_8.json"), + (Test.Wire.API.Golden.Generated.Event_conversation.testObject_Event_conversation_9, "testObject_Event_conversation_9.json"), + (Test.Wire.API.Golden.Generated.Event_conversation.testObject_Event_conversation_11, "testObject_Event_conversation_11.json"), + (Test.Wire.API.Golden.Generated.Event_conversation.testObject_Event_conversation_10, "testObject_Event_conversation_10.json") + ] ] diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_conversation.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_conversation.hs new file mode 100644 index 0000000000..77422d6a91 --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_conversation.hs @@ -0,0 +1,178 @@ +{-# LANGUAGE OverloadedLists #-} + +-- 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 Test.Wire.API.Golden.Generated.Event_conversation where + +import Data.Code +import Data.Domain (Domain (..)) +import Data.Id +import Data.Misc (HttpsUrl (HttpsUrl)) +import Data.Qualified (Qualified (..)) +import Data.Range +import Data.Time +import qualified Data.UUID as UUID +import GHC.Exts (IsList (fromList)) +import Imports +import URI.ByteString (Authority (..), Host (..), Query (..), Scheme (..), URIRef (..)) +import Wire.API.Conversation (Access (..), MutedStatus (..)) +import Wire.API.Conversation.Role +import Wire.API.Conversation.Typing +import Wire.API.Event.Conversation + +testObject_Event_conversation_1 :: Event +testObject_Event_conversation_1 = + Event + { evtConv = Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "oym59-06.i423w"}}, + evtFrom = Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "n8nl6tp.h5"}}, + evtTime = UTCTime {utctDay = ModifiedJulianDay 58119, utctDayTime = 0}, + evtData = EdConvCodeDelete + } + +testObject_Event_conversation_2 :: Event +testObject_Event_conversation_2 = + Event + { evtConv = Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "2m99----34.id7u09"}}, + evtFrom = Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "b.0-7.0.rg"}}, + evtTime = UTCTime {utctDay = ModifiedJulianDay 58119, utctDayTime = 0}, + evtData = + EdMemberUpdate + ( MemberUpdateData + { misTarget = Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "oy8yz.f1"}}, + misOtrMutedStatus = Just (MutedStatus {fromMutedStatus = 1}), + misOtrMutedRef = Nothing, + misOtrArchived = Nothing, + misOtrArchivedRef = Nothing, + misHidden = Nothing, + misHiddenRef = Nothing, + misConvRoleName = Just roleNameWireAdmin + } + ) + } + +testObject_Event_conversation_3 :: Event +testObject_Event_conversation_3 = + Event + { evtConv = Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "91.ii9vf.mbwj9k7lmk"}}, + evtFrom = Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "k3.f.z"}}, + evtTime = UTCTime {utctDay = ModifiedJulianDay 58119, utctDayTime = 0}, + evtData = + EdConvCodeUpdate + ( ConversationCode + { conversationKey = Key {asciiKey = unsafeRange "CRdONS7988O2QdyndJs1"}, + conversationCode = Value {asciiValue = unsafeRange "7d6713"}, + conversationUri = Just $ HttpsUrl (URI {uriScheme = Scheme {schemeBS = "https"}, uriAuthority = Just (Authority {authorityUserInfo = Nothing, authorityHost = Host {hostBS = "example.com"}, authorityPort = Nothing}), uriPath = "", uriQuery = Query {queryPairs = []}, uriFragment = Nothing}) + } + ) + } + +testObject_Event_conversation_4 :: Event +testObject_Event_conversation_4 = + Event + { evtConv = Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "ma--6us.i8o--0440"}}, + evtFrom = Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "97k-u0.b-5c"}}, + evtTime = UTCTime {utctDay = ModifiedJulianDay 58119, utctDayTime = 0}, + evtData = EdConvAccessUpdate (ConversationAccessData {cupAccess = fromList [PrivateAccess, CodeAccess], cupAccessRoles = fromList []}) + } + +testObject_Event_conversation_5 :: Event +testObject_Event_conversation_5 = + Event + { evtConv = Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "q.6lm833.o95.l.y2"}}, + evtFrom = Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "7.m4f7p.ez4zs61"}}, + evtTime = UTCTime {utctDay = ModifiedJulianDay 58119, utctDayTime = 0}, + evtData = EdMLSWelcome "" + } + +testObject_Event_conversation_6 :: Event +testObject_Event_conversation_6 = + Event + { evtConv = Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "391.r"}}, + evtFrom = Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "8.0-6.t7pxv"}}, + evtTime = UTCTime {utctDay = ModifiedJulianDay 58119, utctDayTime = 0}, + evtData = EdOtrMessage (OtrMessage {otrSender = ClientId {client = "1"}, otrRecipient = ClientId {client = "1"}, otrCiphertext = "", otrData = Just "I\68655"}) + } + +testObject_Event_conversation_7 :: Event +testObject_Event_conversation_7 = + Event + { evtConv = Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "b2.ue4k"}}, + evtFrom = Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "64b3--h.u"}}, + evtTime = UTCTime {utctDay = ModifiedJulianDay 58119, utctDayTime = 0}, + evtData = EdOtrMessage (OtrMessage {otrSender = ClientId {client = "3"}, otrRecipient = ClientId {client = "3"}, otrCiphertext = "%\SI", otrData = Nothing}) + } + +testObject_Event_conversation_8 :: Event +testObject_Event_conversation_8 = + Event + { evtConv = Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "36.e9.s-o-17"}}, + evtFrom = Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "0--gy.705nsa8.j4m"}}, + evtTime = UTCTime {utctDay = ModifiedJulianDay 58119, utctDayTime = 0}, + evtData = EdTyping (TypingData {tdStatus = StartedTyping}) + } + +testObject_Event_conversation_9 :: Event +testObject_Event_conversation_9 = + Event + { evtConv = Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "743846p6-pp33.1.ktb9.0bmn.efm2bly"}}, + evtFrom = Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "9--5grmn.j39y3--9n"}}, + evtTime = UTCTime {utctDay = ModifiedJulianDay 58119, utctDayTime = 0}, + evtData = + EdMembersLeave + ( QualifiedUserIdList + { qualifiedUserIdList = + [ Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "ow8i3fhr.v"}}, + Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "8kshw-2l.3w44.6c8763a-77r4.gk13zq"}}, + Qualified + { qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), + qDomain = Domain {_domainText = "xk-no.m5--f8b7"} + }, + Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "x02.p.69y-6.8ncr.u"}}, + Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "1-47tume1e5l32i.v75is-q4-o.u7qc"}}, + Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "8ay.ec.k-8"}}, + Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "u-8.m-42ns2c"}}, + Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "08kh83-8.vu.i24"}}, + Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "t1t683.o3--2.3k5.it-5.e1"}}, + Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "q.ajw-5"}}, + Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "73m4g.c24em3.v"}}, + Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "i7zn.li"}}, + Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "42y78.yekf"}}, + Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "n63-p87m2.dtq"}}, + Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "ns.v2p"}} + ] + } + ) + } + +testObject_Event_conversation_10 :: Event +testObject_Event_conversation_10 = + Event + { evtConv = Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "1852a.o-4"}}, + evtFrom = Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "4-p.d7b8d3.6.c8--jds3-1acy"}}, + evtTime = UTCTime {utctDay = ModifiedJulianDay 58119, utctDayTime = 0}, + evtData = EdMLSMessage "s\b\138w\236\231P(\ESC\216\205" + } + +testObject_Event_conversation_11 :: Event +testObject_Event_conversation_11 = + Event + { evtConv = Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "wwzw-ly4jk5.6790-y.j04o-21.ltl"}}, + evtFrom = Qualified {qUnqualified = Id (fromJust (UUID.fromString "2126ea99-ca79-43ea-ad99-a59616468e8e")), qDomain = Domain {_domainText = "70-o.ncd"}}, + evtTime = UTCTime {utctDay = ModifiedJulianDay 58119, utctDayTime = 0}, + evtData = EdTyping (TypingData {tdStatus = StoppedTyping}) + } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_featureConfig.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_featureConfig.hs new file mode 100644 index 0000000000..63733de48b --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_featureConfig.hs @@ -0,0 +1,54 @@ +{-# LANGUAGE OverloadedLists #-} + +-- 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 Test.Wire.API.Golden.Generated.Event_featureConfig where + +import Data.Aeson +import GHC.Exts (IsList (fromList)) +import Wire.API.Event.FeatureConfig + +testObject_Event_featureConfig_1 :: Event +testObject_Event_featureConfig_1 = Event {_eventType = Update, _eventFeatureName = "", _eventData = Object (fromList [("config", Object (fromList [("domains", Array [])])), ("lockStatus", String "unlocked"), ("status", String "enabled"), ("ttl", String "unlimited")])} + +testObject_Event_featureConfig_2 :: Event +testObject_Event_featureConfig_2 = Event {_eventType = Update, _eventFeatureName = "\DLE", _eventData = Object (fromList [("lockStatus", String "locked"), ("status", String "disabled"), ("ttl", String "unlimited")])} + +testObject_Event_featureConfig_3 :: Event +testObject_Event_featureConfig_3 = Event {_eventType = Update, _eventFeatureName = "4\988540%\ETX", _eventData = Object (fromList [("lockStatus", String "locked"), ("status", String "disabled"), ("ttl", String "unlimited")])} + +testObject_Event_featureConfig_4 :: Event +testObject_Event_featureConfig_4 = Event {_eventType = Update, _eventFeatureName = "n(\1041492>\b", _eventData = Object (fromList [("lockStatus", String "locked"), ("status", String "disabled"), ("ttl", Number 6.0)])} + +testObject_Event_featureConfig_5 :: Event +testObject_Event_featureConfig_5 = Event {_eventType = Update, _eventFeatureName = "\1002596T\n\1092227X", _eventData = Object (fromList [("lockStatus", String "locked"), ("status", String "disabled"), ("ttl", String "unlimited")])} + +testObject_Event_featureConfig_6 :: Event +testObject_Event_featureConfig_6 = Event {_eventType = Update, _eventFeatureName = "\1039478\1022562)TXC\52414\174655K", _eventData = Object (fromList [("lockStatus", String "locked"), ("status", String "enabled"), ("ttl", Number 7.0)])} + +testObject_Event_featureConfig_7 :: Event +testObject_Event_featureConfig_7 = Event {_eventType = Update, _eventFeatureName = "\DEL+\1070185\190816\&9\52178\nW", _eventData = Object (fromList [("lockStatus", String "unlocked"), ("status", String "enabled"), ("ttl", Number 9.0)])} + +testObject_Event_featureConfig_8 :: Event +testObject_Event_featureConfig_8 = Event {_eventType = Update, _eventFeatureName = "7\DLEq-w\11345\DLE B\1028119H\n\DC2R\b", _eventData = Object (fromList [("config", Object (fromList [("enforcedTimeoutSeconds", Number 32.0)])), ("lockStatus", String "locked"), ("status", String "disabled"), ("ttl", Number 3.0)])} + +testObject_Event_featureConfig_9 :: Event +testObject_Event_featureConfig_9 = Event {_eventType = Update, _eventFeatureName = "\18889\20273d\1004321r\GSd'L\63854\SUB,\26907\SOH{@", _eventData = Object (fromList [("lockStatus", String "locked"), ("status", String "disabled"), ("ttl", Number 16.0)])} + +testObject_Event_featureConfig_10 :: Event +testObject_Event_featureConfig_10 = Event {_eventType = Update, _eventFeatureName = "\1105858'\1002071\&1S=\1058029u\1103881\10729:}\SUB#f +-- +-- 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 Test.Wire.API.Golden.Manual.TeamSize where + +import Imports +import Wire.API.Team.Size + +testObject_TeamSize_1 :: TeamSize +testObject_TeamSize_1 = TeamSize 0 + +testObject_TeamSize_2 :: TeamSize +testObject_TeamSize_2 = TeamSize 100 + +testObject_TeamSize_3 :: TeamSize +testObject_TeamSize_3 = TeamSize (fromIntegral $ maxBound @Word64) diff --git a/libs/wire-api/test/golden/testObject_Event_conversation_1.json b/libs/wire-api/test/golden/testObject_Event_conversation_1.json new file mode 100644 index 0000000000..901fb9b9c8 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Event_conversation_1.json @@ -0,0 +1,15 @@ +{ + "conversation": "2126ea99-ca79-43ea-ad99-a59616468e8e", + "data": null, + "from": "2126ea99-ca79-43ea-ad99-a59616468e8e", + "qualified_conversation": { + "domain": "oym59-06.i423w", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "qualified_from": { + "domain": "n8nl6tp.h5", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "time": "2018-01-01T00:00:00.000Z", + "type": "conversation.code-delete" +} diff --git a/libs/wire-api/test/golden/testObject_Event_conversation_10.json b/libs/wire-api/test/golden/testObject_Event_conversation_10.json new file mode 100644 index 0000000000..d313b382c5 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Event_conversation_10.json @@ -0,0 +1,15 @@ +{ + "conversation": "2126ea99-ca79-43ea-ad99-a59616468e8e", + "data": "cwiKd+znUCgb2M0=", + "from": "2126ea99-ca79-43ea-ad99-a59616468e8e", + "qualified_conversation": { + "domain": "1852a.o-4", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "qualified_from": { + "domain": "4-p.d7b8d3.6.c8--jds3-1acy", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "time": "2018-01-01T00:00:00.000Z", + "type": "conversation.mls-message-add" +} diff --git a/libs/wire-api/test/golden/testObject_Event_conversation_11.json b/libs/wire-api/test/golden/testObject_Event_conversation_11.json new file mode 100644 index 0000000000..25a260e522 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Event_conversation_11.json @@ -0,0 +1,17 @@ +{ + "conversation": "2126ea99-ca79-43ea-ad99-a59616468e8e", + "data": { + "status": "stopped" + }, + "from": "2126ea99-ca79-43ea-ad99-a59616468e8e", + "qualified_conversation": { + "domain": "wwzw-ly4jk5.6790-y.j04o-21.ltl", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "qualified_from": { + "domain": "70-o.ncd", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "time": "2018-01-01T00:00:00.000Z", + "type": "conversation.typing" +} diff --git a/libs/wire-api/test/golden/testObject_Event_conversation_2.json b/libs/wire-api/test/golden/testObject_Event_conversation_2.json new file mode 100644 index 0000000000..94d1a78c81 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Event_conversation_2.json @@ -0,0 +1,23 @@ +{ + "conversation": "2126ea99-ca79-43ea-ad99-a59616468e8e", + "data": { + "conversation_role": "wire_admin", + "otr_muted_status": 1, + "qualified_target": { + "domain": "oy8yz.f1", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "target": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "from": "2126ea99-ca79-43ea-ad99-a59616468e8e", + "qualified_conversation": { + "domain": "2m99----34.id7u09", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "qualified_from": { + "domain": "b.0-7.0.rg", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "time": "2018-01-01T00:00:00.000Z", + "type": "conversation.member-update" +} diff --git a/libs/wire-api/test/golden/testObject_Event_conversation_3.json b/libs/wire-api/test/golden/testObject_Event_conversation_3.json new file mode 100644 index 0000000000..2b9001a47a --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Event_conversation_3.json @@ -0,0 +1,19 @@ +{ + "conversation": "2126ea99-ca79-43ea-ad99-a59616468e8e", + "data": { + "code": "7d6713", + "key": "CRdONS7988O2QdyndJs1", + "uri": "https://example.com" + }, + "from": "2126ea99-ca79-43ea-ad99-a59616468e8e", + "qualified_conversation": { + "domain": "91.ii9vf.mbwj9k7lmk", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "qualified_from": { + "domain": "k3.f.z", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "time": "2018-01-01T00:00:00.000Z", + "type": "conversation.code-update" +} diff --git a/libs/wire-api/test/golden/testObject_Event_conversation_4.json b/libs/wire-api/test/golden/testObject_Event_conversation_4.json new file mode 100644 index 0000000000..98dc2ef946 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Event_conversation_4.json @@ -0,0 +1,22 @@ +{ + "conversation": "2126ea99-ca79-43ea-ad99-a59616468e8e", + "data": { + "access": [ + "private", + "code" + ], + "access_role": "private", + "access_role_v2": [] + }, + "from": "2126ea99-ca79-43ea-ad99-a59616468e8e", + "qualified_conversation": { + "domain": "ma--6us.i8o--0440", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "qualified_from": { + "domain": "97k-u0.b-5c", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "time": "2018-01-01T00:00:00.000Z", + "type": "conversation.access-update" +} diff --git a/libs/wire-api/test/golden/testObject_Event_conversation_5.json b/libs/wire-api/test/golden/testObject_Event_conversation_5.json new file mode 100644 index 0000000000..2d5d23e25d --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Event_conversation_5.json @@ -0,0 +1,15 @@ +{ + "conversation": "2126ea99-ca79-43ea-ad99-a59616468e8e", + "data": "", + "from": "2126ea99-ca79-43ea-ad99-a59616468e8e", + "qualified_conversation": { + "domain": "q.6lm833.o95.l.y2", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "qualified_from": { + "domain": "7.m4f7p.ez4zs61", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "time": "2018-01-01T00:00:00.000Z", + "type": "conversation.mls-welcome" +} diff --git a/libs/wire-api/test/golden/testObject_Event_conversation_6.json b/libs/wire-api/test/golden/testObject_Event_conversation_6.json new file mode 100644 index 0000000000..974245e9eb --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Event_conversation_6.json @@ -0,0 +1,20 @@ +{ + "conversation": "2126ea99-ca79-43ea-ad99-a59616468e8e", + "data": { + "data": "I𐰯", + "recipient": "1", + "sender": "1", + "text": "" + }, + "from": "2126ea99-ca79-43ea-ad99-a59616468e8e", + "qualified_conversation": { + "domain": "391.r", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "qualified_from": { + "domain": "8.0-6.t7pxv", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "time": "2018-01-01T00:00:00.000Z", + "type": "conversation.otr-message-add" +} diff --git a/libs/wire-api/test/golden/testObject_Event_conversation_7.json b/libs/wire-api/test/golden/testObject_Event_conversation_7.json new file mode 100644 index 0000000000..d3ecd337a4 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Event_conversation_7.json @@ -0,0 +1,19 @@ +{ + "conversation": "2126ea99-ca79-43ea-ad99-a59616468e8e", + "data": { + "recipient": "3", + "sender": "3", + "text": "%\u000f" + }, + "from": "2126ea99-ca79-43ea-ad99-a59616468e8e", + "qualified_conversation": { + "domain": "b2.ue4k", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "qualified_from": { + "domain": "64b3--h.u", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "time": "2018-01-01T00:00:00.000Z", + "type": "conversation.otr-message-add" +} diff --git a/libs/wire-api/test/golden/testObject_Event_conversation_8.json b/libs/wire-api/test/golden/testObject_Event_conversation_8.json new file mode 100644 index 0000000000..8de3cbc36f --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Event_conversation_8.json @@ -0,0 +1,17 @@ +{ + "conversation": "2126ea99-ca79-43ea-ad99-a59616468e8e", + "data": { + "status": "started" + }, + "from": "2126ea99-ca79-43ea-ad99-a59616468e8e", + "qualified_conversation": { + "domain": "36.e9.s-o-17", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "qualified_from": { + "domain": "0--gy.705nsa8.j4m", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "time": "2018-01-01T00:00:00.000Z", + "type": "conversation.typing" +} diff --git a/libs/wire-api/test/golden/testObject_Event_conversation_9.json b/libs/wire-api/test/golden/testObject_Event_conversation_9.json new file mode 100644 index 0000000000..028aeb144d --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Event_conversation_9.json @@ -0,0 +1,95 @@ +{ + "conversation": "2126ea99-ca79-43ea-ad99-a59616468e8e", + "data": { + "qualified_user_ids": [ + { + "domain": "ow8i3fhr.v", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + { + "domain": "8kshw-2l.3w44.6c8763a-77r4.gk13zq", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + { + "domain": "xk-no.m5--f8b7", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + { + "domain": "x02.p.69y-6.8ncr.u", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + { + "domain": "1-47tume1e5l32i.v75is-q4-o.u7qc", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + { + "domain": "8ay.ec.k-8", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + { + "domain": "u-8.m-42ns2c", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + { + "domain": "08kh83-8.vu.i24", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + { + "domain": "t1t683.o3--2.3k5.it-5.e1", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + { + "domain": "q.ajw-5", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + { + "domain": "73m4g.c24em3.v", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + { + "domain": "i7zn.li", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + { + "domain": "42y78.yekf", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + { + "domain": "n63-p87m2.dtq", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + { + "domain": "ns.v2p", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + } + ], + "user_ids": [ + "2126ea99-ca79-43ea-ad99-a59616468e8e", + "2126ea99-ca79-43ea-ad99-a59616468e8e", + "2126ea99-ca79-43ea-ad99-a59616468e8e", + "2126ea99-ca79-43ea-ad99-a59616468e8e", + "2126ea99-ca79-43ea-ad99-a59616468e8e", + "2126ea99-ca79-43ea-ad99-a59616468e8e", + "2126ea99-ca79-43ea-ad99-a59616468e8e", + "2126ea99-ca79-43ea-ad99-a59616468e8e", + "2126ea99-ca79-43ea-ad99-a59616468e8e", + "2126ea99-ca79-43ea-ad99-a59616468e8e", + "2126ea99-ca79-43ea-ad99-a59616468e8e", + "2126ea99-ca79-43ea-ad99-a59616468e8e", + "2126ea99-ca79-43ea-ad99-a59616468e8e", + "2126ea99-ca79-43ea-ad99-a59616468e8e", + "2126ea99-ca79-43ea-ad99-a59616468e8e" + ] + }, + "from": "2126ea99-ca79-43ea-ad99-a59616468e8e", + "qualified_conversation": { + "domain": "743846p6-pp33.1.ktb9.0bmn.efm2bly", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "qualified_from": { + "domain": "9--5grmn.j39y3--9n", + "id": "2126ea99-ca79-43ea-ad99-a59616468e8e" + }, + "time": "2018-01-01T00:00:00.000Z", + "type": "conversation.member-leave" +} diff --git a/libs/wire-api/test/golden/testObject_Event_featureConfig_1.json b/libs/wire-api/test/golden/testObject_Event_featureConfig_1.json new file mode 100644 index 0000000000..1a937b095c --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Event_featureConfig_1.json @@ -0,0 +1,12 @@ +{ + "data": { + "config": { + "domains": [] + }, + "lockStatus": "unlocked", + "status": "enabled", + "ttl": "unlimited" + }, + "name": "", + "type": "feature-config.update" +} diff --git a/libs/wire-api/test/golden/testObject_Event_featureConfig_10.json b/libs/wire-api/test/golden/testObject_Event_featureConfig_10.json new file mode 100644 index 0000000000..7807e689ee --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Event_featureConfig_10.json @@ -0,0 +1,9 @@ +{ + "data": { + "lockStatus": "locked", + "status": "enabled", + "ttl": 3 + }, + "name": "􍿂'󴩗1S=􂓭u􍠉⧩:}\u001a#f\u0008", + "type": "feature-config.update" +} diff --git a/libs/wire-api/test/golden/testObject_Event_featureConfig_5.json b/libs/wire-api/test/golden/testObject_Event_featureConfig_5.json new file mode 100644 index 0000000000..a297f463f1 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Event_featureConfig_5.json @@ -0,0 +1,9 @@ +{ + "data": { + "lockStatus": "locked", + "status": "disabled", + "ttl": "unlimited" + }, + "name": "󴱤T\n􊪃X", + "type": "feature-config.update" +} diff --git a/libs/wire-api/test/golden/testObject_Event_featureConfig_6.json b/libs/wire-api/test/golden/testObject_Event_featureConfig_6.json new file mode 100644 index 0000000000..da07ae8708 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Event_featureConfig_6.json @@ -0,0 +1,9 @@ +{ + "data": { + "lockStatus": "locked", + "status": "enabled", + "ttl": 7 + }, + "name": "󽱶󹩢)TXC첾𪨿K", + "type": "feature-config.update" +} diff --git a/libs/wire-api/test/golden/testObject_Event_featureConfig_7.json b/libs/wire-api/test/golden/testObject_Event_featureConfig_7.json new file mode 100644 index 0000000000..42ba1027ec --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Event_featureConfig_7.json @@ -0,0 +1,9 @@ +{ + "data": { + "lockStatus": "unlocked", + "status": "enabled", + "ttl": 9 + }, + "name": "+􅑩𮥠9쯒\nW", + "type": "feature-config.update" +} diff --git a/libs/wire-api/test/golden/testObject_Event_featureConfig_8.json b/libs/wire-api/test/golden/testObject_Event_featureConfig_8.json new file mode 100644 index 0000000000..b548d6d82b --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Event_featureConfig_8.json @@ -0,0 +1,12 @@ +{ + "data": { + "config": { + "enforcedTimeoutSeconds": 32 + }, + "lockStatus": "locked", + "status": "disabled", + "ttl": 3 + }, + "name": "7\u0010q-wⱑ\u0010 B󻀗H\n\u0012R\u0008", + "type": "feature-config.update" +} diff --git a/libs/wire-api/test/golden/testObject_Event_featureConfig_9.json b/libs/wire-api/test/golden/testObject_Event_featureConfig_9.json new file mode 100644 index 0000000000..00e9648b07 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Event_featureConfig_9.json @@ -0,0 +1,9 @@ +{ + "data": { + "lockStatus": "locked", + "status": "disabled", + "ttl": 16 + }, + "name": "䧉伱d󵌡r\u001dd'L葉\u001a,椛\u0001{@", + "type": "feature-config.update" +} diff --git a/libs/wire-api/test/golden/testObject_TeamSize_1.json b/libs/wire-api/test/golden/testObject_TeamSize_1.json new file mode 100644 index 0000000000..c883020b74 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_TeamSize_1.json @@ -0,0 +1,3 @@ +{ + "teamSize": 0 +} diff --git a/libs/wire-api/test/golden/testObject_TeamSize_2.json b/libs/wire-api/test/golden/testObject_TeamSize_2.json new file mode 100644 index 0000000000..b33bf1f3bd --- /dev/null +++ b/libs/wire-api/test/golden/testObject_TeamSize_2.json @@ -0,0 +1,3 @@ +{ + "teamSize": 100 +} diff --git a/libs/wire-api/test/golden/testObject_TeamSize_3.json b/libs/wire-api/test/golden/testObject_TeamSize_3.json new file mode 100644 index 0000000000..2e47b19f3e --- /dev/null +++ b/libs/wire-api/test/golden/testObject_TeamSize_3.json @@ -0,0 +1,3 @@ +{ + "teamSize": 1.8446744073709551615e19 +} diff --git a/libs/wire-api/test/unit/Main.hs b/libs/wire-api/test/unit/Main.hs index 74dcadd90e..46178a1d7a 100644 --- a/libs/wire-api/test/unit/Main.hs +++ b/libs/wire-api/test/unit/Main.hs @@ -28,6 +28,7 @@ import qualified Test.Wire.API.MLS as MLS import qualified Test.Wire.API.Roundtrip.Aeson as Roundtrip.Aeson import qualified Test.Wire.API.Roundtrip.ByteString as Roundtrip.ByteString import qualified Test.Wire.API.Roundtrip.CSV as Roundtrip.CSV +import qualified Test.Wire.API.Roundtrip.HttpApiData as Roundtrip.HttpApiData import qualified Test.Wire.API.Roundtrip.MLS as Roundtrip.MLS import qualified Test.Wire.API.Routes as Routes import qualified Test.Wire.API.Swagger as Swagger @@ -52,6 +53,7 @@ main = User.Auth.tests, Roundtrip.Aeson.tests, Roundtrip.ByteString.tests, + Roundtrip.HttpApiData.tests, Roundtrip.MLS.tests, Swagger.tests, Roundtrip.CSV.tests, diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/HttpApiData.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/HttpApiData.hs new file mode 100644 index 0000000000..f910ad0a78 --- /dev/null +++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/HttpApiData.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 Test.Wire.API.Roundtrip.HttpApiData (tests) where + +import Imports +import Servant.API +import qualified Test.Tasty as T +import Test.Tasty.QuickCheck (Arbitrary, counterexample, testProperty, (===)) +import Type.Reflection (typeRep) +import qualified Wire.API.User as User +import qualified Wire.Arbitrary as Arbitrary () + +tests :: T.TestTree +tests = + T.localOption (T.Timeout (60 * 1000000) "60s") . T.testGroup "HttpApiData roundtrip tests" $ + [ testRoundTrip @User.InvitationCode + ] + +testRoundTrip :: + forall a. + (Arbitrary a, Typeable a, ToHttpApiData a, FromHttpApiData a, Eq a, Show a) => + T.TestTree +testRoundTrip = testProperty msg trip + where + msg = show (typeRep @a) + trip (v :: a) = + counterexample (show $ v) $ + Right v === (parseQueryParam . toQueryParam) v diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 0cef6f4c10..758b9659e3 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -48,6 +48,7 @@ library Wire.API.MLS.Credential Wire.API.MLS.Epoch Wire.API.MLS.Extension + Wire.API.MLS.GlobalTeamConversation Wire.API.MLS.Group Wire.API.MLS.GroupInfoBundle Wire.API.MLS.KeyPackage @@ -91,6 +92,16 @@ library Wire.API.Routes.Public.Cannon Wire.API.Routes.Public.Cargohold Wire.API.Routes.Public.Galley + Wire.API.Routes.Public.Galley.Bot + Wire.API.Routes.Public.Galley.Conversation + Wire.API.Routes.Public.Galley.CustomBackend + Wire.API.Routes.Public.Galley.Feature + Wire.API.Routes.Public.Galley.LegalHold + Wire.API.Routes.Public.Galley.Messaging + Wire.API.Routes.Public.Galley.MLS + Wire.API.Routes.Public.Galley.Team + Wire.API.Routes.Public.Galley.TeamConversation + Wire.API.Routes.Public.Galley.TeamMember Wire.API.Routes.Public.Gundeck Wire.API.Routes.Public.Spar Wire.API.Routes.Public.Util @@ -347,6 +358,8 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Generated.Email_user Test.Wire.API.Golden.Generated.EmailUpdate_provider Test.Wire.API.Golden.Generated.EmailUpdate_user + Test.Wire.API.Golden.Generated.Event_conversation + Test.Wire.API.Golden.Generated.Event_featureConfig Test.Wire.API.Golden.Generated.Event_team Test.Wire.API.Golden.Generated.Event_user Test.Wire.API.Golden.Generated.EventType_team @@ -517,6 +530,7 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Manual.ListConversations Test.Wire.API.Golden.Manual.QualifiedUserClientPrekeyMap Test.Wire.API.Golden.Manual.SearchResultContact + Test.Wire.API.Golden.Manual.TeamSize Test.Wire.API.Golden.Manual.Token Test.Wire.API.Golden.Manual.UserClientPrekeyMap Test.Wire.API.Golden.Manual.UserIdList @@ -629,6 +643,7 @@ test-suite wire-api-tests Test.Wire.API.Roundtrip.Aeson Test.Wire.API.Roundtrip.ByteString Test.Wire.API.Roundtrip.CSV + Test.Wire.API.Roundtrip.HttpApiData Test.Wire.API.Roundtrip.MLS Test.Wire.API.Routes Test.Wire.API.Swagger diff --git a/libs/wire-message-proto-lens/default.nix b/libs/wire-message-proto-lens/default.nix index 598b4edb7f..3c58511773 100644 --- a/libs/wire-message-proto-lens/default.nix +++ b/libs/wire-message-proto-lens/default.nix @@ -2,8 +2,14 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, base, Cabal, gitignoreSource, lib -, proto-lens-protoc, proto-lens-runtime, proto-lens-setup +{ mkDerivation +, base +, Cabal +, gitignoreSource +, lib +, proto-lens-protoc +, proto-lens-runtime +, proto-lens-setup }: mkDerivation { pname = "wire-message-proto-lens"; diff --git a/libs/zauth/default.nix b/libs/zauth/default.nix index 9cc4a774c0..0ff83b4703 100644 --- a/libs/zauth/default.nix +++ b/libs/zauth/default.nix @@ -2,11 +2,29 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, attoparsec, base, base64-bytestring, bytestring -, bytestring-conversion, errors, exceptions, gitignoreSource -, imports, lens, lib, mtl, mwc-random, optparse-applicative -, sodium-crypto-sign, tasty, tasty-hunit, tasty-quickcheck, text -, time, uuid, vector +{ mkDerivation +, attoparsec +, base +, base64-bytestring +, bytestring +, bytestring-conversion +, errors +, exceptions +, gitignoreSource +, imports +, lens +, lib +, mtl +, mwc-random +, optparse-applicative +, sodium-crypto-sign +, tasty +, tasty-hunit +, tasty-quickcheck +, text +, time +, uuid +, vector }: mkDerivation { pname = "zauth"; @@ -15,17 +33,47 @@ mkDerivation { isLibrary = true; isExecutable = true; libraryHaskellDepends = [ - attoparsec base base64-bytestring bytestring bytestring-conversion - errors exceptions imports lens mtl mwc-random sodium-crypto-sign - time uuid vector + attoparsec + base + base64-bytestring + bytestring + bytestring-conversion + errors + exceptions + imports + lens + mtl + mwc-random + sodium-crypto-sign + time + uuid + vector ]; executableHaskellDepends = [ - base base64-bytestring bytestring bytestring-conversion errors - imports lens optparse-applicative sodium-crypto-sign uuid + base + base64-bytestring + bytestring + bytestring-conversion + errors + imports + lens + optparse-applicative + sodium-crypto-sign + uuid ]; testHaskellDepends = [ - base bytestring bytestring-conversion errors imports lens - sodium-crypto-sign tasty tasty-hunit tasty-quickcheck text uuid + base + bytestring + bytestring-conversion + errors + imports + lens + sodium-crypto-sign + tasty + tasty-hunit + tasty-quickcheck + text + uuid ]; description = "Creation and validation of signed tokens"; license = lib.licenses.agpl3Only; diff --git a/libs/zauth/src/Data/ZAuth/Creation.hs b/libs/zauth/src/Data/ZAuth/Creation.hs index 3302428076..22542e5727 100644 --- a/libs/zauth/src/Data/ZAuth/Creation.hs +++ b/libs/zauth/src/Data/ZAuth/Creation.hs @@ -155,10 +155,10 @@ providerToken dur pid = do d <- expiry dur newToken d P Nothing (mkProvider pid) -renewToken :: ToByteString a => Integer -> Token a -> Create (Token a) -renewToken dur tkn = do +renewToken :: ToByteString a => Integer -> Header -> a -> Create (Token a) +renewToken dur hdr bdy = do d <- expiry dur - newToken d (tkn ^. header . typ) (tkn ^. header . tag) (tkn ^. body) + newToken d (hdr ^. typ) (hdr ^. tag) bdy newToken :: ToByteString a => POSIXTime -> Type -> Maybe Tag -> a -> Create (Token a) newToken ti ty ta a = do diff --git a/nix/default.nix b/nix/default.nix index 11ed4ac483..34f37250e0 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -11,16 +11,18 @@ let }; profileEnv = pkgs.writeTextFile { - name = "profile-env"; - destination = "/.profile"; - # This gets sourced by direnv. Set NIX_PATH, so `nix-shell` uses the same nixpkgs as here. - text = '' - export NIX_PATH=nixpkgs=${toString pkgs.path} - export LOCALE_ARCHIVE=${pkgs.glibcLocales}/lib/locale/locale-archive - ''; - }; + name = "profile-env"; + destination = "/.profile"; + # This gets sourced by direnv. Set NIX_PATH, so `nix-shell` uses the same nixpkgs as here. + text = '' + export NIX_PATH=nixpkgs=${toString pkgs.path} + export LOCALE_ARCHIVE=${pkgs.glibcLocales}/lib/locale/locale-archive + ''; + }; wireServer = import ./wire-server.nix pkgs; + nginz = pkgs.callPackage ./nginz.nix { }; + nginz-disco = pkgs.callPackage ./nginz-disco.nix { }; # packages necessary to build wire-server docs docsPkgs = [ @@ -65,4 +67,5 @@ let }; mls-test-cli = pkgs.mls-test-cli; rusty-jwt-tools = pkgs.rusty-jwt-tools; -in {inherit pkgs profileEnv wireServer docs docsEnv mls-test-cli;} +in +{ inherit pkgs profileEnv wireServer docs docsEnv mls-test-cli nginz nginz-disco; } diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index 43ad8c1fc9..87875e4e3a 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -54,7 +54,7 @@ # 1. Update version number. # 2. Make the 'sha256' blank string. # 3. Run step 3. from how to add a git pin. -{lib, fetchgit}: hself: hsuper: +{ lib, fetchgit }: hself: hsuper: let gitPins = { HaskellNet-SSL = { @@ -70,7 +70,7 @@ let rev = "2e3282e5fb27ba8d989c271a0a989823fad7ec43"; sha256 = "0vfzysn9sgpxymfvpahxrp74fczgjnw3kgknj6zk0473qk85488f"; }; - packages = { + packages = { wai-middleware-prometheus = "wai-middleware-prometheus"; }; }; @@ -89,7 +89,8 @@ let }; packages = { x509-store = "x509-store"; - };}; + }; + }; amazonka = { src = fetchgit { url = "https://github.com/wireapp/amazonka"; @@ -107,7 +108,8 @@ let amazonka-sqs = "lib/services/amazonka-sqs"; amazonka-sso = "lib/services/amazonka-sso"; amazonka-sts = "lib/services/amazonka-sts"; - };}; + }; + }; bloodhound = { src = fetchgit { url = "https://github.com/wireapp/bloodhound"; @@ -154,7 +156,8 @@ let http-client-openssl = "http-client-openssl"; http-client-tls = "http-client-tls"; http-conduit = "http-conduit"; - };}; + }; + }; http2 = { src = fetchgit { url = "https://github.com/wireapp/http2"; @@ -229,24 +232,39 @@ let }; # Name -> Source -> Maybe Subpath -> Drv mkGitDrv = name: src: subpath: - let subpathArg = if subpath == null - then "" - else "--subpath='${subpath}'"; - in hself.callCabal2nixWithOptions name src "${subpathArg}" {}; + let + subpathArg = + if subpath == null + then "" + else "--subpath='${subpath}'"; + in + hself.callCabal2nixWithOptions name src "${subpathArg}" { }; # [[AtrrSet]] - gitPackages = lib.attrsets.mapAttrsToList (name: pin: - let packages = if pin?packages - then pin.packages - else { "${name}" = null;}; - in lib.attrsets.mapAttrsToList (name: subpath: - {"${name}" = mkGitDrv name pin.src subpath;} - ) packages - ) gitPins; + gitPackages = lib.attrsets.mapAttrsToList + (name: pin: + let + packages = + if pin?packages + then pin.packages + else { "${name}" = null; }; + in + lib.attrsets.mapAttrsToList + (name: subpath: + { "${name}" = mkGitDrv name pin.src subpath; } + ) + packages + ) + gitPins; # AttrSet - hackagePackages = lib.attrsets.mapAttrs (pkg: {version, sha256}: - hself.callHackageDirect { - ver = version; - inherit pkg sha256; - } {} - ) hackagePins; -in lib.lists.foldr (a: b: a // b) hackagePackages (lib.lists.flatten gitPackages) + hackagePackages = lib.attrsets.mapAttrs + (pkg: { version, sha256 }: + hself.callHackageDirect + { + ver = version; + inherit pkg sha256; + } + { } + ) + hackagePins; +in +lib.lists.foldr (a: b: a // b) hackagePackages (lib.lists.flatten gitPackages) diff --git a/nix/manual-overrides.nix b/nix/manual-overrides.nix index 3a38a4435a..0cecced516 100644 --- a/nix/manual-overrides.nix +++ b/nix/manual-overrides.nix @@ -1,4 +1,4 @@ -{ libsodium, protobuf, hlib, mls-test-cli, openssl }: +{ libsodium, protobuf, hlib, mls-test-cli }: # FUTUREWORK: Figure out a way to detect if some of these packages are not # actually marked broken, so we can cleanup this file on every nixpkgs bump. hself: hsuper: { @@ -58,6 +58,4 @@ hself: hsuper: { # Make hoogle static to reduce size of the hoogle image hoogle = hlib.justStaticExecutables hsuper.hoogle; - - HsOpenSSL = hsuper.HsOpenSSL.override { inherit openssl; }; } diff --git a/nix/nginz-disco.nix b/nix/nginz-disco.nix new file mode 100644 index 0000000000..be3dabac47 --- /dev/null +++ b/nix/nginz-disco.nix @@ -0,0 +1,42 @@ +{ stdenv +, dockerTools +, gnugrep +, coreutils +, which +, dumb-init +, bashInteractive +, lib +, makeWrapper +, writers +, dig +, gawk +, diffutils +}: +let + nginz-disco = stdenv.mkDerivation { + name = "nginz-disco"; + src = (writers.writeBash "nginz_disco.sh" ../tools/nginz_disco/nginz_disco.sh); + phases = "installPhase"; + nativeBuildInputs = [ makeWrapper ]; + installPhase = '' + mkdir -p $out/bin + cp $src $out/bin/nginz_disco.sh + wrapProgram $out/bin/nginz_disco.sh \ + --prefix PATH : "${lib.makeBinPath [ gnugrep gawk dig diffutils ]}" + ''; + }; + + nginz-disco-image = dockerTools.streamLayeredImage { + name = "quay.io/wire/nginz_disco"; + maxLayers = 10; + contents = [ + bashInteractive + coreutils + which + ]; + config = { + Entrypoint = [ "${dumb-init}/bin/dumb-init" "--" "${nginz-disco}/bin/nginz_disco.sh" ]; + }; + }; +in +nginz-disco-image diff --git a/nix/nginz.nix b/nix/nginz.nix new file mode 100644 index 0000000000..67636b9180 --- /dev/null +++ b/nix/nginz.nix @@ -0,0 +1,80 @@ +{ stdenv +, symlinkJoin +, dockerTools +, writeTextDir +, runCommand +, gnugrep +, coreutils +, which +, inotify-tools +, dumb-init +, cacert +, bashInteractive +, lib +, makeWrapper +, writers +, nginz +}: +let + + nginzWithReloader = stdenv.mkDerivation { + name = "reload-script"; + src = (writers.writeBash "nginz_reload.sh" ../services/nginz/nginz_reload.sh); + phases = "installPhase"; + nativeBuildInputs = [ makeWrapper ]; + installPhase = '' + mkdir -p $out/bin + cp $src $out/bin/nginz_reload.sh + wrapProgram $out/bin/nginz_reload.sh \ + --prefix PATH : "${lib.makeBinPath [ inotify-tools nginz ]}" + ''; + }; + + # copied from nixpkgs fakeNss, but using nginx as username + nginxFakeNss = symlinkJoin { + name = "fake-nss"; + paths = [ + (writeTextDir "etc/passwd" '' + root:x:0:0:root user:/var/empty:/bin/sh + nginx:x:101:101:nginx:/var/empty:/bin/sh + nobody:x:65534:65534:nobody:/var/empty:/bin/sh + '') + (writeTextDir "etc/group" '' + root:x:0: + nginx:x:101: + nobody:x:65534: + '') + (writeTextDir "etc/nsswitch.conf" '' + hosts: files dns + '') + (runCommand "var-empty" { } '' + mkdir -p $out/var/empty + '') + # it seems nginx still tries to log, and doesn't create + # these directories automatically + (runCommand "nginx-misc" { } '' + mkdir -p $out/var/log/nginx + mkdir -p $out/var/cache/nginx + '') + ]; + }; + + nginzImage = dockerTools.streamLayeredImage { + name = "quay.io/wire/nginz"; + maxLayers = 10; + contents = [ + cacert + bashInteractive + gnugrep + which + coreutils + nginxFakeNss + nginz # so preStop lifecycle hook in cannon can nginx -c … quit + ]; + config = { + Entrypoint = [ "${dumb-init}/bin/dumb-init" "--" "${nginzWithReloader}/bin/nginz_reload.sh" "-g" "daemon off;" "-c" "/etc/wire/nginz/conf/nginx.conf" ]; + Env = [ "SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt" ]; + }; + }; +in +nginzImage diff --git a/nix/overlay.nix b/nix/overlay.nix index 917301f96d..d63b121432 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -30,10 +30,11 @@ let src = if stdenv.isDarwin then - fetchurl { - url = darwinAmd64Url; - sha256 = darwinAmd64Sha256; - } + fetchurl + { + url = darwinAmd64Url; + sha256 = darwinAmd64Sha256; + } else fetchurl { url = linuxAmd64Url; diff --git a/nix/pkgs/mls-test-cli/default.nix b/nix/pkgs/mls-test-cli/default.nix index 7d7d696113..b49f61ceaa 100644 --- a/nix/pkgs/mls-test-cli/default.nix +++ b/nix/pkgs/mls-test-cli/default.nix @@ -15,8 +15,8 @@ rustPlatform.buildRustPackage rec { src = fetchFromGitHub { owner = "wireapp"; repo = "mls-test-cli"; - sha256 = "sha256-/XQ/9oQTPkRqgMzDGRm+Oh9jgkdeDM1vRJ6/wEf2+bY="; - rev = "c6f80be2839ac1ed2894e96044541d1c3cf6ecdf"; + sha256 = "sha256-FjgAcYdUr/ZWdQxbck2UEG6NEEQLuz0S4a55hrAxUs4="; + rev = "82fc148964ef5baa92a90d086fdc61adaa2b5dbf"; }; doCheck = false; cargoSha256 = "sha256-AlZrxa7f5JwxxrzFBgeFSaYU6QttsUpfLYfq1HzsdbE="; diff --git a/nix/pkgs/python-docs/sphinx-multiversion.nix b/nix/pkgs/python-docs/sphinx-multiversion.nix index ef09691cb3..2b97e1ef91 100644 --- a/nix/pkgs/python-docs/sphinx-multiversion.nix +++ b/nix/pkgs/python-docs/sphinx-multiversion.nix @@ -1,9 +1,8 @@ -{ - buildPythonApplication, - buildPythonPackage, - fetchPypi, - - sphinx, +{ buildPythonApplication +, buildPythonPackage +, fetchPypi +, sphinx +, }: buildPythonPackage rec { pname = "sphinx-multiversion"; diff --git a/nix/pkgs/python-docs/sphinx_reredirects.nix b/nix/pkgs/python-docs/sphinx_reredirects.nix index 2985a9d775..1bff318055 100644 --- a/nix/pkgs/python-docs/sphinx_reredirects.nix +++ b/nix/pkgs/python-docs/sphinx_reredirects.nix @@ -1,9 +1,8 @@ -{ - fetchPypi, - buildPythonPackage, - - sphinx, -} : +{ fetchPypi +, buildPythonPackage +, sphinx +, +}: buildPythonPackage rec { pname = "sphinx_reredirects"; diff --git a/nix/pkgs/python-docs/sphinxcontrib-kroki.nix b/nix/pkgs/python-docs/sphinxcontrib-kroki.nix index 5cfa07c365..f65a43a135 100644 --- a/nix/pkgs/python-docs/sphinxcontrib-kroki.nix +++ b/nix/pkgs/python-docs/sphinxcontrib-kroki.nix @@ -1,11 +1,10 @@ -{ - fetchPypi, - buildPythonPackage, - - sphinx, - requests, - pyyaml, -} : +{ fetchPypi +, buildPythonPackage +, sphinx +, requests +, pyyaml +, +}: buildPythonPackage rec { pname = "sphinxcontrib-kroki"; diff --git a/nix/pkgs/python-docs/svg2rlg.nix b/nix/pkgs/python-docs/svg2rlg.nix index 7d7fd4a696..d154f21595 100644 --- a/nix/pkgs/python-docs/svg2rlg.nix +++ b/nix/pkgs/python-docs/svg2rlg.nix @@ -1,8 +1,6 @@ -{ - buildPythonPackage, - fetchPypi, - - reportlab +{ buildPythonPackage +, fetchPypi +, reportlab }: buildPythonPackage rec { pname = "svg2rlg"; @@ -12,6 +10,6 @@ buildPythonPackage rec { sha256 = "sha256-BdtEgLkOkS4Icn1MskOF/jPoQ23vB5uPFJtho1Bji+4="; }; - buildInputs = [reportlab]; + buildInputs = [ reportlab ]; doCheck = false; } diff --git a/nix/sources.json b/nix/sources.json index 5c08ee0357..2c25068a16 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -1,14 +1,14 @@ { "nixpkgs": { - "branch": "hls-hlint-plugin-workaround", - "description": "Wire's fork of NixOS/nixpkgs. Use until HLS > 1.7.0.0 is available in NixOS/nixpkgs", - "homepage": "https://github.com/wireapp/nixpkgs", - "owner": "wireapp", + "branch": "nixpkgs-unstable", + "description": "Nix Packages collection", + "homepage": "https://github.com/NixOS/nixpkgs", + "owner": "NixOS", "repo": "nixpkgs", - "rev": "0f8a37f54f802a9e8bcf3bcfa89c5ab2017d9498", - "sha256": "1g28g6m3bs8axwkih8ihnv2h8g53s267l7kpaghwxzr65bz6hj7w", + "rev": "1f3ebb2bd1a353a42e8f833895c26d8415c7b791", + "sha256": "03y1a3lv44b4fdnykyms5nd24v2mqn8acz1xa4jkbmryc29rsgcw", "type": "tarball", - "url": "https://github.com/wireapp/nixpkgs/archive/0f8a37f54f802a9e8bcf3bcfa89c5ab2017d9498.tar.gz", + "url": "https://github.com/NixOS/nixpkgs/archive/1f3ebb2bd1a353a42e8f833895c26d8415c7b791.tar.gz", "url_template": "https://github.com///archive/.tar.gz" } } diff --git a/nix/wire-server.nix b/nix/wire-server.nix index c3270518e4..c0c97594e3 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -43,277 +43,309 @@ # components and the required dependencies. We then use this package set along # with nixpkgs' dockerTools to make derivations for docker images that we need. pkgs: -let lib = pkgs.lib; - hlib = pkgs.haskell.lib; - withCleanedPath = drv: - hlib.overrideCabal drv (old: { - src = lib.cleanSourceWith { - src = old.src; - filter = path: type: - let baseName = baseNameOf (toString path); - in baseName != "dist"; - }; - }); +let + lib = pkgs.lib; + hlib = pkgs.haskell.lib; + withCleanedPath = drv: + hlib.overrideCabal drv (old: { + src = lib.cleanSourceWith { + src = old.src; + filter = path: type: + let baseName = baseNameOf (toString path); + in baseName != "dist"; + }; + }); - gitignoreSource = - let gitignoreSrc = pkgs.fetchFromGitHub { - owner = "hercules-ci"; - repo = "gitignore.nix"; - # put the latest commit sha of gitignore Nix library here: - rev = "a20de23b925fd8264fd7fad6454652e142fd7f73"; - # use what nix suggests in the mismatch message here: - sha256 = "sha256:07vg2i9va38zbld9abs9lzqblz193vc5wvqd6h7amkmwf66ljcgh"; - }; - in (import gitignoreSrc { inherit (pkgs) lib; }).gitignoreSource; + gitignoreSource = + let + gitignoreSrc = pkgs.fetchFromGitHub { + owner = "hercules-ci"; + repo = "gitignore.nix"; + # put the latest commit sha of gitignore Nix library here: + rev = "a20de23b925fd8264fd7fad6454652e142fd7f73"; + # use what nix suggests in the mismatch message here: + sha256 = "sha256:07vg2i9va38zbld9abs9lzqblz193vc5wvqd6h7amkmwf66ljcgh"; + }; + in + (import gitignoreSrc { inherit (pkgs) lib; }).gitignoreSource; - # Mapping from package -> [executable] - executablesMap = { - brig = ["brig" "brig-index" "brig-integration" "brig-schema"]; - cannon = ["cannon"]; - cargohold = ["cargohold" "cargohold-integration"]; - federator = ["federator" "federator-integration"]; - galley = ["galley" "galley-integration" "galley-schema" "galley-migrate-data"]; - gundeck = ["gundeck" "gundeck-integration" "gundeck-schema"]; - proxy = ["proxy"]; - spar = ["spar" "spar-integration" "spar-schema" "spar-migrate-data"]; - stern = ["stern"]; + # Mapping from package -> [executable] + executablesMap = { + brig = [ "brig" "brig-index" "brig-integration" "brig-schema" ]; + cannon = [ "cannon" ]; + cargohold = [ "cargohold" "cargohold-integration" ]; + federator = [ "federator" "federator-integration" ]; + galley = [ "galley" "galley-integration" "galley-schema" "galley-migrate-data" ]; + gundeck = [ "gundeck" "gundeck-integration" "gundeck-schema" ]; + proxy = [ "proxy" ]; + spar = [ "spar" "spar-integration" "spar-schema" "spar-migrate-data" ]; + stern = [ "stern" ]; - billing-team-member-backfill = ["billing-team-member-backfill"]; - api-simulations = ["api-smoketest" "api-loadtest"]; - zauth = ["zauth"]; - }; + billing-team-member-backfill = [ "billing-team-member-backfill" ]; + api-simulations = [ "api-smoketest" "api-loadtest" ]; + zauth = [ "zauth" ]; + }; - attrsets = lib.attrsets; + attrsets = lib.attrsets; - pinnedPackages = import ./haskell-pins.nix { - fetchgit = pkgs.fetchgit; - inherit lib; - }; + pinnedPackages = import ./haskell-pins.nix { + fetchgit = pkgs.fetchgit; + inherit lib; + }; - localPackages = {enableOptimization, enableDocs, enableTests}: hsuper: hself: - # The default packages are expected to have optimizations and docs turned - # on. - let defaultPkgs = import ./local-haskell-packages.nix { - inherit gitignoreSource; - } hsuper hself; + localPackages = { enableOptimization, enableDocs, enableTests }: hsuper: hself: + # The default packages are expected to have optimizations and docs turned + # on. + let + defaultPkgs = import ./local-haskell-packages.nix + { + inherit gitignoreSource; + } + hsuper + hself; - werror = _: hlib.failOnAllWarnings; - opt = _: drv: - if enableOptimization - then drv - else - # We need to explicitly add `-O0` because all the cabal files - # explicitly have `-O2` in them - hlib.appendConfigureFlag (hlib.disableOptimization drv) "--ghc-option=-O0"; - tests = _: drv: - if enableTests - then drv - else hlib.dontCheck drv; - docs = _: drv: if enableDocs - then drv - else hlib.dontHaddock drv; + werror = _: hlib.failOnAllWarnings; + opt = _: drv: + if enableOptimization + then drv + else + # We need to explicitly add `-O0` because all the cabal files + # explicitly have `-O2` in them + hlib.appendConfigureFlag (hlib.disableOptimization drv) "--ghc-option=-O0"; + tests = _: drv: + if enableTests + then drv + else hlib.dontCheck drv; + docs = _: drv: + if enableDocs + then drv + else hlib.dontHaddock drv; - overrideAll = fn: overrides: - attrsets.mapAttrs fn (overrides); - in lib.lists.foldr overrideAll defaultPkgs [ - werror - opt - docs - tests - ]; - manualOverrides = import ./manual-overrides.nix (with pkgs; { - inherit hlib libsodium protobuf mls-test-cli; - openssl = openssl_1_1; - }); + overrideAll = fn: overrides: + attrsets.mapAttrs fn (overrides); + in + lib.lists.foldr overrideAll defaultPkgs [ + werror + opt + docs + tests + ]; + manualOverrides = import ./manual-overrides.nix (with pkgs; { + inherit hlib libsodium protobuf mls-test-cli; + }); - executables = hself: hsuper: - attrsets.genAttrs (builtins.attrNames executablesMap) (e: withCleanedPath hsuper.${e}); + executables = hself: hsuper: + attrsets.genAttrs (builtins.attrNames executablesMap) (e: withCleanedPath hsuper.${e}); - staticExecutables = hself: hsuper: - attrsets.mapAttrs' (name: _: + staticExecutables = hself: hsuper: + attrsets.mapAttrs' + (name: _: attrsets.nameValuePair "${name}-static" (hlib.justStaticExecutables hsuper."${name}") - ) executablesMap; + ) + executablesMap; + + hPkgs = localMods@{ enableOptimization, enableDocs, enableTests }: pkgs.haskell.packages.ghc8107.override { + overrides = lib.composeManyExtensions [ + pinnedPackages + (localPackages localMods) + manualOverrides + executables + staticExecutables + ]; + }; - hPkgs = localMods@{enableOptimization, enableDocs, enableTests}: pkgs.haskell.packages.ghc8107.override{ - overrides = lib.composeManyExtensions [ - pinnedPackages - (localPackages localMods) - manualOverrides - executables - staticExecutables - ]; + extractExec = localMods@{ enableOptimization, enableDocs, enableTests }: hPkgName: execName: + pkgs.stdenv.mkDerivation { + name = execName; + buildInputs = [ (hPkgs localMods)."${hPkgName}-static" ]; + phases = "installPhase"; + installPhase = '' + mkdir -p $out/bin + cp "${(hPkgs localMods)."${hPkgName}-static"}/bin/${execName}" "$out/bin/${execName}" + ''; }; - extractExec = localMods@{enableOptimization, enableDocs, enableTests}: hPkgName: execName: - pkgs.stdenv.mkDerivation { - name = execName; - buildInputs = [(hPkgs localMods)."${hPkgName}-static"]; - phases = "installPhase"; - installPhase = '' - mkdir -p $out/bin - cp "${(hPkgs localMods)."${hPkgName}-static"}/bin/${execName}" "$out/bin/${execName}" - ''; - }; + # We extract static executables out of the output of building the packages + # so they don't depend on all the haskell dependencies. These exectuables + # are "static" from the perspective of ghc, i.e. they don't dynamically + # depend on other haskell packages but they still dynamically depend on C + # dependencies like openssl, cryptobox, libxml2, etc. Doing this makes the + # final images that we generate much smaller as we don't have to carry + # around so files for all haskell packages. + staticExecs = localMods@{ enableOptimization, enableDocs, enableTests }: + let + nested = attrsets.mapAttrs + (hPkgName: execNames: + attrsets.genAttrs execNames (extractExec localMods hPkgName) + ) + executablesMap; + unnested = lib.lists.foldr (x: y: x // y) { } (attrsets.attrValues nested); + in + unnested; - # We extract static executables out of the output of building the packages - # so they don't depend on all the haskell dependencies. These exectuables - # are "static" from the perspective of ghc, i.e. they don't dynamically - # depend on other haskell packages but they still dynamically depend on C - # dependencies like openssl, cryptobox, libxml2, etc. Doing this makes the - # final images that we generate much smaller as we don't have to carry - # around so files for all haskell packages. - staticExecs = localMods@{enableOptimization, enableDocs, enableTests}: - let nested = attrsets.mapAttrs (hPkgName: execNames: - attrsets.genAttrs execNames (extractExec localMods hPkgName) - ) executablesMap; - unnested = lib.lists.foldr (x: y: x // y) {} (attrsets.attrValues nested); - in unnested; + # Docker tools doesn't create tmp directories but some processes need this + # and so we have to create it ourself. + tmpDir = pkgs.runCommand "tmp-dir" { } '' + mkdir -p $out/tmp + mkdir -p $out/var/tmp + ''; - # Docker tools doesn't create tmp directories but some processes need this - # and so we have to create it ourself. - tmpDir = pkgs.runCommand "tmp-dir" {} '' - mkdir -p $out/tmp - mkdir -p $out/var/tmp + brig-templates = pkgs.stdenvNoCC.mkDerivation { + name = "brig-templates"; + src = ../services/brig/deb/opt/brig/templates; + installPhase = '' + mkdir -p $out/usr/share/wire + cp -r $src $out/usr/share/wire/templates ''; + }; - brig-templates = pkgs.stdenvNoCC.mkDerivation { - name = "brig-templates"; - src = ../services/brig/deb/opt/brig/templates; - installPhase = '' - mkdir -p $out/usr/share/wire - cp -r $src $out/usr/share/wire/templates - ''; - }; + # Some images require extra things which is not possible to specify using + # cabal file dependencies, so cabal2nix cannot automatically add these. + # + # extraContents :: Map Text [Derivation] + extraContents = { + brig = [ brig-templates ]; + brig-integration = [ brig-templates pkgs.mls-test-cli ]; + galley-integration = [ pkgs.mls-test-cli ]; + }; - # Some images require extra things which is not possible to specify using - # cabal file dependencies, so cabal2nix cannot automatically add these. - # - # extraContents :: Map Text [Derivation] - extraContents = { - brig = [brig-templates]; - brig-integration = [brig-templates pkgs.mls-test-cli]; - galley-integration= [pkgs.mls-test-cli]; - }; + # useful to poke around a container during a 'kubectl exec' + debugUtils = with pkgs; [ + bashInteractive + gnugrep + coreutils + dig + curl + less + gnutar + gzip + openssl + which + ]; - images = localMods@{enableOptimization, enableDocs, enableTests}: - attrsets.mapAttrs (execName: drv: + images = localMods@{ enableOptimization, enableDocs, enableTests }: + attrsets.mapAttrs + (execName: drv: pkgs.dockerTools.streamLayeredImage { name = "quay.io/wire/${execName}"; maxLayers = 10; contents = [ pkgs.cacert pkgs.iana-etc - pkgs.coreutils - pkgs.bashInteractive pkgs.dumb-init drv tmpDir - ] ++ pkgs.lib.optionals (builtins.hasAttr execName extraContents) (builtins.getAttr execName extraContents); + ] ++ debugUtils ++ pkgs.lib.optionals (builtins.hasAttr execName extraContents) (builtins.getAttr execName extraContents); # Any mkdir running in this step won't actually make it to the image, # hence we use the tmpDir derivation in the contents fakeRootCommands = '' - chmod 1777 tmp - chmod 1777 var/tmp - ''; + chmod 1777 tmp + chmod 1777 var/tmp + ''; config = { - Entrypoint = ["${pkgs.dumb-init}/bin/dumb-init" "--" "${drv}/bin/${execName}"]; - Env = ["SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt"]; + Entrypoint = [ "${pkgs.dumb-init}/bin/dumb-init" "--" "${drv}/bin/${execName}" ]; + Env = [ "SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt" ]; }; } - ) (staticExecs localMods); + ) + (staticExecs localMods); - localModsEnableAll = { - enableOptimization = true; - enableDocs = true; - enableTests = true; - }; - localModsOnlyTests = { - enableOptimization = false; - enableDocs = false; - enableTests = true; - }; - localModsOnlyDocs = { - enableOptimization = false; - enableDocs = true; - enableTests = false; - }; + localModsEnableAll = { + enableOptimization = true; + enableDocs = true; + enableTests = true; + }; + localModsOnlyTests = { + enableOptimization = false; + enableDocs = false; + enableTests = true; + }; + localModsOnlyDocs = { + enableOptimization = false; + enableDocs = true; + enableTests = false; + }; - imagesList = pkgs.writeTextFile { - name = "imagesList"; - text = "${lib.concatStringsSep "\n" (builtins.attrNames (images localModsEnableAll))}"; - }; - wireServerPackages = (builtins.attrNames (localPackages localModsEnableAll {} {})); + imagesList = pkgs.writeTextFile { + name = "imagesList"; + text = "${lib.concatStringsSep "\n" (builtins.attrNames (images localModsEnableAll))}"; + }; + wireServerPackages = (builtins.attrNames (localPackages localModsEnableAll { } { })); - hoogle = (hPkgs localModsOnlyDocs).hoogleWithPackages (p: builtins.map (e: p.${e}) wireServerPackages); + hoogle = (hPkgs localModsOnlyDocs).hoogleWithPackages (p: builtins.map (e: p.${e}) wireServerPackages); - # More about dockerTools.streamLayeredImage: - # https://nixos.org/manual/nixpkgs/unstable/#ssec-pkgs-dockerTools-streamLayeredImage - hoogleImage = pkgs.dockerTools.streamLayeredImage { - name = "quay.io/wire/wire-server-hoogle"; - maxLayers = 50; - contents = [ - pkgs.cacert - pkgs.coreutils - pkgs.bashInteractive - pkgs.dumb-init - hoogle - ]; - config = { - Entrypoint = ["${pkgs.dumb-init}/bin/dumb-init" "--" "${hoogle}/bin/hoogle" "server" "--local" "--host=*"]; - Env = ["SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt"]; - }; + # More about dockerTools.streamLayeredImage: + # https://nixos.org/manual/nixpkgs/unstable/#ssec-pkgs-dockerTools-streamLayeredImage + hoogleImage = pkgs.dockerTools.streamLayeredImage { + name = "quay.io/wire/wire-server-hoogle"; + maxLayers = 50; + contents = [ + pkgs.cacert + pkgs.coreutils + pkgs.bashInteractive + pkgs.dumb-init + hoogle + ]; + config = { + Entrypoint = [ "${pkgs.dumb-init}/bin/dumb-init" "--" "${hoogle}/bin/hoogle" "server" "--local" "--host=*" ]; + Env = [ "SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt" ]; }; + }; - # Tools common between CI and developers - commonTools = [ - pkgs.cabal2nix - pkgs.gnumake - pkgs.gnused - pkgs.helm - pkgs.helmfile - pkgs.hlint - pkgs.jq - pkgs.kubectl - pkgs.ormolu - pkgs.shellcheck - (hlib.justStaticExecutables pkgs.haskellPackages.cabal-fmt) - ] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ - pkgs.skopeo - ]; + # Tools common between CI and developers + commonTools = [ + pkgs.cabal2nix + pkgs.gnumake + pkgs.gnused + pkgs.helm + pkgs.helmfile + pkgs.hlint + ( hlib.justStaticExecutables pkgs.haskellPackages.apply-refact ) + pkgs.jq + pkgs.kubectl + pkgs.nixpkgs-fmt + pkgs.ormolu + pkgs.shellcheck + pkgs.treefmt + (hlib.justStaticExecutables pkgs.haskellPackages.cabal-fmt) + ] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ + pkgs.skopeo + ]; - # Building an image which can do nix builds is hard. This is programmed - # nicely in docker.nix at the root of https://github.com/nixos/nix. We get - # this file using "${pkgs.nix.src}/docker.nix" so we don't have to also pin - # the nix repository along with the nixpkgs repository. - ciImage = import "${pkgs.nix.src}/docker.nix" { - inherit pkgs; - name = "quay.io/wire/wire-server-ci"; - maxLayers = 2; - # We don't need to push the "latest" tag, every step in CI should depend - # deterministically on a specific image. - tag = null; - bundleNixpkgs = false; - extraPkgs = commonTools ++ [pkgs.cachix]; - nixConf = { - experimental-features = "nix-command"; - }; + # Building an image which can do nix builds is hard. This is programmed + # nicely in docker.nix at the root of https://github.com/nixos/nix. We get + # this file using "${pkgs.nix.src}/docker.nix" so we don't have to also pin + # the nix repository along with the nixpkgs repository. + ciImage = import "${pkgs.nix.src}/docker.nix" { + inherit pkgs; + name = "quay.io/wire/wire-server-ci"; + maxLayers = 2; + # We don't need to push the "latest" tag, every step in CI should depend + # deterministically on a specific image. + tag = null; + bundleNixpkgs = false; + extraPkgs = commonTools ++ [ pkgs.cachix ]; + nixConf = { + experimental-features = "nix-command"; }; + }; - shell = (hPkgs localModsOnlyTests).shellFor { - packages = p: builtins.map (e: p.${e}) wireServerPackages; - }; - ghcWithPackages = shell.nativeBuildInputs ++ shell.buildInputs; + shell = (hPkgs localModsOnlyTests).shellFor { + packages = p: builtins.map (e: p.${e}) wireServerPackages; + }; + ghcWithPackages = shell.nativeBuildInputs ++ shell.buildInputs; - profileEnv = pkgs.writeTextFile { - name = "profile-env"; - destination = "/.profile"; - # This gets sourced by direnv. Set NIX_PATH, so `nix-shell` uses the same nixpkgs as here. - text = '' - export NIX_PATH=nixpkgs=${toString pkgs.path} - export LOCALE_ARCHIVE=${pkgs.glibcLocales}/lib/locale/locale-archive - ''; - }; -in { + profileEnv = pkgs.writeTextFile { + name = "profile-env"; + destination = "/.profile"; + # This gets sourced by direnv. Set NIX_PATH, so `nix-shell` uses the same nixpkgs as here. + text = '' + export NIX_PATH=nixpkgs=${toString pkgs.path} + export LOCALE_ARCHIVE=${pkgs.glibcLocales}/lib/locale/locale-archive + ''; + }; +in +{ inherit ciImage hoogleImage; images = images localModsEnableAll; diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 1c8cbf0ab8..0fd8a01535 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -534,6 +534,7 @@ executable brig-integration , data-timeout , email-validate , exceptions + , extra , federator , filepath >=1.4 , galley-types diff --git a/services/brig/default.nix b/services/brig/default.nix index e4ffbf1402..cb9aa3523d 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -2,39 +2,157 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, aeson, amazonka, amazonka-dynamodb, amazonka-ses -, amazonka-sqs, async, attoparsec, auto-update, base, base-prelude -, base16-bytestring, base64-bytestring, bilge, binary, bloodhound -, brig-types, bytestring, bytestring-conversion, cargohold-types -, case-insensitive, cassandra-util, comonad, conduit, containers -, cookie, cryptobox-haskell, currency-codes, data-default -, data-timeout, dns, dns-util, either, email-validate -, enclosed-exceptions, errors, exceptions, extended, extra -, federator, file-embed, file-embed-lzma, filepath, fsnotify -, galley-types, geoip2, gitignoreSource, gundeck-types, hashable -, HaskellNet, HaskellNet-SSL, hscim, HsOpenSSL -, HsOpenSSL-x509-system, html-entities, http-api-data, http-client -, http-client-openssl, http-client-tls, http-media -, http-reverse-proxy, http-types, imports -, insert-ordered-containers, iproute, iso639, jwt-tools, lens -, lens-aeson, lib, metrics-core, metrics-wai, mime, mime-mail -, mmorph, MonadRandom, mtl, multihash, mwc-random, network -, network-conduit-tls, optparse-applicative, pem, polysemy -, polysemy-plugin, polysemy-wire-zoo, process, proto-lens -, QuickCheck, random, random-shuffle, raw-strings-qq, resource-pool -, resourcet, retry, ropes, safe, safe-exceptions, saml2-web-sso -, schema-profunctor, scientific, scrypt, servant, servant-client -, servant-client-core, servant-server, servant-swagger -, servant-swagger-ui, sodium-crypto-sign, spar, split, ssl-util -, statistics, stomp-queue, string-conversions, swagger, swagger2 -, tagged, tasty, tasty-cannon, tasty-hunit, tasty-quickcheck -, template, template-haskell, temporary, text, text-icu-translit -, time, tinylog, transformers, types-common, types-common-aws -, types-common-journal, unliftio, unordered-containers -, uri-bytestring, uuid, vector, wai, wai-extra -, wai-middleware-gunzip, wai-predicates, wai-route, wai-routing -, wai-utilities, warp, warp-tls, wire-api, wire-api-federation -, yaml, zauth +{ mkDerivation +, aeson +, amazonka +, amazonka-dynamodb +, amazonka-ses +, amazonka-sqs +, async +, attoparsec +, auto-update +, base +, base-prelude +, base16-bytestring +, base64-bytestring +, bilge +, binary +, bloodhound +, brig-types +, bytestring +, bytestring-conversion +, cargohold-types +, case-insensitive +, cassandra-util +, comonad +, conduit +, containers +, cookie +, cryptobox-haskell +, currency-codes +, data-default +, data-timeout +, dns +, dns-util +, either +, email-validate +, enclosed-exceptions +, errors +, exceptions +, extended +, extra +, federator +, file-embed +, file-embed-lzma +, filepath +, fsnotify +, galley-types +, geoip2 +, gitignoreSource +, gundeck-types +, hashable +, HaskellNet +, HaskellNet-SSL +, hscim +, HsOpenSSL +, HsOpenSSL-x509-system +, html-entities +, http-api-data +, http-client +, http-client-openssl +, http-client-tls +, http-media +, http-reverse-proxy +, http-types +, imports +, insert-ordered-containers +, iproute +, iso639 +, jwt-tools +, lens +, lens-aeson +, lib +, metrics-core +, metrics-wai +, mime +, mime-mail +, mmorph +, MonadRandom +, mtl +, multihash +, mwc-random +, network +, network-conduit-tls +, optparse-applicative +, pem +, polysemy +, polysemy-plugin +, polysemy-wire-zoo +, process +, proto-lens +, QuickCheck +, random +, random-shuffle +, raw-strings-qq +, resource-pool +, resourcet +, retry +, ropes +, safe +, safe-exceptions +, saml2-web-sso +, schema-profunctor +, scientific +, scrypt +, servant +, servant-client +, servant-client-core +, servant-server +, servant-swagger +, servant-swagger-ui +, sodium-crypto-sign +, spar +, split +, ssl-util +, statistics +, stomp-queue +, string-conversions +, swagger +, swagger2 +, tagged +, tasty +, tasty-cannon +, tasty-hunit +, tasty-quickcheck +, template +, template-haskell +, temporary +, text +, text-icu-translit +, time +, tinylog +, transformers +, types-common +, types-common-aws +, types-common-journal +, unliftio +, unordered-containers +, uri-bytestring +, uuid +, vector +, wai +, wai-extra +, wai-middleware-gunzip +, wai-predicates +, wai-route +, wai-routing +, wai-utilities +, warp +, warp-tls +, wire-api +, wire-api-federation +, yaml +, zauth }: mkDerivation { pname = "brig"; @@ -43,56 +161,256 @@ mkDerivation { isLibrary = true; isExecutable = true; libraryHaskellDepends = [ - aeson amazonka amazonka-dynamodb amazonka-ses amazonka-sqs async - attoparsec auto-update base base-prelude base16-bytestring - base64-bytestring bilge bloodhound brig-types bytestring - bytestring-conversion cassandra-util comonad conduit containers - cookie cryptobox-haskell currency-codes data-default data-timeout - dns dns-util either enclosed-exceptions errors exceptions extended - extra file-embed file-embed-lzma filepath fsnotify galley-types - geoip2 gundeck-types hashable HaskellNet HaskellNet-SSL HsOpenSSL - HsOpenSSL-x509-system html-entities http-client http-client-openssl - http-media http-types imports insert-ordered-containers iproute - iso639 jwt-tools lens lens-aeson metrics-core metrics-wai mime - mime-mail mmorph MonadRandom mtl multihash mwc-random network - network-conduit-tls optparse-applicative pem polysemy - polysemy-plugin polysemy-wire-zoo proto-lens random-shuffle - resource-pool resourcet retry ropes safe safe-exceptions - saml2-web-sso schema-profunctor scientific scrypt servant - servant-client servant-client-core servant-server servant-swagger - servant-swagger-ui sodium-crypto-sign split ssl-util statistics - stomp-queue string-conversions swagger swagger2 tagged template - template-haskell text text-icu-translit time tinylog transformers - types-common types-common-aws types-common-journal unliftio - unordered-containers uri-bytestring uuid vector wai wai-extra - wai-middleware-gunzip wai-predicates wai-routing wai-utilities warp - wire-api wire-api-federation yaml zauth + aeson + amazonka + amazonka-dynamodb + amazonka-ses + amazonka-sqs + async + attoparsec + auto-update + base + base-prelude + base16-bytestring + base64-bytestring + bilge + bloodhound + brig-types + bytestring + bytestring-conversion + cassandra-util + comonad + conduit + containers + cookie + cryptobox-haskell + currency-codes + data-default + data-timeout + dns + dns-util + either + enclosed-exceptions + errors + exceptions + extended + extra + file-embed + file-embed-lzma + filepath + fsnotify + galley-types + geoip2 + gundeck-types + hashable + HaskellNet + HaskellNet-SSL + HsOpenSSL + HsOpenSSL-x509-system + html-entities + http-client + http-client-openssl + http-media + http-types + imports + insert-ordered-containers + iproute + iso639 + jwt-tools + lens + lens-aeson + metrics-core + metrics-wai + mime + mime-mail + mmorph + MonadRandom + mtl + multihash + mwc-random + network + network-conduit-tls + optparse-applicative + pem + polysemy + polysemy-plugin + polysemy-wire-zoo + proto-lens + random-shuffle + resource-pool + resourcet + retry + ropes + safe + safe-exceptions + saml2-web-sso + schema-profunctor + scientific + scrypt + servant + servant-client + servant-client-core + servant-server + servant-swagger + servant-swagger-ui + sodium-crypto-sign + split + ssl-util + statistics + stomp-queue + string-conversions + swagger + swagger2 + tagged + template + template-haskell + text + text-icu-translit + time + tinylog + transformers + types-common + types-common-aws + types-common-journal + unliftio + unordered-containers + uri-bytestring + uuid + vector + wai + wai-extra + wai-middleware-gunzip + wai-predicates + wai-routing + wai-utilities + warp + wire-api + wire-api-federation + yaml + zauth ]; executableHaskellDepends = [ - aeson async attoparsec base base16-bytestring base64-bytestring - bilge bloodhound brig-types bytestring bytestring-conversion - cargohold-types case-insensitive cassandra-util containers cookie - data-default data-timeout email-validate exceptions extended - federator filepath galley-types gundeck-types hscim HsOpenSSL - http-api-data http-client http-client-tls http-media - http-reverse-proxy http-types imports lens lens-aeson metrics-wai - mime MonadRandom mtl network optparse-applicative pem polysemy - polysemy-wire-zoo process proto-lens QuickCheck random - random-shuffle raw-strings-qq retry safe saml2-web-sso servant - servant-client servant-client-core spar string-conversions tasty - tasty-cannon tasty-hunit temporary text time tinylog transformers - types-common types-common-aws types-common-journal unliftio - unordered-containers uri-bytestring uuid vector wai wai-extra - wai-route wai-utilities warp warp-tls wire-api wire-api-federation - yaml zauth + aeson + async + attoparsec + base + base16-bytestring + base64-bytestring + bilge + bloodhound + brig-types + bytestring + bytestring-conversion + cargohold-types + case-insensitive + cassandra-util + containers + cookie + data-default + data-timeout + email-validate + exceptions + extended + extra + federator + filepath + galley-types + gundeck-types + hscim + HsOpenSSL + http-api-data + http-client + http-client-tls + http-media + http-reverse-proxy + http-types + imports + lens + lens-aeson + metrics-wai + mime + MonadRandom + mtl + network + optparse-applicative + pem + polysemy + polysemy-wire-zoo + process + proto-lens + QuickCheck + random + random-shuffle + raw-strings-qq + retry + safe + saml2-web-sso + servant + servant-client + servant-client-core + spar + string-conversions + tasty + tasty-cannon + tasty-hunit + temporary + text + time + tinylog + transformers + types-common + types-common-aws + types-common-journal + unliftio + unordered-containers + uri-bytestring + uuid + vector + wai + wai-extra + wai-route + wai-utilities + warp + warp-tls + wire-api + wire-api-federation + yaml + zauth ]; testHaskellDepends = [ - aeson base binary bloodhound brig-types bytestring containers - data-timeout dns dns-util exceptions HsOpenSSL http-types imports - lens polysemy polysemy-wire-zoo QuickCheck retry - servant-client-core string-conversions tasty tasty-hunit - tasty-quickcheck time tinylog types-common unliftio uri-bytestring - uuid wai-utilities wire-api wire-api-federation + aeson + base + binary + bloodhound + brig-types + bytestring + containers + data-timeout + dns + dns-util + exceptions + HsOpenSSL + http-types + imports + lens + polysemy + polysemy-wire-zoo + QuickCheck + retry + servant-client-core + string-conversions + tasty + tasty-hunit + tasty-quickcheck + time + tinylog + types-common + unliftio + uri-bytestring + uuid + wai-utilities + wire-api + wire-api-federation ]; description = "User Service"; license = lib.licenses.agpl3Only; diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 8e2d114dff..dd1e233f77 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -374,10 +374,10 @@ claimLocalMultiPrekeyBundles protectee userClients = do -- | Enqueue an orderly deletion of an existing client. execDelete :: UserId -> Maybe ConnId -> Client -> (AppT r) () execDelete u con c = do - wrapClient $ Data.rmClient u (clientId c) for_ (clientCookie c) $ \l -> wrapClient $ Auth.revokeCookies u [] [l] queue <- view internalEvents Queue.enqueue queue (Internal.DeleteClient (clientId c) u con) + wrapClient $ Data.rmClient u (clientId c) -- | 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/Error.hs b/services/brig/src/Brig/API/Error.hs index f90addbec2..d688373eba 100644 --- a/services/brig/src/Brig/API/Error.hs +++ b/services/brig/src/Brig/API/Error.hs @@ -258,9 +258,6 @@ propertyValueTooLarge = Wai.mkError status403 "property-value-too-large" "The pr clientCapabilitiesCannotBeRemoved :: Wai.Error clientCapabilitiesCannotBeRemoved = Wai.mkError status409 "client-capabilities-cannot-be-removed" "You can only add capabilities to a client, not remove them." -noEmail :: Wai.Error -noEmail = Wai.mkError status403 "no-email" "This operation requires the user to have a verified email address." - 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/Public.hs b/services/brig/src/Brig/API/Public.hs index 09c233e98a..e45d35c529 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -113,7 +113,7 @@ import Wire.API.Error import qualified Wire.API.Error.Brig as E import qualified Wire.API.Properties as Public import qualified Wire.API.Routes.MultiTablePaging as Public -import Wire.API.Routes.Named +import Wire.API.Routes.Named (Named (Named)) import Wire.API.Routes.Public.Brig import qualified Wire.API.Routes.Public.Cannon as CannonAPI import qualified Wire.API.Routes.Public.Cargohold as CargoholdAPI @@ -195,6 +195,8 @@ servantSitemap = :<|> userHandleAPI :<|> searchAPI :<|> authAPI + :<|> callingAPI + :<|> Team.servantAPI where userAPI :: ServerT UserAPI (Handler r) userAPI = @@ -312,6 +314,11 @@ servantSitemap = :<|> Named @"list-cookies" listCookies :<|> Named @"remove-cookies" removeCookies + callingAPI :: ServerT CallingAPI (Handler r) + callingAPI = + Named @"get-calls-config" Calling.getCallsConfig + :<|> Named @"get-calls-config-v2" Calling.getCallsConfigV2 + -- Note [ephemeral user sideeffect] -- If the user is ephemeral and expired, it will be removed upon calling -- CheckUserExists[Un]Qualified, see 'Brig.API.User.userGC'. @@ -332,8 +339,6 @@ sitemap :: Routes Doc.ApiBuilder (Handler r) () sitemap = do Provider.routesPublic - Team.routesPublic - Calling.routesPublic apiDocs :: forall r. diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index b0a991140c..ac1a0a0eec 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -256,7 +256,7 @@ createUserSpar new = do pure account -- Add to team - userTeam <- withExceptT CreateUserSparRegistrationError $ addUserToTeamSSO account tid (SSOIdentity ident Nothing Nothing) + userTeam <- withExceptT CreateUserSparRegistrationError $ addUserToTeamSSO account tid (SSOIdentity ident Nothing Nothing) (newUserSparRole new) -- Set up feature flags let uid = userId (accountUser account) @@ -274,10 +274,10 @@ createUserSpar new = do Just handl -> withExceptT CreateUserSparHandleError $ changeHandle uid Nothing handl AllowSCIMUpdates Nothing -> throwE $ CreateUserSparHandleError ChangeHandleInvalid - addUserToTeamSSO :: UserAccount -> TeamId -> UserIdentity -> ExceptT RegisterError (AppT r) CreateUserTeam - addUserToTeamSSO account tid ident = do + addUserToTeamSSO :: UserAccount -> TeamId -> UserIdentity -> Role -> ExceptT RegisterError (AppT r) CreateUserTeam + addUserToTeamSSO account tid ident role = do let uid = userId (accountUser account) - added <- lift $ liftSem $ GalleyProvider.addTeamMember uid tid (Nothing, defaultRole) + added <- lift $ liftSem $ GalleyProvider.addTeamMember uid tid (Nothing, role) unless added $ throwE RegisterErrorTooManyTeamMembers lift $ do @@ -538,13 +538,14 @@ initAccountFeatureConfig uid = do createUserInviteViaScim :: Members '[ BlacklistStore, - UserPendingActivationStore p + UserPendingActivationStore p, + GalleyProvider ] r => UserId -> NewUserScimInvitation -> ExceptT Error.Error (AppT r) UserAccount -createUserInviteViaScim uid (NewUserScimInvitation tid loc name rawEmail) = do +createUserInviteViaScim uid (NewUserScimInvitation tid loc name rawEmail _) = do email <- either (const . throwE . Error.StdError $ errorToWai @'E.InvalidEmail) pure (validateEmail rawEmail) let emKey = userEmailKey email verifyUniquenessAndCheckBlacklist emKey !>> identityErrorToBrigError @@ -565,7 +566,6 @@ createUserInviteViaScim uid (NewUserScimInvitation tid loc name rawEmail) = do -- the SCIM user. True lift . wrapClient $ Data.insertAccount account Nothing Nothing activated - pure account -- | docs/reference/user/registration.md {#RefRestrictRegistration}. diff --git a/services/brig/src/Brig/Calling/API.hs b/services/brig/src/Brig/Calling/API.hs index c6e48162fb..41dcb19298 100644 --- a/services/brig/src/Brig/Calling/API.hs +++ b/services/brig/src/Brig/Calling/API.hs @@ -18,7 +18,8 @@ -- with this program. If not, see . module Brig.Calling.API - ( routesPublic, + ( getCallsConfig, + getCallsConfigV2, -- * Exposed for testing purposes newConfig, @@ -45,16 +46,10 @@ import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as NonEmpty import Data.Misc (HttpsUrl) import Data.Range -import qualified Data.Swagger.Build.Api as Doc import Data.Text.Ascii (AsciiBase64, encodeBase64) import Data.Text.Strict.Lens import Data.Time.Clock.POSIX (getPOSIXTime) import Imports hiding (head) -import Network.Wai (Response) -import Network.Wai.Predicate hiding (and, result, setStatus, (#)) -import Network.Wai.Routing hiding (toList) -import Network.Wai.Utilities hiding (code, message) -import Network.Wai.Utilities.Swagger (document) import OpenSSL.EVP.Digest (Digest, hmacBS) import Polysemy import qualified Polysemy.Error as Polysemy @@ -65,42 +60,6 @@ import qualified Wire.API.Call.Config as Public import Wire.Network.DNS.SRV (srvTarget) import Wire.Sem.Logger.TinyLog (loggerToTinyLog) -routesPublic :: Routes Doc.ApiBuilder (Handler r) () -routesPublic = do - -- Deprecated endpoint, but still used by old clients. - -- See https://github.com/zinfra/backend-issues/issues/1616 for context - get "/calls/config" (continue getCallsConfigH) $ - accept "application" "json" - .&. header "Z-User" - .&. header "Z-Connection" - document "GET" "getCallsConfig" $ do - Doc.deprecated - Doc.summary - "Retrieve TURN server addresses and credentials for \ - \ IP addresses, scheme `turn` and transport `udp` only " - Doc.returns (Doc.ref Public.modelRtcConfiguration) - Doc.response 200 "RTCConfiguration" Doc.end - - get "/calls/config/v2" (continue getCallsConfigV2H) $ - accept "application" "json" - .&. header "Z-User" - .&. header "Z-Connection" - .&. opt (query "limit") - document "GET" "getCallsConfigV2" $ do - Doc.summary - "Retrieve all TURN server addresses and credentials. \ - \Clients are expected to do a DNS lookup to resolve \ - \the IP addresses of the given hostnames " - Doc.parameter Doc.Query "limit" Doc.int32' $ do - Doc.description "Limit resulting list. Allowes values [1..10]" - Doc.optional - Doc.returns (Doc.ref Public.modelRtcConfiguration) - Doc.response 200 "RTCConfiguration" Doc.end - -getCallsConfigV2H :: JSON ::: UserId ::: ConnId ::: Maybe (Range 1 10 Int) -> (Handler r) Response -getCallsConfigV2H (_ ::: uid ::: connid ::: limit) = - json <$> getCallsConfigV2 uid connid limit - -- | ('UserId', 'ConnId' are required as args here to make sure this is an authenticated end-point.) getCallsConfigV2 :: UserId -> ConnId -> Maybe (Range 1 10 Int) -> (Handler r) Public.RTCConfiguration getCallsConfigV2 _ _ limit = do @@ -121,7 +80,7 @@ getCallsConfigV2 _ _ limit = do handleNoTurnServers eitherConfig -- | Throws '500 Internal Server Error' when no turn servers are found. This is --- done to keep backwards compatiblity, the previous code initialized an 'IORef' +-- done to keep backwards compatibility, the previous code initialized an 'IORef' -- with an 'error' so reading the 'IORef' threw a 500. -- -- FUTUREWORK: Making this a '404 Not Found' would be more idiomatic, but this @@ -132,10 +91,6 @@ handleNoTurnServers (Left NoTurnServers) = do Log.err $ Log.msg (Log.val "Call config requested before TURN URIs could be discovered.") throwE $ StdError internalServerError -getCallsConfigH :: JSON ::: UserId ::: ConnId -> (Handler r) Response -getCallsConfigH (_ ::: uid ::: connid) = - json <$> getCallsConfig uid connid - getCallsConfig :: UserId -> ConnId -> (Handler r) Public.RTCConfiguration getCallsConfig _ _ = do env <- view turnEnv diff --git a/services/brig/src/Brig/Effects/SFT.hs b/services/brig/src/Brig/Effects/SFT.hs index 25a3fbc012..7672876e4f 100644 --- a/services/brig/src/Brig/Effects/SFT.hs +++ b/services/brig/src/Brig/Effects/SFT.hs @@ -1,19 +1,3 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2021 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 GeneralizedNewtypeDeriving #-} -- This file is part of the Wire Server implementation. diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 26cbdc0bae..84ea17a47a 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -898,6 +898,14 @@ addBot zuid zcon cid add = do let sid = addBotService add -- Get the conversation and check preconditions cnv <- lift (liftSem $ GalleyProvider.getConv zuid cid) >>= maybeConvNotFound + -- Check that the user is a conversation admin and therefore is allowed to add a bot to this conversation. + -- Note that this precondition is also checked in the internal galley API, + -- but by having this check here we prevent any (useless) data to be written to the database + -- as well as the unnecessary creation of the bot via the external service API call. + -- However, in case we refine the roles model in the future, this check might not be granular enough. + -- In that case we should rather do an internal call to galley to check for the correct permissions. + -- Also see `removeBot` for a similar check. + guardConvAdmin cnv let mems = cnvMembers cnv unless (cnvType cnv == RegularConv) $ throwStd invalidConv @@ -974,6 +982,12 @@ removeBot :: Members '[GalleyProvider] r => UserId -> ConnId -> ConvId -> BotId removeBot zusr zcon cid bid = do -- Get the conversation and check preconditions cnv <- lift (liftSem $ GalleyProvider.getConv zusr cid) >>= maybeConvNotFound + -- Check that the user is a conversation admin and therefore is allowed to remove a bot from the conversation. + -- Note that this precondition is also checked in the internal galley API. + -- However, in case we refine the roles model in the future, this check might not be granular enough. + -- In that case we should rather do an internal call to galley to check for the correct permissions. + -- Also see `addBot` for a similar check. + guardConvAdmin cnv let mems = cnvMembers cnv unless (cnvType cnv == RegularConv) $ throwStd invalidConv @@ -985,6 +999,11 @@ removeBot zusr zcon cid bid = do Just _ -> do lift $ Public.RemoveBotResponse <$$> wrapHttpClient (deleteBot zusr (Just zcon) bid cid) +guardConvAdmin :: Conversation -> ExceptT Error (AppT r) () +guardConvAdmin conv = do + let selfMember = cmSelf . cnvMembers $ conv + unless (memConvRoleName selfMember == roleNameWireAdmin) $ throwStd accessDenied + -------------------------------------------------------------------------------- -- Bot API diff --git a/services/brig/src/Brig/RPC.hs b/services/brig/src/Brig/RPC.hs index 986ea5725f..bdba711649 100644 --- a/services/brig/src/Brig/RPC.hs +++ b/services/brig/src/Brig/RPC.hs @@ -43,6 +43,9 @@ x3 = limitRetries 3 <> exponentialBackoff 100000 zUser :: UserId -> Request -> Request zUser = header "Z-User" . toByteString' +zClient :: ClientId -> Request -> Request +zClient = header "Z-Client" . toByteString' + remote :: ByteString -> Msg -> Msg remote = field "remote" diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 7d64a9f9c9..d5814a1116 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -16,7 +16,7 @@ -- with this program. If not, see . module Brig.Team.API - ( routesPublic, + ( servantAPI, routesInternal, ) where @@ -47,12 +47,11 @@ import qualified Brig.User.Search.TeamSize as TeamSize import Control.Lens (view, (^.)) import Control.Monad.Trans.Except (mapExceptT) import Data.Aeson hiding (json) -import Data.ByteString.Conversion +import Data.ByteString.Conversion (toByteString') import Data.Id import qualified Data.List1 as List1 import Data.Range import Data.String.Conversions (cs) -import qualified Data.Swagger.Build.Api as Doc import qualified Galley.Types.Teams as Team import qualified Galley.Types.Teams.Intra as Team import Imports hiding (head) @@ -61,14 +60,15 @@ import Network.Wai (Response) import Network.Wai.Predicate hiding (and, result, setStatus) import Network.Wai.Routing import Network.Wai.Utilities hiding (code, message) -import Network.Wai.Utilities.Swagger (document) -import qualified Network.Wai.Utilities.Swagger as Doc import Polysemy (Members) +import Servant hiding (Handler, JSON, addHeader) import System.Logger (Msg) import qualified System.Logger.Class as Log import Util.Logging (logFunction, logTeam) import Wire.API.Error import qualified Wire.API.Error.Brig as E +import Wire.API.Routes.Named +import Wire.API.Routes.Public.Brig import Wire.API.Team import Wire.API.Team.Invitation import qualified Wire.API.Team.Invitation as Public @@ -77,125 +77,24 @@ import qualified Wire.API.Team.Member as Teams import Wire.API.Team.Permission (Perm (AddTeamMember)) import Wire.API.Team.Role import qualified Wire.API.Team.Role as Public -import qualified Wire.API.Team.Size as Public import Wire.API.User hiding (fromEmail) import qualified Wire.API.User as Public -routesPublic :: +servantAPI :: Members '[ BlacklistStore, GalleyProvider ] r => - Routes Doc.ApiBuilder (Handler r) () -routesPublic = do - post "/teams/:tid/invitations" (continue createInvitationPublicH) $ - accept "application" "json" - .&. header "Z-User" - .&. capture "tid" - .&. jsonRequest @Public.InvitationRequest - document "POST" "sendTeamInvitation" $ do - Doc.summary "Create and send a new team invitation." - Doc.notes - "Invitations are sent by email. The maximum allowed number of \ - \pending team invitations is equal to the team size." - Doc.parameter Doc.Path "tid" Doc.bytes' $ - Doc.description "Team ID" - Doc.body (Doc.ref Public.modelTeamInvitationRequest) $ - Doc.description "JSON body" - Doc.returns (Doc.ref Public.modelTeamInvitation) - Doc.response 201 "Invitation was created and sent." Doc.end - Doc.errorResponse noEmail - Doc.errorResponse (errorToWai @'E.NoIdentity) - Doc.errorResponse (errorToWai @'E.InvalidEmail) - Doc.errorResponse (errorToWai @'E.BlacklistedEmail) - Doc.errorResponse (errorToWai @'E.TooManyTeamInvitations) - - get "/teams/:tid/invitations" (continue listInvitationsH) $ - accept "application" "json" - .&. header "Z-User" - .&. capture "tid" - .&. opt (query "start") - .&. def (unsafeRange 100) (query "size") - document "GET" "listTeamInvitations" $ do - Doc.summary "List the sent team invitations" - Doc.parameter Doc.Path "tid" Doc.bytes' $ - Doc.description "Team ID" - Doc.parameter Doc.Query "start" Doc.string' $ do - Doc.description "Invitation id to start from (ascending)." - Doc.optional - Doc.parameter Doc.Query "size" Doc.int32' $ do - Doc.description "Number of results to return (default 100, max 500)." - Doc.optional - Doc.returns (Doc.ref Public.modelTeamInvitationList) - Doc.response 200 "List of sent invitations" Doc.end - - get "/teams/:tid/invitations/:iid" (continue getInvitationH) $ - accept "application" "json" - .&. header "Z-User" - .&. capture "tid" - .&. capture "iid" - document "GET" "getInvitation" $ do - Doc.summary "Get a pending team invitation by ID." - Doc.parameter Doc.Path "tid" Doc.bytes' $ - Doc.description "Team ID" - Doc.parameter Doc.Path "id" Doc.bytes' $ - Doc.description "Team Invitation ID" - Doc.returns (Doc.ref Public.modelTeamInvitation) - Doc.response 200 "Invitation" Doc.end - - delete "/teams/:tid/invitations/:iid" (continue deleteInvitationH) $ - accept "application" "json" - .&. header "Z-User" - .&. capture "tid" - .&. capture "iid" - document "DELETE" "deleteInvitation" $ do - Doc.summary "Delete a pending invitation by ID." - Doc.parameter Doc.Path "tid" Doc.bytes' $ - Doc.description "Team ID" - Doc.parameter Doc.Path "iid" Doc.bytes' $ - Doc.description "Team Invitation ID" - Doc.response 200 "Invitation deleted." Doc.end - - get "/teams/invitations/info" (continue getInvitationByCodeH) $ - accept "application" "json" - .&. query "code" - document "GET" "getInvitationInfo" $ do - Doc.summary "Get invitation info given a code." - Doc.parameter Doc.Query "code" Doc.bytes' $ - Doc.description "Invitation code" - Doc.returns (Doc.ref Public.modelTeamInvitation) - Doc.response 200 "Invitation successful." Doc.end - Doc.errorResponse (errorToWai @'E.InvalidInvitationCode) - - -- FUTUREWORK: Add another endpoint to allow resending of invitation codes - head "/teams/invitations/by-email" (continue headInvitationByEmailH) $ - accept "application" "json" - .&. query "email" - - document "HEAD" "headInvitationPending" $ do - Doc.summary "Check if there is an invitation pending given an email address." - Doc.parameter Doc.Query "email" Doc.bytes' $ - Doc.description "Email address" - Doc.response 200 "Pending invitation exists." Doc.end - Doc.response 404 "No pending invitations exists." Doc.end - Doc.response 409 "Multiple conflicting invitations to different teams exists." Doc.end - - get "/teams/:tid/size" (continue teamSizePublicH) $ - accept "application" "json" - .&. header "Z-User" - .&. capture "tid" - - document "GET" "teamSize" $ do - Doc.summary - "Returns the number of team members as an integer. \ - \Can be out of sync by roughly the `refresh_interval` \ - \of the ES index." - Doc.parameter Doc.Path "tid" Doc.bytes' $ - Doc.description "Team ID" - Doc.returns (Doc.ref Public.modelTeamSize) - Doc.response 200 "Invitation successful." Doc.end - Doc.response 403 "No permission (not admin or owner of this team)." Doc.end + ServerT TeamsAPI (Handler r) +servantAPI = + Named @"send-team-invitation" createInvitationPublicH + :<|> Named @"get-team-invitations" listInvitations + :<|> Named @"get-team-invitation" getInvitation + :<|> Named @"delete-team-invitation" deleteInvitation + :<|> Named @"get-team-invitation-info" getInvitationByCode + :<|> Named @"head-team-invitations" headInvitationByEmail + :<|> Named @"get-team-size" teamSizePublic routesInternal :: Members @@ -231,9 +130,6 @@ routesInternal = do accept "application" "json" .&. jsonRequest @NewUserScimInvitation -teamSizePublicH :: Members '[GalleyProvider] r => JSON ::: UserId ::: TeamId -> (Handler r) Response -teamSizePublicH (_ ::: uid ::: tid) = json <$> teamSizePublic uid tid - teamSizePublic :: Members '[GalleyProvider] r => UserId -> TeamId -> (Handler r) TeamSize teamSizePublic uid tid = do ensurePermissions uid tid [AddTeamMember] -- limit this to team admins to reduce risk of involuntary DOS attacks @@ -266,16 +162,17 @@ createInvitationPublicH :: GalleyProvider ] r => - JSON ::: UserId ::: TeamId ::: JsonRequest Public.InvitationRequest -> - (Handler r) Response -createInvitationPublicH (_ ::: uid ::: tid ::: req) = do - body <- parseJsonBody req - newInv <- createInvitationPublic uid tid body - pure . setStatus status201 . loc (inInvitation newInv) . json $ newInv + UserId -> + TeamId -> + Public.InvitationRequest -> + Handler r (Public.Invitation, Public.InvitationLocation) +createInvitationPublicH uid tid body = do + inv <- createInvitationPublic uid tid body + pure (inv, loc inv) where - loc iid = - addHeader "Location" $ - "/teams/" <> toByteString' tid <> "/invitations/" <> toByteString' iid + loc :: Invitation -> InvitationLocation + loc inv = + InvitationLocation $ "/teams/" <> toByteString' tid <> "/invitations/" <> toByteString' (inInvitation inv) data CreateInvitationInviter = CreateInvitationInviter { inviterUid :: UserId, @@ -298,7 +195,7 @@ createInvitationPublic uid tid body = do inviter <- do let inviteePerms = Team.rolePermissions inviteeRole idt <- maybe (throwStd (errorToWai @'E.NoIdentity)) pure =<< lift (fetchUserIdentity uid) - from <- maybe (throwStd noEmail) pure (emailIdentity idt) + from <- maybe (throwStd (errorToWai @'E.NoEmail)) pure (emailIdentity idt) ensurePermissionToAddUser uid tid inviteePerms pure $ CreateInvitationInviter uid from @@ -334,9 +231,9 @@ createInvitationViaScim :: r => NewUserScimInvitation -> (Handler r) UserAccount -createInvitationViaScim newUser@(NewUserScimInvitation tid loc name email) = do +createInvitationViaScim newUser@(NewUserScimInvitation tid loc name email role) = do env <- ask - let inviteeRole = defaultRole + let inviteeRole = role fromEmail = env ^. emailSender invreq = InvitationRequest @@ -436,55 +333,37 @@ createInvitation' tid inviteeRole mbInviterUid fromEmail body = do timeout (newInv, code) <$ sendInvitationMail inviteeEmail tid fromEmail code locale -deleteInvitationH :: Members '[GalleyProvider] r => JSON ::: UserId ::: TeamId ::: InvitationId -> (Handler r) Response -deleteInvitationH (_ ::: uid ::: tid ::: iid) = do - empty <$ deleteInvitation uid tid iid - deleteInvitation :: Members '[GalleyProvider] r => UserId -> TeamId -> InvitationId -> (Handler r) () deleteInvitation uid tid iid = do ensurePermissions uid tid [AddTeamMember] lift $ wrapClient $ DB.deleteInvitation tid iid -listInvitationsH :: Members '[GalleyProvider] r => JSON ::: UserId ::: TeamId ::: Maybe InvitationId ::: Range 1 500 Int32 -> (Handler r) Response -listInvitationsH (_ ::: uid ::: tid ::: start ::: size) = do - json <$> listInvitations uid tid start size - -listInvitations :: Members '[GalleyProvider] r => UserId -> TeamId -> Maybe InvitationId -> Range 1 500 Int32 -> (Handler r) Public.InvitationList -listInvitations uid tid start size = do +listInvitations :: Members '[GalleyProvider] r => UserId -> TeamId -> Maybe InvitationId -> Maybe (Range 1 500 Int32) -> (Handler r) Public.InvitationList +listInvitations uid tid start mSize = do ensurePermissions uid tid [AddTeamMember] showInvitationUrl <- lift $ liftSem $ GalleyProvider.getExposeInvitationURLsToTeamAdmin tid - rs <- lift $ wrapClient $ DB.lookupInvitations showInvitationUrl tid start size + rs <- lift $ wrapClient $ DB.lookupInvitations showInvitationUrl tid start (fromMaybe (unsafeRange 100) mSize) pure $! Public.InvitationList (DB.resultList rs) (DB.resultHasMore rs) -getInvitationH :: Members '[GalleyProvider] r => JSON ::: UserId ::: TeamId ::: InvitationId -> (Handler r) Response -getInvitationH (_ ::: uid ::: tid ::: iid) = do - inv <- getInvitation uid tid iid - pure $ case inv of - Just i -> json i - Nothing -> setStatus status404 empty - getInvitation :: Members '[GalleyProvider] r => UserId -> TeamId -> InvitationId -> (Handler r) (Maybe Public.Invitation) getInvitation uid tid iid = do ensurePermissions uid tid [AddTeamMember] showInvitationUrl <- lift $ liftSem $ GalleyProvider.getExposeInvitationURLsToTeamAdmin tid lift $ wrapClient $ DB.lookupInvitation showInvitationUrl tid iid -getInvitationByCodeH :: JSON ::: Public.InvitationCode -> (Handler r) Response -getInvitationByCodeH (_ ::: c) = do - json <$> getInvitationByCode c - getInvitationByCode :: Public.InvitationCode -> (Handler r) Public.Invitation getInvitationByCode c = do inv <- lift . wrapClient $ DB.lookupInvitationByCode HideInvitationUrl c maybe (throwStd $ errorToWai @'E.InvalidInvitationCode) pure inv -headInvitationByEmailH :: JSON ::: Email -> (Handler r) Response -headInvitationByEmailH (_ ::: e) = do - inv <- lift $ wrapClient $ DB.lookupInvitationInfoByEmail e - pure $ case inv of - DB.InvitationByEmail _ -> setStatus status200 empty - DB.InvitationByEmailNotFound -> setStatus status404 empty - DB.InvitationByEmailMoreThanOne -> setStatus status409 empty +headInvitationByEmail :: Email -> (Handler r) Public.HeadInvitationByEmailResult +headInvitationByEmail e = do + lift $ + wrapClient $ + DB.lookupInvitationInfoByEmail e <&> \case + DB.InvitationByEmail _ -> Public.InvitationByEmail + DB.InvitationByEmailNotFound -> Public.InvitationByEmailNotFound + DB.InvitationByEmailMoreThanOne -> Public.InvitationByEmailMoreThanOne -- | FUTUREWORK: This should also respond with status 409 in case of -- @DB.InvitationByEmailMoreThanOne@. Refactor so that 'headInvitationByEmailH' and diff --git a/services/brig/src/Brig/Team/DB.hs b/services/brig/src/Brig/Team/DB.hs index 4af2ad91ec..f48a42534a 100644 --- a/services/brig/src/Brig/Team/DB.hs +++ b/services/brig/src/Brig/Team/DB.hs @@ -32,8 +32,8 @@ module Brig.Team.DB lookupInvitationByEmail, mkInvitationCode, mkInvitationId, - InvitationInfo (..), InvitationByEmail (..), + InvitationInfo (..), ) where @@ -61,7 +61,7 @@ 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.Invitation hiding (HeadInvitationByEmailResult (..)) import Wire.API.Team.Role import Wire.API.User diff --git a/services/brig/src/Brig/User/Auth/Cookie.hs b/services/brig/src/Brig/User/Auth/Cookie.hs index 11b9e6ee60..a72d560de9 100644 --- a/services/brig/src/Brig/User/Auth/Cookie.hs +++ b/services/brig/src/Brig/User/Auth/Cookie.hs @@ -193,7 +193,7 @@ newAccessToken :: newAccessToken c mt = do t' <- case mt of Nothing -> ZAuth.newAccessToken (cookieValue c) - Just t -> ZAuth.renewAccessToken t + Just t -> ZAuth.renewAccessToken (ZAuth.userTokenClient (cookieValue c)) t zSettings <- view (zauthEnv . ZAuth.settings) let ttl = view (ZAuth.settingsTTL (Proxy @a)) zSettings pure $ diff --git a/services/brig/src/Brig/ZAuth.hs b/services/brig/src/Brig/ZAuth.hs index f0425dd037..055352cfa6 100644 --- a/services/brig/src/Brig/ZAuth.hs +++ b/services/brig/src/Brig/ZAuth.hs @@ -86,7 +86,7 @@ module Brig.ZAuth ) where -import Control.Lens (Lens', makeLenses, over, (^.)) +import Control.Lens (Lens', makeLenses, over, (.~), (^.)) import Control.Monad.Catch import Data.Aeson import Data.Bits @@ -238,7 +238,7 @@ instance TokenPair LegalHoldUser LegalHoldAccess where class (FromByteString (Token a), ToByteString a) => AccessTokenLike a where accessTokenOf :: Token a -> UserId accessTokenClient :: Token a -> Maybe ClientId - renewAccessToken :: MonadZAuth m => Token a -> m (Token a) + renewAccessToken :: MonadZAuth m => Maybe ClientId -> Token a -> m (Token a) settingsTTL :: Proxy a -> Lens' Settings Integer instance AccessTokenLike Access where @@ -319,13 +319,19 @@ newAccessToken' xt = liftZAuth $ do let AccessTokenTimeout ttl = z ^. settings . accessTokenTimeout in ZC.accessToken1 ttl (xt ^. body . user) (xt ^. body . client) -renewAccessToken' :: MonadZAuth m => Token Access -> m (Token Access) -renewAccessToken' old = liftZAuth $ do +renewAccessToken' :: MonadZAuth m => Maybe ClientId -> Token Access -> m (Token Access) +renewAccessToken' mcid old = liftZAuth $ do z <- ask liftIO $ ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ let AccessTokenTimeout ttl = z ^. settings . accessTokenTimeout - in ZC.renewToken ttl old + in ZC.renewToken + ttl + (old ^. header) + ( clientId + .~ fmap Data.Id.client mcid + $ (old ^. body) + ) newBotToken :: MonadZAuth m => ProviderId -> BotId -> ConvId -> m (Token Bot) newBotToken pid bid cid = liftZAuth $ do @@ -385,13 +391,20 @@ newLegalHoldAccessToken xt = liftZAuth $ do (xt ^. body . legalHoldUser . user) (xt ^. body . legalHoldUser . client) -renewLegalHoldAccessToken :: MonadZAuth m => Token LegalHoldAccess -> m (Token LegalHoldAccess) -renewLegalHoldAccessToken old = liftZAuth $ do +renewLegalHoldAccessToken :: + MonadZAuth m => + Maybe ClientId -> + Token LegalHoldAccess -> + m (Token LegalHoldAccess) +renewLegalHoldAccessToken _mcid old = liftZAuth $ do z <- ask liftIO $ ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ let LegalHoldAccessTokenTimeout ttl = z ^. settings . legalHoldAccessTokenTimeout - in ZC.renewToken ttl old + in ZC.renewToken + ttl + (old ^. header) + (old ^. body) validateToken :: (MonadZAuth m, ToByteString a) => diff --git a/services/brig/test/integration/API/Provider.hs b/services/brig/test/integration/API/Provider.hs index 1449ce29c5..727560b07d 100644 --- a/services/brig/test/integration/API/Provider.hs +++ b/services/brig/test/integration/API/Provider.hs @@ -148,6 +148,7 @@ tests dom conf p db b c g = do testGroup "bot-teams" [ test p "add-remove" $ testAddRemoveBotTeam conf db b g c, + test p "add-remove-access-denied-for-non-conv-admin" $ testNonConvAdminCannotAddRemoveBot conf db b g, test p "team-only" $ testBotTeamOnlyConv conf db b g c, test p "message" $ testMessageBotTeam conf db b g c, test p "delete conv" $ testDeleteConvBotTeam conf db b g c, @@ -566,6 +567,30 @@ testAddBotBlocked config db brig galley = withTestService config db brig defServ const 403 === statusCode const (Just "access-denied") === fmap Error.label . responseJsonMaybe +testNonConvAdminCannotAddRemoveBot :: Config -> DB.ClientState -> Brig -> Galley -> Http () +testNonConvAdminCannotAddRemoveBot config db brig galley = withTestService config db brig defServiceApp $ \sref _buf -> do + let pid = sref ^. serviceRefProvider + let sid = sref ^. serviceRefId + (ownerId, tid) <- Team.createUserWithTeam brig + member <- Team.createTeamMember brig galley ownerId tid fullPermissions + let memberId = userId member + whitelistService brig ownerId tid pid sid + cid <- Team.createTeamConvWithRole roleNameWireMember galley tid ownerId [memberId] Nothing + addBot brig memberId pid sid cid !!! do + const 403 === statusCode + const (Just "access-denied") === fmap Error.label . responseJsonMaybe + rs <- responseJsonError =<< addBot brig ownerId pid sid cid DB.ClientState -> Brig -> Galley -> Cannon -> Http () testGetBotConvBlocked config db brig galley cannon = withTestService config db brig defServiceApp $ \sref buf -> do (user1, userId -> u2, _, tid, cid, pid, sid) <- prepareBotUsersTeam brig galley sref @@ -1305,6 +1330,31 @@ removeBot brig uid cid bid = . header "Z-User" (toByteString' uid) . header "Z-Connection" "conn" +data RemoveBot = RemoveBot + { _rmBotConv :: !ConvId, + _rmBotId :: !BotId + } + +instance ToJSON RemoveBot where + toJSON a = + object + [ "conversation" .= _rmBotConv a, + "bot" .= _rmBotId a + ] + +removeBotInternal :: + Galley -> + UserId -> + ConvId -> + BotId -> + Http ResponseLBS +removeBotInternal galley uid cid bid = + delete $ + galley + . paths ["i", "bots"] + . header "Z-User" (toByteString' uid) + . Bilge.json (RemoveBot cid bid) + createConv :: Galley -> UserId -> @@ -1328,7 +1378,19 @@ createConvWithAccessRoles ars g u us = . contentJson . body (RequestBodyLBS (encode conv)) where - conv = NewConv us [] Nothing Set.empty ars Nothing Nothing Nothing roleNameWireAdmin ProtocolProteusTag Nothing + conv = + NewConv + us + [] + Nothing + Set.empty + ars + Nothing + Nothing + Nothing + roleNameWireAdmin + ProtocolProteusTag + Nothing postMessage :: Galley -> diff --git a/services/brig/test/integration/API/Team.hs b/services/brig/test/integration/API/Team.hs index 7c44cc73be..7df81805cf 100644 --- a/services/brig/test/integration/API/Team.hs +++ b/services/brig/test/integration/API/Team.hs @@ -36,21 +36,29 @@ import Control.Lens hiding ((.=)) import Control.Monad.Catch (MonadCatch, MonadThrow) import Data.Aeson import Data.ByteString.Conversion +import Data.ByteString.Lazy (toStrict) +import Data.Either.Extra (eitherToMaybe) import Data.Id hiding (client) import Data.Json.Util (UTCTimeMillis, toUTCTimeMillis) +import Data.LegalHold (UserLegalHoldStatus (UserLegalHoldDisabled)) +import qualified Data.Text as Text import qualified Data.Text.Ascii as Ascii +import Data.Text.Encoding (encodeUtf8) import Data.Time (addUTCTime, getCurrentTime) import qualified Data.UUID as UUID (fromString) import qualified Data.UUID.V4 as UUID import qualified Galley.Types.Teams as Team import qualified Galley.Types.Teams.Intra as Team import Imports +import qualified Network.HTTP.Types as HTTP +import qualified Network.Wai as Wai import qualified Network.Wai.Test as WaiTest import qualified Network.Wai.Utilities.Error as Error import Numeric.Natural (Natural) import Test.Tasty hiding (Timeout) import qualified Test.Tasty.Cannon as WS import Test.Tasty.HUnit +import URI.ByteString import UnliftIO.Async (mapConcurrently_, pooledForConcurrentlyN_, replicateConcurrently) import Util import Util.AWS as Util @@ -58,6 +66,7 @@ import Web.Cookie (parseSetCookie, setCookieName) import Wire.API.Asset import Wire.API.Connection import Wire.API.Team hiding (newTeam) +import Wire.API.Team.Feature import qualified Wire.API.Team.Feature as Public import Wire.API.Team.Invitation import Wire.API.Team.Member hiding (invitation, userId) @@ -80,6 +89,10 @@ tests conf m n b c g aws = do "team" [ testGroup "invitation" $ [ test m "post /teams/:tid/invitations - 201" $ testInvitationEmail b, + test m "get /teams/:tid/invitations/:iid - 200" $ testGetInvitation b, + test m "delete /teams/:tid/invitations/:iid - 200" $ testDeleteInvitation b, + test m "post /teams/:tid/invitations - invitation url" $ testInvitationUrl conf b, + test m "post /teams/:tid/invitations - no invitation url" $ testNoInvitationUrl conf b, test m "post /teams/:tid/invitations - email lookup" $ testInvitationEmailLookup b, test m "post /teams/:tid/invitations - email lookup nginz" $ testInvitationEmailLookupNginz b n, test m "post /teams/:tid/invitations - email lookup register" $ testInvitationEmailLookupRegister b, @@ -114,26 +127,37 @@ tests conf m n b c g aws = do 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] + testGroup "size" $ + [ test m "get /i/teams/:tid/size" $ testTeamSizeInternal b, + test m "get /teams/:tid/size" $ testTeamSizePublic b + ] ] -testTeamSize :: Brig -> Http () -testTeamSize brig = do - (tid, _, _) <- createPopulatedBindingTeam brig 10 +testTeamSizeInternal :: Brig -> Http () +testTeamSizeInternal brig = do + testTeamSize brig (\tid _ -> brig . paths ["i", "teams", toByteString' tid, "size"]) + +testTeamSizePublic :: Brig -> Http () +testTeamSizePublic brig = do + testTeamSize brig (\tid uid -> brig . paths ["teams", toByteString' tid, "size"] . zUser uid) + +testTeamSize :: Brig -> (TeamId -> UserId -> Request -> Request) -> Http () +testTeamSize brig req = do + (tid, owner, _) <- createPopulatedBindingTeam brig 10 SearchUtil.refreshIndex brig -- 10 Team Members and an admin let expectedSize = 11 - assertSize tid expectedSize + assertSize tid owner expectedSize -- Even suspended teams should report correct size suspendTeam brig tid !!! const 200 === statusCode SearchUtil.refreshIndex brig - assertSize tid expectedSize + assertSize tid owner expectedSize where - assertSize :: HasCallStack => TeamId -> Natural -> Http () - assertSize tid expectedSize = + assertSize :: HasCallStack => TeamId -> UserId -> Natural -> Http () + assertSize tid uid expectedSize = void $ - get (brig . paths ["i", "teams", toByteString' tid, "size"]) Http () testInvitationEmail brig = do (inviter, tid) <- createUserWithTeam brig invite <- stdInvitationRequest <$> randomEmail - void $ postInvitation brig tid inviter invite + res <- + postInvitation brig tid inviter invite toByteString' tid <> "/invitations/" <> toByteString' (inInvitation inv) + liftIO $ do + Just inviter @=? inCreatedBy inv + tid @=? inTeam inv + assertInvitationResponseInvariants invite inv + (isNothing . inInviteeUrl) inv @? "No invitation url expected" + actualHeader @?= Just expectedHeader + +assertInvitationResponseInvariants :: InvitationRequest -> Invitation -> Assertion +assertInvitationResponseInvariants invReq inv = do + irInviteeName invReq @=? inInviteeName inv + irInviteePhone invReq @=? inInviteePhone inv + irInviteeEmail invReq @=? inInviteeEmail inv + +testGetInvitation :: Brig -> Http () +testGetInvitation brig = do + (inviter, tid) <- createUserWithTeam brig + invite <- stdInvitationRequest <$> randomEmail + inv1 <- responseJsonError =<< postInvitation brig tid inviter invite Http () +testDeleteInvitation brig = do + (inviter, tid) <- createUserWithTeam brig + invite <- stdInvitationRequest <$> randomEmail + iid <- inInvitation <$> (responseJsonError =<< postInvitation brig tid inviter invite Brig -> Http () +testInvitationUrl opts brig = do + (inviter, tid) <- createUserWithTeam brig + invite <- stdInvitationRequest <$> randomEmail + + void . withMockedGalley opts (invitationUrlGalleyMock FeatureStatusEnabled tid inviter) $ do + resp <- + postInvitation brig tid inviter invite + (toStrict . toByteString)) + getQueryParam "team" resp @=? (pure . encodeUtf8 . idToText) tid + +getQueryParam :: ByteString -> ResponseLBS -> Maybe ByteString +getQueryParam name r = do + inv <- (eitherToMaybe . responseJsonEither) r + url <- inInviteeUrl inv + (lookup name . queryPairs . uriQuery) url + +-- | Mock the feature API because exposeInvitationURLsToTeamAdmin depends on +-- static configuration that cannot be changed at runtime. +invitationUrlGalleyMock :: + FeatureStatus -> + TeamId -> + UserId -> + ReceivedRequest -> + MockT IO Wai.Response +invitationUrlGalleyMock featureStatus tid inviter (ReceivedRequest mth pth _body) + | mth == "GET" + && pth == ["i", "teams", Text.pack (show tid), "features", "exposeInvitationURLsToTeamAdmin"] = + pure . Wai.responseLBS HTTP.status200 mempty $ + encode + ( withStatus + featureStatus + LockStatusUnlocked + ExposeInvitationURLsToTeamAdminConfig + FeatureTTLUnlimited + ) + | mth == "GET" + && pth == ["i", "teams", Text.pack (show tid), "members", Text.pack (show inviter)] = + pure . Wai.responseLBS HTTP.status200 mempty $ + encode (mkTeamMember inviter fullPermissions Nothing UserLegalHoldDisabled) + | otherwise = pure $ Wai.responseLBS HTTP.status500 mempty "Unexpected request to mocked galley" + +-- FUTUREWORK: This test should be rewritten to be free of mocks once Galley is +-- inlined into Brig. +testNoInvitationUrl :: Opt.Opts -> Brig -> Http () +testNoInvitationUrl opts brig = do + (inviter, tid) <- createUserWithTeam brig + invite <- stdInvitationRequest <$> randomEmail + + void . withMockedGalley opts (invitationUrlGalleyMock FeatureStatusDisabled tid inviter) $ do + resp <- + postInvitation brig tid inviter invite + Http () testInvitationEmailLookup brig = do @@ -366,7 +497,7 @@ createAndVerifyInvitation' replacementBrigApp acceptFn invite brig galley = do inv <- responseJsonError =<< postInvitation brig tid inviter invite let invmeta = Just (inviter, inCreatedAt inv) Just inviteeCode <- getInvitationCode brig tid (inInvitation inv) - Just invitation <- getInvitation brig inviteeCode + Just invitation <- getInvitationInfo brig inviteeCode rsp2 <- post ( brig @@ -473,8 +604,7 @@ testTeamNoPassword brig = do ] ) ) - !!! const 400 - === statusCode + !!! const 400 === statusCode -- And so do any other binding team members code <- liftIO $ InvitationCode . Ascii.encodeBase64Url <$> randomBytes 24 post @@ -490,8 +620,7 @@ testTeamNoPassword brig = do ] ) ) - !!! const 400 - === statusCode + !!! const 400 === statusCode testInvitationCodeExists :: Brig -> Http () testInvitationCodeExists brig = do @@ -610,7 +739,8 @@ testInvitationPaging brig = do let range = queryRange (toByteString' <$> start) (Just step) r <- get (brig . paths ["teams", toByteString' tid, "invitations"] . zUser uid . range) - responseJsonMaybe r liftIO $ assertEqual "page size" actualPageLen (length invs) liftIO $ assertEqual "has more" (count' < total) more @@ -637,7 +767,7 @@ testInvitationInfo brig = do let invite = stdInvitationRequest email inv <- responseJsonError =<< postInvitation brig tid uid invite Just invCode <- getInvitationCode brig tid (inInvitation inv) - Just invitation <- getInvitation brig invCode + Just invitation <- getInvitationInfo brig invCode liftIO $ assertEqual "Invitations differ" inv invitation testInvitationInfoBadCode :: Brig -> Http () diff --git a/services/brig/test/integration/API/Team/Util.hs b/services/brig/test/integration/API/Team/Util.hs index 6fa61788ab..c1a6724b9d 100644 --- a/services/brig/test/integration/API/Team/Util.hs +++ b/services/brig/test/integration/API/Team/Util.hs @@ -214,10 +214,24 @@ updatePermissions from tid (to, perm) galley = changeMember = Member.mkNewTeamMember to perm Nothing createTeamConv :: HasCallStack => Galley -> TeamId -> UserId -> [UserId] -> Maybe Milliseconds -> Http ConvId -createTeamConv g tid u us mtimer = do +createTeamConv = createTeamConvWithRole roleNameWireAdmin + +createTeamConvWithRole :: HasCallStack => RoleName -> Galley -> TeamId -> UserId -> [UserId] -> Maybe Milliseconds -> Http ConvId +createTeamConvWithRole role g tid u us mtimer = do let tinfo = Just $ ConvTeamInfo tid let conv = - NewConv us [] Nothing (Set.fromList []) Nothing tinfo mtimer Nothing roleNameWireAdmin ProtocolProteusTag Nothing + NewConv + us + [] + Nothing + (Set.fromList []) + Nothing + tinfo + mtimer + Nothing + role + ProtocolProteusTag + Nothing r <- post ( g @@ -350,8 +364,8 @@ register' e t c brig = ) ) -getInvitation :: Brig -> InvitationCode -> (MonadIO m, MonadHttp m) => m (Maybe Invitation) -getInvitation brig c = do +getInvitationInfo :: Brig -> InvitationCode -> (MonadIO m, MonadHttp m) => m (Maybe Invitation) +getInvitationInfo brig c = do r <- get $ brig @@ -359,6 +373,14 @@ getInvitation brig c = do . queryItem "code" (toByteString' c) pure . decode . fromMaybe "" $ responseBody r +getInvitation :: Brig -> TeamId -> InvitationId -> UserId -> Http ResponseLBS +getInvitation brig tid iid uid = + get (brig . paths ["teams", toByteString' tid, "invitations", toByteString' iid] . zUser uid) + +deleteInvitation :: Brig -> TeamId -> InvitationId -> UserId -> Http () +deleteInvitation brig tid iid uid = + delete (brig . paths ["teams", toByteString' tid, "invitations", toByteString' iid] . zUser uid) !!! const 200 === statusCode + postInvitation :: (MonadIO m, MonadHttp m, HasCallStack) => Brig -> diff --git a/services/brig/test/integration/API/TeamUserSearch.hs b/services/brig/test/integration/API/TeamUserSearch.hs index 68c0f00768..26159afdcd 100644 --- a/services/brig/test/integration/API/TeamUserSearch.hs +++ b/services/brig/test/integration/API/TeamUserSearch.hs @@ -1,5 +1,3 @@ -{-# OPTIONS_GHC -Wno-unused-imports #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -24,16 +22,13 @@ import API.Team.Util (createPopulatedBindingTeamWithNamesAndHandles) import API.User.Util (activateEmail, initiateEmailUpdateNoSend) import Bilge (Manager, MonadHttp) import qualified Brig.Options as Opt -import Brig.User.Search.TeamUserSearch (TeamUserSearchSortBy (..), TeamUserSearchSortOrder (..)) import Control.Monad.Catch (MonadCatch) import Control.Retry () -import Data.ByteString.Conversion (ToByteString (..), toByteString) +import Data.ByteString.Conversion (toByteString) import Data.Handle (fromHandle) import Data.Id (TeamId, UserId) -import qualified Data.Map.Strict as M import Data.String.Conversions (cs) import Imports -import System.Random import System.Random.Shuffle (shuffleM) import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (assertBool, assertEqual) diff --git a/services/brig/test/integration/API/User/Auth.hs b/services/brig/test/integration/API/User/Auth.hs index 01cb9db293..49ec997223 100644 --- a/services/brig/test/integration/API/User/Auth.hs +++ b/services/brig/test/integration/API/User/Auth.hs @@ -151,6 +151,7 @@ tests conf m z db b g n = test m "new-session-cookie" (testNewSessionCookie conf b), test m "suspend-inactive" (testSuspendInactiveUsers conf b), test m "client access" (testAccessWithClientId b), + test m "client access with old token" (testAccessWithClientIdAndOldToken b), test m "client access incorrect" (testAccessWithIncorrectClientId b), test m "multiple client accesses" (testAccessWithExistingClientId b) ], @@ -1014,6 +1015,58 @@ testAccessWithClientId brig = do assertSaneAccessToken now (userId u) (decodeToken' @ZAuth.Access r) ZAuth.accessTokenClient @ZAuth.Access atoken @?= Just (clientId cl) +-- here a fresh client gets a token without client_id first, then allocates a +-- new client ID and finally calls access again with the new client_id +testAccessWithClientIdAndOldToken :: Brig -> Http () +testAccessWithClientIdAndOldToken brig = do + u <- randomUser brig + rs <- + login + brig + ( emailLogin + (fromJust (userEmail u)) + defPassword + (Just "nexus1") + ) + PersistentCookie + toByteString' token0) + . cookie c + ) + Http () testAccessWithIncorrectClientId brig = do u <- randomUser brig diff --git a/services/brig/test/integration/Federation/Util.hs b/services/brig/test/integration/Federation/Util.hs index dbc08acb71..5c7e1552cb 100644 --- a/services/brig/test/integration/Federation/Util.hs +++ b/services/brig/test/integration/Federation/Util.hs @@ -1,7 +1,6 @@ {-# LANGUAGE PartialTypeSignatures #-} {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} {-# OPTIONS_GHC -Wno-partial-type-signatures #-} -{-# OPTIONS_GHC -Wno-unused-imports #-} -- This file is part of the Wire Server implementation. -- @@ -23,47 +22,19 @@ module Federation.Util where import Bilge -import Bilge.Assert ((!!!), (. -- for SES notifications -{-# OPTIONS_GHC -fno-warn-orphans -Wno-deprecations #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} module Util where diff --git a/services/cannon/default.nix b/services/cannon/default.nix index ef0bff58d7..ef66129761 100644 --- a/services/cannon/default.nix +++ b/services/cannon/default.nix @@ -2,16 +2,58 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, aeson, api-field-json-th, async, base, bilge -, bytestring, bytestring-conversion, conduit, criterion -, data-default, data-timeout, exceptions, extended, extra -, gitignoreSource, gundeck-types, hashable, http-types, imports -, lens, lens-family-core, lib, metrics-wai, mwc-random, QuickCheck -, random, retry, safe-exceptions, servant, servant-conduit -, servant-server, strict, swagger, tasty, tasty-hunit -, tasty-quickcheck, text, tinylog, types-common, unix, unliftio -, uuid, vector, wai, wai-extra, wai-predicates, wai-utilities -, wai-websockets, warp, websockets, wire-api +{ mkDerivation +, aeson +, api-field-json-th +, async +, base +, bilge +, bytestring +, bytestring-conversion +, conduit +, criterion +, data-default +, data-timeout +, exceptions +, extended +, extra +, gitignoreSource +, gundeck-types +, hashable +, http-types +, imports +, lens +, lens-family-core +, lib +, metrics-wai +, mwc-random +, QuickCheck +, random +, retry +, safe-exceptions +, servant +, servant-conduit +, servant-server +, strict +, swagger +, tasty +, tasty-hunit +, tasty-quickcheck +, text +, tinylog +, types-common +, unix +, unliftio +, uuid +, vector +, wai +, wai-extra +, wai-predicates +, wai-utilities +, wai-websockets +, warp +, websockets +, wire-api }: mkDerivation { pname = "cannon"; @@ -20,24 +62,85 @@ mkDerivation { isLibrary = true; isExecutable = true; libraryHaskellDepends = [ - aeson api-field-json-th async base bilge bytestring - bytestring-conversion conduit data-default data-timeout exceptions - extended extra gundeck-types hashable http-types imports lens - lens-family-core metrics-wai mwc-random retry safe-exceptions - servant servant-conduit servant-server strict swagger text tinylog - types-common unix unliftio uuid vector wai wai-extra wai-predicates - wai-utilities wai-websockets warp websockets wire-api + aeson + api-field-json-th + async + base + bilge + bytestring + bytestring-conversion + conduit + data-default + data-timeout + exceptions + extended + extra + gundeck-types + hashable + http-types + imports + lens + lens-family-core + metrics-wai + mwc-random + retry + safe-exceptions + servant + servant-conduit + servant-server + strict + swagger + text + tinylog + types-common + unix + unliftio + uuid + vector + wai + wai-extra + wai-predicates + wai-utilities + wai-websockets + warp + websockets + wire-api ]; executableHaskellDepends = [ base extended imports types-common ]; testHaskellDepends = [ - async base bytestring criterion extended imports metrics-wai - QuickCheck random tasty tasty-hunit tasty-quickcheck types-common - uuid wai-utilities wire-api + async + base + bytestring + criterion + extended + imports + metrics-wai + QuickCheck + random + tasty + tasty-hunit + tasty-quickcheck + types-common + uuid + wai-utilities + wire-api ]; benchmarkHaskellDepends = [ - async base bytestring criterion extended imports metrics-wai - QuickCheck random tasty tasty-hunit tasty-quickcheck types-common - uuid wai-utilities + async + base + bytestring + criterion + extended + imports + metrics-wai + QuickCheck + random + tasty + tasty-hunit + tasty-quickcheck + types-common + uuid + wai-utilities ]; description = "Push Notification API"; license = lib.licenses.agpl3Only; diff --git a/services/cargohold/default.nix b/services/cargohold/default.nix index 23fc8e986b..082cf037e3 100644 --- a/services/cargohold/default.nix +++ b/services/cargohold/default.nix @@ -2,20 +2,74 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, aeson, amazonka, amazonka-core, amazonka-s3 -, attoparsec, auto-update, base, base64-bytestring, bilge -, bytestring, bytestring-conversion, cargohold-types -, case-insensitive, conduit, conduit-extra, containers, cryptonite -, data-default, errors, exceptions, extended, federator -, gitignoreSource, HsOpenSSL, HsOpenSSL-x509-system, http-api-data -, http-client, http-client-openssl, http-client-tls, http-media -, http-types, imports, kan-extensions, lens, lib, metrics-core -, metrics-wai, mime, mmorph, mtl, optparse-applicative, resourcet -, retry, safe, servant, servant-client, servant-client-core -, servant-server, swagger, tagged, tasty, tasty-hunit, text, time -, tinylog, types-common, types-common-aws, unliftio -, unordered-containers, uri-bytestring, uuid, wai, wai-extra -, wai-utilities, wire-api, wire-api-federation, yaml +{ mkDerivation +, aeson +, amazonka +, amazonka-core +, amazonka-s3 +, attoparsec +, auto-update +, base +, base64-bytestring +, bilge +, bytestring +, bytestring-conversion +, cargohold-types +, case-insensitive +, conduit +, conduit-extra +, containers +, cryptonite +, data-default +, errors +, exceptions +, extended +, federator +, gitignoreSource +, HsOpenSSL +, HsOpenSSL-x509-system +, http-api-data +, http-client +, http-client-openssl +, http-client-tls +, http-media +, http-types +, imports +, kan-extensions +, lens +, lib +, metrics-core +, metrics-wai +, mime +, mmorph +, mtl +, optparse-applicative +, resourcet +, retry +, safe +, servant +, servant-client +, servant-client-core +, servant-server +, swagger +, tagged +, tasty +, tasty-hunit +, text +, time +, tinylog +, types-common +, types-common-aws +, unliftio +, unordered-containers +, uri-bytestring +, uuid +, wai +, wai-extra +, wai-utilities +, wire-api +, wire-api-federation +, yaml }: mkDerivation { pname = "cargohold"; @@ -24,25 +78,105 @@ mkDerivation { isLibrary = true; isExecutable = true; libraryHaskellDepends = [ - aeson amazonka amazonka-core amazonka-s3 attoparsec auto-update - base base64-bytestring bilge bytestring bytestring-conversion - cargohold-types case-insensitive conduit conduit-extra containers - cryptonite data-default errors exceptions extended HsOpenSSL - HsOpenSSL-x509-system http-client http-client-openssl http-types - imports kan-extensions lens metrics-core metrics-wai mime - optparse-applicative resourcet retry safe servant servant-server - swagger text time tinylog types-common types-common-aws unliftio - unordered-containers uri-bytestring uuid wai wai-extra - wai-utilities wire-api wire-api-federation yaml + aeson + amazonka + amazonka-core + amazonka-s3 + attoparsec + auto-update + base + base64-bytestring + bilge + bytestring + bytestring-conversion + cargohold-types + case-insensitive + conduit + conduit-extra + containers + cryptonite + data-default + errors + exceptions + extended + HsOpenSSL + HsOpenSSL-x509-system + http-client + http-client-openssl + http-types + imports + kan-extensions + lens + metrics-core + metrics-wai + mime + optparse-applicative + resourcet + retry + safe + servant + servant-server + swagger + text + time + tinylog + types-common + types-common-aws + unliftio + unordered-containers + uri-bytestring + uuid + wai + wai-extra + wai-utilities + wire-api + wire-api-federation + yaml ]; executableHaskellDepends = [ - aeson base base64-bytestring bilge bytestring bytestring-conversion - cargohold-types conduit containers cryptonite data-default errors - exceptions extended federator HsOpenSSL http-api-data http-client - http-client-tls http-media http-types imports kan-extensions lens - mime mmorph mtl optparse-applicative safe servant-client - servant-client-core tagged tasty tasty-hunit text time types-common - uuid wai wai-utilities wire-api wire-api-federation yaml + aeson + base + base64-bytestring + bilge + bytestring + bytestring-conversion + cargohold-types + conduit + containers + cryptonite + data-default + errors + exceptions + extended + federator + HsOpenSSL + http-api-data + http-client + http-client-tls + http-media + http-types + imports + kan-extensions + lens + mime + mmorph + mtl + optparse-applicative + safe + servant-client + servant-client-core + tagged + tasty + tasty-hunit + text + time + types-common + uuid + wai + wai-utilities + wire-api + wire-api-federation + yaml ]; description = "Asset Storage API"; license = lib.licenses.agpl3Only; diff --git a/services/federator/default.nix b/services/federator/default.nix index 40d51f9e4e..2704a157dc 100644 --- a/services/federator/default.nix +++ b/services/federator/default.nix @@ -2,21 +2,83 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, aeson, async, base, bilge, binary, bytestring -, bytestring-conversion, connection, constraints, containers -, cryptonite, data-default, directory, dns, dns-util, either -, errors, exceptions, extended, filepath, gitignoreSource, hinotify -, hspec, http-client, http-client-openssl, http-client-tls -, http-media, http-types, http2, imports, interpolate -, kan-extensions, lens, lib, metrics-core, metrics-wai, mtl -, network, network-uri, optparse-applicative, pem, polysemy -, polysemy-wire-zoo, QuickCheck, random, retry, servant -, servant-client, servant-client-core, streaming-commons -, string-conversions, tasty, tasty-hunit, tasty-quickcheck -, temporary, text, time-manager, tinylog, tls, transformers -, types-common, unix, uri-bytestring, uuid, wai, wai-extra -, wai-utilities, warp, warp-tls, wire-api, wire-api-federation -, x509, x509-store, x509-system, x509-validation, yaml +{ mkDerivation +, aeson +, async +, base +, bilge +, binary +, bytestring +, bytestring-conversion +, connection +, constraints +, containers +, cryptonite +, data-default +, directory +, dns +, dns-util +, either +, errors +, exceptions +, extended +, filepath +, gitignoreSource +, hinotify +, hspec +, http-client +, http-client-openssl +, http-client-tls +, http-media +, http-types +, http2 +, imports +, interpolate +, kan-extensions +, lens +, lib +, metrics-core +, metrics-wai +, mtl +, network +, network-uri +, optparse-applicative +, pem +, polysemy +, polysemy-wire-zoo +, QuickCheck +, random +, retry +, servant +, servant-client +, servant-client-core +, streaming-commons +, string-conversions +, tasty +, tasty-hunit +, tasty-quickcheck +, temporary +, text +, time-manager +, tinylog +, tls +, transformers +, types-common +, unix +, uri-bytestring +, uuid +, wai +, wai-extra +, wai-utilities +, warp +, warp-tls +, wire-api +, wire-api-federation +, x509 +, x509-store +, x509-system +, x509-validation +, yaml }: mkDerivation { pname = "federator"; @@ -25,44 +87,200 @@ mkDerivation { isLibrary = true; isExecutable = true; libraryHaskellDepends = [ - aeson async base bilge binary bytestring bytestring-conversion - constraints containers data-default dns dns-util either exceptions - extended filepath hinotify http-client http-client-openssl - http-media http-types http2 imports kan-extensions lens - metrics-core metrics-wai mtl network network-uri pem polysemy - polysemy-wire-zoo retry servant servant-client-core - streaming-commons string-conversions text time-manager tinylog tls - types-common unix uri-bytestring uuid wai wai-utilities warp - warp-tls wire-api wire-api-federation x509 x509-store x509-system + aeson + async + base + bilge + binary + bytestring + bytestring-conversion + constraints + containers + data-default + dns + dns-util + either + exceptions + extended + filepath + hinotify + http-client + http-client-openssl + http-media + http-types + http2 + imports + kan-extensions + lens + metrics-core + metrics-wai + mtl + network + network-uri + pem + polysemy + polysemy-wire-zoo + retry + servant + servant-client-core + streaming-commons + string-conversions + text + time-manager + tinylog + tls + types-common + unix + uri-bytestring + uuid + wai + wai-utilities + warp + warp-tls + wire-api + wire-api-federation + x509 + x509-store + x509-system x509-validation ]; executableHaskellDepends = [ - aeson async base bilge binary bytestring bytestring-conversion - connection constraints containers cryptonite data-default dns - dns-util either errors exceptions extended filepath hinotify hspec - http-client http-client-openssl http-client-tls http-media - http-types http2 imports kan-extensions lens metrics-core - metrics-wai mtl network network-uri optparse-applicative pem - polysemy polysemy-wire-zoo QuickCheck random retry servant - servant-client-core streaming-commons string-conversions tasty - tasty-hunit text time-manager tinylog tls types-common unix - uri-bytestring uuid wai wai-utilities warp warp-tls wire-api - wire-api-federation x509 x509-store x509-system x509-validation + aeson + async + base + bilge + binary + bytestring + bytestring-conversion + connection + constraints + containers + cryptonite + data-default + dns + dns-util + either + errors + exceptions + extended + filepath + hinotify + hspec + http-client + http-client-openssl + http-client-tls + http-media + http-types + http2 + imports + kan-extensions + lens + metrics-core + metrics-wai + mtl + network + network-uri + optparse-applicative + pem + polysemy + polysemy-wire-zoo + QuickCheck + random + retry + servant + servant-client-core + streaming-commons + string-conversions + tasty + tasty-hunit + text + time-manager + tinylog + tls + types-common + unix + uri-bytestring + uuid + wai + wai-utilities + warp + warp-tls + wire-api + wire-api-federation + x509 + x509-store + x509-system + x509-validation yaml ]; testHaskellDepends = [ - aeson async base bilge binary bytestring bytestring-conversion - constraints containers data-default directory dns dns-util either - exceptions extended filepath hinotify http-client - http-client-openssl http-media http-types http2 imports interpolate - kan-extensions lens metrics-core metrics-wai mtl network - network-uri pem polysemy polysemy-wire-zoo QuickCheck retry servant - servant-client servant-client-core streaming-commons - string-conversions tasty tasty-hunit tasty-quickcheck temporary - text time-manager tinylog tls transformers types-common unix - uri-bytestring uuid wai wai-extra wai-utilities warp warp-tls - wire-api wire-api-federation x509 x509-store x509-system - x509-validation yaml + aeson + async + base + bilge + binary + bytestring + bytestring-conversion + constraints + containers + data-default + directory + dns + dns-util + either + exceptions + extended + filepath + hinotify + http-client + http-client-openssl + http-media + http-types + http2 + imports + interpolate + kan-extensions + lens + metrics-core + metrics-wai + mtl + network + network-uri + pem + polysemy + polysemy-wire-zoo + QuickCheck + retry + servant + servant-client + servant-client-core + streaming-commons + string-conversions + tasty + tasty-hunit + tasty-quickcheck + temporary + text + time-manager + tinylog + tls + transformers + types-common + unix + uri-bytestring + uuid + wai + wai-extra + wai-utilities + warp + warp-tls + wire-api + wire-api-federation + x509 + x509-store + x509-system + x509-validation + yaml ]; description = "Federation Service"; license = lib.licenses.agpl3Only; diff --git a/services/federator/src/Federator/InternalServer.hs b/services/federator/src/Federator/InternalServer.hs index 084907c6eb..1a7d33bce1 100644 --- a/services/federator/src/Federator/InternalServer.hs +++ b/services/federator/src/Federator/InternalServer.hs @@ -1,5 +1,5 @@ {-# LANGUAGE PartialTypeSignatures #-} -{-# OPTIONS_GHC -Wno-partial-type-signatures -Wno-unused-imports #-} +{-# OPTIONS_GHC -Wno-partial-type-signatures #-} -- This file is part of the Wire Server implementation. -- @@ -20,58 +20,22 @@ module Federator.InternalServer where -import Control.Exception (bracketOnError) -import qualified Control.Exception as E -import Control.Lens (view) import Data.Binary.Builder import qualified Data.ByteString as BS -import qualified Data.ByteString.Char8 as C8 -import qualified Data.ByteString.Lazy as LBS -import Data.Default -import Data.Domain (domainText) -import Data.Either.Validation (Validation (..)) import qualified Data.Text as Text -import qualified Data.Text.Encoding as Text -import Data.X509.CertificateStore -import Federator.App (runAppT) -import Federator.Discovery (DiscoverFederator, DiscoveryFailure (DiscoveryFailureDNSError, DiscoveryFailureSrvNotAvailable), runFederatorDiscovery) -import Federator.Env (Env, TLSSettings, applog, caStore, dnsResolver, runSettings, tls) +import Federator.Env (Env) import Federator.Error.ServerError import Federator.Options (RunSettings) import Federator.Remote import Federator.Response import Federator.Validation -import Foreign (mallocBytes) -import Foreign.Marshal (free) import Imports -import Network.HPACK (BufferSize) -import Network.HTTP.Client.Internal (openSocketConnection) -import Network.HTTP.Client.OpenSSL (withOpenSSL) import qualified Network.HTTP.Types as HTTP -import qualified Network.HTTP2.Client as HTTP2 -import Network.Socket (Socket) -import qualified Network.Socket as NS -import Network.TLS -import qualified Network.TLS as TLS -import qualified Network.TLS.Extra.Cipher as TLS import qualified Network.Wai as Wai -import qualified Network.Wai.Handler.Warp as Warp import Polysemy import Polysemy.Error -import qualified Polysemy.Error as Polysemy -import Polysemy.IO (embedToMonadIO) import Polysemy.Input -import qualified Polysemy.Input as Polysemy -import qualified Polysemy.Resource as Polysemy -import Polysemy.TinyLog (TinyLog) -import qualified Polysemy.TinyLog as Log -import Servant.Client.Core -import qualified System.TimeManager as T -import qualified System.X509 as TLS import Wire.API.Federation.Component -import Wire.Network.DNS.Effect (DNSLookup) -import qualified Wire.Network.DNS.Effect as Lookup -import Wire.Network.DNS.SRV (SrvTarget (..)) data RequestData = RequestData { rdTargetDomain :: Text, diff --git a/services/federator/test/unit/Test/Federator/ExternalServer.hs b/services/federator/test/unit/Test/Federator/ExternalServer.hs index dfffa52046..8ecb1184d8 100644 --- a/services/federator/test/unit/Test/Federator/ExternalServer.hs +++ b/services/federator/test/unit/Test/Federator/ExternalServer.hs @@ -1,19 +1,3 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2020 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 . {-# OPTIONS_GHC -Wno-orphans #-} -- This file is part of the Wire Server implementation. diff --git a/services/galley/default.nix b/services/galley/default.nix index 6fecc03f56..cf8abcd206 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -2,32 +2,131 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, aeson, aeson-qq, amazonka, amazonka-sqs -, asn1-encoding, asn1-types, async, base, base64-bytestring, bilge -, binary, blake2, brig-types, bytestring, bytestring-conversion -, case-insensitive, cassandra-util, cassava, cereal, comonad -, conduit, containers, cookie, cryptonite, currency-codes -, data-default, data-timeout, directory, either -, enclosed-exceptions, errors, exceptions, extended, extra -, federator, filepath, galley-types, gitignoreSource, gundeck-types -, hex, HsOpenSSL, HsOpenSSL-x509-system, hspec, http-client -, http-client-openssl, http-client-tls, http-media, http-types -, imports, insert-ordered-containers, kan-extensions, lens -, lens-aeson, lib, memory, metrics-core, metrics-wai, mtl -, optparse-applicative, pem, polysemy, polysemy-wire-zoo, process -, proto-lens, protobuf, QuickCheck, quickcheck-instances, random -, raw-strings-qq, resourcet, retry, safe, safe-exceptions -, saml2-web-sso, schema-profunctor, semigroups, servant -, servant-client, servant-client-core, servant-server -, servant-swagger, servant-swagger-ui, singletons, sop-core, split -, ssl-util, stm, string-conversions, swagger, swagger2, tagged -, tasty, tasty-cannon, tasty-hspec, tasty-hunit, tasty-quickcheck -, temporary, text, time, tinylog, tls, transformers, types-common -, types-common-aws, types-common-journal, unix, unliftio -, unordered-containers, uri-bytestring, uuid, vector, wai -, wai-extra, wai-middleware-gunzip, wai-predicates, wai-routing -, wai-utilities, warp, warp-tls, wire-api, wire-api-federation -, wire-message-proto-lens, x509, yaml +{ mkDerivation +, aeson +, aeson-qq +, amazonka +, amazonka-sqs +, asn1-encoding +, asn1-types +, async +, base +, base64-bytestring +, bilge +, binary +, blake2 +, brig-types +, bytestring +, bytestring-conversion +, case-insensitive +, cassandra-util +, cassava +, cereal +, comonad +, conduit +, containers +, cookie +, cryptonite +, currency-codes +, data-default +, data-timeout +, directory +, either +, enclosed-exceptions +, errors +, exceptions +, extended +, extra +, federator +, filepath +, galley-types +, gitignoreSource +, gundeck-types +, hex +, HsOpenSSL +, HsOpenSSL-x509-system +, hspec +, http-client +, http-client-openssl +, http-client-tls +, http-media +, http-types +, imports +, insert-ordered-containers +, kan-extensions +, lens +, lens-aeson +, lib +, memory +, metrics-core +, metrics-wai +, mtl +, optparse-applicative +, pem +, polysemy +, polysemy-wire-zoo +, process +, proto-lens +, protobuf +, QuickCheck +, quickcheck-instances +, random +, raw-strings-qq +, resourcet +, retry +, safe +, safe-exceptions +, saml2-web-sso +, schema-profunctor +, semigroups +, servant +, servant-client +, servant-client-core +, servant-server +, servant-swagger +, servant-swagger-ui +, singletons +, sop-core +, split +, ssl-util +, stm +, string-conversions +, swagger +, swagger2 +, tagged +, tasty +, tasty-cannon +, tasty-hspec +, tasty-hunit +, tasty-quickcheck +, temporary +, text +, time +, tinylog +, tls +, transformers +, types-common +, types-common-aws +, types-common-journal +, unix +, unliftio +, unordered-containers +, uri-bytestring +, uuid +, vector +, wai +, wai-extra +, wai-middleware-gunzip +, wai-predicates +, wai-routing +, wai-utilities +, warp +, warp-tls +, wire-api +, wire-api-federation +, wire-message-proto-lens +, x509 +, yaml }: mkDerivation { pname = "galley"; @@ -36,53 +135,237 @@ mkDerivation { isLibrary = true; isExecutable = true; libraryHaskellDepends = [ - aeson amazonka amazonka-sqs asn1-encoding asn1-types async base - base64-bytestring bilge binary blake2 brig-types bytestring - bytestring-conversion case-insensitive cassandra-util cassava - cereal comonad containers cryptonite currency-codes data-default - data-timeout either enclosed-exceptions errors exceptions extended - extra galley-types gundeck-types hex HsOpenSSL - HsOpenSSL-x509-system http-client http-client-openssl - http-client-tls http-media http-types imports - insert-ordered-containers kan-extensions lens memory metrics-core - metrics-wai mtl optparse-applicative pem polysemy polysemy-wire-zoo - proto-lens protobuf QuickCheck random raw-strings-qq resourcet - retry safe safe-exceptions saml2-web-sso schema-profunctor - semigroups servant servant-client servant-client-core - servant-server servant-swagger servant-swagger-ui singletons - sop-core split ssl-util stm string-conversions swagger swagger2 - tagged text time tinylog tls transformers types-common - types-common-aws types-common-journal unliftio unordered-containers - uri-bytestring uuid vector wai wai-extra wai-middleware-gunzip - wai-predicates wai-routing wai-utilities warp wire-api - wire-api-federation x509 + aeson + amazonka + amazonka-sqs + asn1-encoding + asn1-types + async + base + base64-bytestring + bilge + binary + blake2 + brig-types + bytestring + bytestring-conversion + case-insensitive + cassandra-util + cassava + cereal + comonad + containers + cryptonite + currency-codes + data-default + data-timeout + either + enclosed-exceptions + errors + exceptions + extended + extra + galley-types + gundeck-types + hex + HsOpenSSL + HsOpenSSL-x509-system + http-client + http-client-openssl + http-client-tls + http-media + http-types + imports + insert-ordered-containers + kan-extensions + lens + memory + metrics-core + metrics-wai + mtl + optparse-applicative + pem + polysemy + polysemy-wire-zoo + proto-lens + protobuf + QuickCheck + random + raw-strings-qq + resourcet + retry + safe + safe-exceptions + saml2-web-sso + schema-profunctor + semigroups + servant + servant-client + servant-client-core + servant-server + servant-swagger + servant-swagger-ui + singletons + sop-core + split + ssl-util + stm + string-conversions + swagger + swagger2 + tagged + text + time + tinylog + tls + transformers + types-common + types-common-aws + types-common-journal + unliftio + unordered-containers + uri-bytestring + uuid + vector + wai + wai-extra + wai-middleware-gunzip + wai-predicates + wai-routing + wai-utilities + warp + wire-api + wire-api-federation + x509 ]; executableHaskellDepends = [ - aeson aeson-qq amazonka amazonka-sqs async base base64-bytestring - bilge binary brig-types bytestring bytestring-conversion - case-insensitive cassandra-util cassava cereal comonad conduit - containers cookie cryptonite currency-codes data-default - data-timeout directory errors exceptions extended extra federator - filepath galley-types gundeck-types hex HsOpenSSL - HsOpenSSL-x509-system hspec http-client http-client-openssl - http-client-tls http-media http-types imports kan-extensions lens - lens-aeson memory metrics-wai mtl optparse-applicative pem process - proto-lens protobuf QuickCheck quickcheck-instances random - raw-strings-qq retry safe saml2-web-sso schema-profunctor servant - servant-client servant-client-core servant-server servant-swagger - singletons sop-core ssl-util string-conversions tagged tasty - tasty-cannon tasty-hunit temporary text time tinylog tls - transformers types-common types-common-journal unix unliftio - unordered-containers uri-bytestring uuid vector wai wai-extra - wai-utilities warp warp-tls wire-api wire-api-federation - wire-message-proto-lens yaml + aeson + aeson-qq + amazonka + amazonka-sqs + async + base + base64-bytestring + bilge + binary + brig-types + bytestring + bytestring-conversion + case-insensitive + cassandra-util + cassava + cereal + comonad + conduit + containers + cookie + cryptonite + currency-codes + data-default + data-timeout + directory + errors + exceptions + extended + extra + federator + filepath + galley-types + gundeck-types + hex + HsOpenSSL + HsOpenSSL-x509-system + hspec + http-client + http-client-openssl + http-client-tls + http-media + http-types + imports + kan-extensions + lens + lens-aeson + memory + metrics-wai + mtl + optparse-applicative + pem + process + proto-lens + protobuf + QuickCheck + quickcheck-instances + random + raw-strings-qq + retry + safe + saml2-web-sso + schema-profunctor + servant + servant-client + servant-client-core + servant-server + servant-swagger + singletons + sop-core + ssl-util + string-conversions + tagged + tasty + tasty-cannon + tasty-hunit + temporary + text + time + tinylog + tls + transformers + types-common + types-common-journal + unix + unliftio + unordered-containers + uri-bytestring + uuid + vector + wai + wai-extra + wai-utilities + warp + warp-tls + wire-api + wire-api-federation + wire-message-proto-lens + yaml ]; testHaskellDepends = [ - base case-insensitive containers extended extra galley-types - http-types imports lens QuickCheck raw-strings-qq safe - saml2-web-sso servant-client servant-swagger ssl-util tagged tasty - tasty-hspec tasty-hunit tasty-quickcheck transformers types-common - wai wai-predicates wire-api wire-api-federation + base + case-insensitive + containers + extended + extra + galley-types + http-types + imports + lens + QuickCheck + raw-strings-qq + safe + saml2-web-sso + servant-client + servant-swagger + ssl-util + tagged + tasty + tasty-hspec + tasty-hunit + tasty-quickcheck + transformers + types-common + wai + wai-predicates + wire-api + wire-api-federation ]; description = "Conversations"; license = lib.licenses.agpl3Only; diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 6e3529e497..470372cc4a 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -42,7 +42,17 @@ library Galley.API.MLS.Welcome Galley.API.One2One Galley.API.Public + Galley.API.Public.Bot + Galley.API.Public.Conversation + Galley.API.Public.CustomBackend + Galley.API.Public.Feature + Galley.API.Public.LegalHold + Galley.API.Public.Messaging + Galley.API.Public.MLS Galley.API.Public.Servant + Galley.API.Public.Team + Galley.API.Public.TeamConversation + Galley.API.Public.TeamMember Galley.API.Push Galley.API.Query Galley.API.Teams @@ -528,11 +538,15 @@ executable galley-integration executable galley-migrate-data main-is: Main.hs + + -- cabal-fmt: expand migrate-data/src other-modules: Galley.DataMigration Galley.DataMigration.Types + Main Paths_galley V1_BackfillBillingTeamMembers + V2_MigrateMLSMembers hs-source-dirs: migrate-data/src default-extensions: @@ -588,6 +602,7 @@ executable galley-migrate-data , exceptions , extended , extra >=1.3 + , galley , galley-types , imports , lens @@ -674,6 +689,8 @@ executable galley-schema V73_MemberClientTable V74_ExposeInvitationsToTeamAdmin V75_MLSGroupInfo + V76_ProposalOrigin + V77_MLSGroupMemberClient hs-source-dirs: schema/src default-extensions: diff --git a/services/galley/migrate-data/src/Galley/DataMigration/Types.hs b/services/galley/migrate-data/src/Galley/DataMigration/Types.hs index 3cc0f74cbb..76aad12dad 100644 --- a/services/galley/migrate-data/src/Galley/DataMigration/Types.hs +++ b/services/galley/migrate-data/src/Galley/DataMigration/Types.hs @@ -42,7 +42,8 @@ newtype MigrationActionT m a = MigrationActionT {unMigrationAction :: ReaderT En Monad, MonadIO, MonadThrow, - MonadReader Env + MonadReader Env, + MonadUnliftIO ) instance MonadTrans MigrationActionT where diff --git a/services/galley/migrate-data/src/Main.hs b/services/galley/migrate-data/src/Main.hs index 08b6e51cfa..f6a051b8d5 100644 --- a/services/galley/migrate-data/src/Main.hs +++ b/services/galley/migrate-data/src/Main.hs @@ -22,11 +22,17 @@ import Imports import Options.Applicative import qualified System.Logger.Extended as Log import qualified V1_BackfillBillingTeamMembers +import qualified V2_MigrateMLSMembers main :: IO () main = do o <- execParser (info (helper <*> cassandraSettingsParser) desc) l <- Log.mkLogger Log.Debug Nothing Nothing - migrate l o [V1_BackfillBillingTeamMembers.migration] + migrate + l + o + [ V1_BackfillBillingTeamMembers.migration, + V2_MigrateMLSMembers.migration + ] where desc = header "Galley Cassandra Data Migrations" <> fullDesc diff --git a/services/galley/migrate-data/src/V2_MigrateMLSMembers.hs b/services/galley/migrate-data/src/V2_MigrateMLSMembers.hs new file mode 100644 index 0000000000..f0a444ec54 --- /dev/null +++ b/services/galley/migrate-data/src/V2_MigrateMLSMembers.hs @@ -0,0 +1,101 @@ +-- 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 V2_MigrateMLSMembers where + +import Cassandra +import Conduit +import Data.Conduit.Internal (zipSources) +import qualified Data.Conduit.List as C +import Data.Domain +import Data.Id +import Data.Map.Strict (lookup) +import qualified Data.Map.Strict as Map +import Galley.Cassandra.Instances () +import Galley.DataMigration.Types +import Imports hiding (lookup) +import qualified System.Logger.Class as Log +import UnliftIO (pooledMapConcurrentlyN_) +import UnliftIO.Async (pooledMapConcurrentlyN) +import Wire.API.MLS.Group +import Wire.API.MLS.KeyPackage + +migration :: Migration +migration = + Migration + { version = MigrationVersion 2, + text = "Migrating from member_client to mls_group_member_client", + action = + runConduit $ + zipSources + (C.sourceList [(1 :: Int32) ..]) + getMemberClientsFromLegacy + .| C.mapM_ + ( \(i, rows) -> do + Log.info (Log.field "Entries " (show (i * pageSize))) + let convIds = map rowConvId rows + m <- lookupGroupIds convIds + let newRows = flip mapMaybe rows $ \(conv, domain, uid, client, ref) -> + conv `lookup` m >>= \groupId -> pure (groupId, domain, uid, client, ref) + insertMemberClients newRows + ) + } + +rowConvId :: (ConvId, Domain, UserId, ClientId, KeyPackageRef) -> ConvId +rowConvId (conv, _, _, _, _) = conv + +pageSize :: Int32 +pageSize = 1000 + +getMemberClientsFromLegacy :: MonadClient m => ConduitM () [(ConvId, Domain, UserId, ClientId, KeyPackageRef)] m () +getMemberClientsFromLegacy = paginateC cql (paramsP LocalQuorum () pageSize) x5 + where + cql :: PrepQuery R () (ConvId, Domain, UserId, ClientId, KeyPackageRef) + cql = "SELECT conv, user_domain, user, client, key_package_ref from member_client" + +lookupGroupIds :: [ConvId] -> MigrationActionT IO (Map ConvId GroupId) +lookupGroupIds convIds = do + rows <- pooledMapConcurrentlyN 8 (\convId -> retry x5 (query1 cql (params LocalQuorum (Identity convId)))) convIds + rows' <- + rows + & mapM + ( \case + (Just (c, mg)) -> do + case mg of + Nothing -> do + Log.warn (Log.msg ("No group found for conv " <> show c)) + pure Nothing + Just g -> pure (Just (c, g)) + Nothing -> do + Log.warn (Log.msg ("Conversation is missing for entry" :: Text)) + pure Nothing + ) + + rows' + & catMaybes + & Map.fromList + & pure + where + cql :: PrepQuery R (Identity ConvId) (ConvId, Maybe GroupId) + cql = "SELECT conv, group_id from conversation where conv = ?" + +insertMemberClients :: (MonadUnliftIO m, MonadClient m) => [(GroupId, Domain, UserId, ClientId, KeyPackageRef)] -> m () +insertMemberClients rows = do + pooledMapConcurrentlyN_ 8 (\row -> retry x5 (write cql (params LocalQuorum row))) rows + where + cql :: PrepQuery W (GroupId, Domain, UserId, ClientId, KeyPackageRef) () + cql = "INSERT INTO mls_group_member_client (group_id, user_domain, user, client, key_package_ref) VALUES (?, ?, ?, ?, ?)" diff --git a/services/galley/schema/default.nix b/services/galley/schema/default.nix index d8003a87ba..b4d117ecf8 100644 --- a/services/galley/schema/default.nix +++ b/services/galley/schema/default.nix @@ -1,5 +1,12 @@ -{ mkDerivation, base, filepath, imports, lib, optparse-applicative -, shelly, system-filepath, text +{ mkDerivation +, base +, filepath +, imports +, lib +, optparse-applicative +, shelly +, system-filepath +, text }: mkDerivation { pname = "makedeb"; @@ -8,7 +15,12 @@ mkDerivation { isLibrary = true; isExecutable = true; libraryHaskellDepends = [ - base filepath imports optparse-applicative shelly system-filepath + base + filepath + imports + optparse-applicative + shelly + system-filepath text ]; executableHaskellDepends = [ base imports optparse-applicative ]; diff --git a/services/galley/schema/src/Main.hs b/services/galley/schema/src/Main.hs index 1c987650cc..bb3642744b 100644 --- a/services/galley/schema/src/Main.hs +++ b/services/galley/schema/src/Main.hs @@ -78,6 +78,8 @@ import qualified V72_DropManagedConversations import qualified V73_MemberClientTable import qualified V74_ExposeInvitationsToTeamAdmin import qualified V75_MLSGroupInfo +import qualified V76_ProposalOrigin +import qualified V77_MLSGroupMemberClient main :: IO () main = do @@ -141,7 +143,9 @@ main = do V72_DropManagedConversations.migration, V73_MemberClientTable.migration, V74_ExposeInvitationsToTeamAdmin.migration, - V75_MLSGroupInfo.migration + V75_MLSGroupInfo.migration, + V76_ProposalOrigin.migration, + V77_MLSGroupMemberClient.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/V76_ProposalOrigin.hs b/services/galley/schema/src/V76_ProposalOrigin.hs new file mode 100644 index 0000000000..c47ffc4d49 --- /dev/null +++ b/services/galley/schema/src/V76_ProposalOrigin.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 V76_ProposalOrigin + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + Migration 76 "Add the origin column to the pending proposals table" $ + schema' + [r| ALTER TABLE mls_proposal_refs ADD ( + origin int + ) + |] diff --git a/services/galley/schema/src/V77_MLSGroupMemberClient.hs b/services/galley/schema/src/V77_MLSGroupMemberClient.hs new file mode 100644 index 0000000000..8847b53c22 --- /dev/null +++ b/services/galley/schema/src/V77_MLSGroupMemberClient.hs @@ -0,0 +1,36 @@ +-- 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 V77_MLSGroupMemberClient (migration) where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + Migration 77 "Add table mls_group_member_client which replaces member_client" $ do + schema' + [r| CREATE TABLE mls_group_member_client ( + group_id blob, + user_domain text, + user uuid, + client text, + key_package_ref blob, + PRIMARY KEY (group_id, user_domain, user, client) + ); + |] diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index dcae8353e8..44e9d9d433 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -96,6 +96,11 @@ import qualified Wire.API.User as User data NoChanges = NoChanges type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Constraint where + HasConversationActionEffects 'ConversationSelfInviteTag r = + Members + '[ ErrorS 'InvalidOperation + ] + r HasConversationActionEffects 'ConversationJoinTag r = Members '[ BrigAccess, @@ -130,6 +135,7 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con '[ MemberStore, Error InternalError, Error NoChanges, + ErrorS 'InvalidOperation, ExternalAccess, FederatorAccess, GundeckAccess, @@ -157,6 +163,7 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con Error InvalidInput, Error NoChanges, ErrorS 'InvalidTargetAccess, + ErrorS 'InvalidOperation, ErrorS ('ActionDenied 'RemoveConversationMember), ExternalAccess, FederatorAccess, @@ -270,6 +277,13 @@ ensureAllowed tag loc action conv origUser = do -- not a team conv, so one of the other access roles has to allow this. when (Set.null $ cupAccessRoles action Set.\\ Set.fromList [TeamMemberAccessRole]) $ throwS @'InvalidTargetAccess + SConversationSelfInviteTag -> + unless + (convType conv == GlobalTeamConv) + $ throwS @'InvalidOperation + SConversationLeaveTag -> + when (convType conv == GlobalTeamConv) $ + throwS @'InvalidOperation _ -> pure () -- | Returns additional members that resulted from the action (e.g. ConversationJoin) @@ -342,6 +356,8 @@ performAction tag origUser lconv action = do SConversationAccessDataTag -> do (bm, act) <- performConversationAccessData origUser lconv action pure (bm, act) + SConversationSelfInviteTag -> + pure (mempty, action) performConversationJoin :: (HasConversationActionEffects 'ConversationJoinTag r) => @@ -579,7 +595,7 @@ updateLocalConversation lcnv qusr con action = do let tag = sing @tag -- retrieve conversation - conv <- getConversationWithError lcnv + conv <- getConversationWithError lcnv (qUnqualified qusr) -- check that the action does not bypass the underlying protocol unless (protocolValidAction (convProtocol conv) (fromSing tag)) $ @@ -618,7 +634,10 @@ updateLocalConversationUnchecked lconv qusr con action = do conv = tUnqualified lconv -- retrieve member - self <- noteS @'ConvNotFound $ getConvMember lconv conv qusr + self <- + if (cnvmType . convMetadata . tUnqualified $ lconv) == GlobalTeamConv + then pure $ Left $ localMemberFromUser (qUnqualified qusr) + else noteS @'ConvNotFound $ getConvMember lconv conv qusr -- perform checks ensureConversationActionAllowed (sing @tag) lcnv action conv self @@ -638,6 +657,23 @@ updateLocalConversationUnchecked lconv qusr con action = do -- -------------------------------------------------------------------------------- -- -- Utilities +localMemberFromUser :: UserId -> LocalMember +localMemberFromUser uid = + LocalMember + { lmId = uid, + lmStatus = + MemberStatus + { msOtrMutedStatus = Nothing, + msOtrMutedRef = Nothing, + msOtrArchived = False, + msOtrArchivedRef = Nothing, + msHidden = False, + msHiddenRef = Nothing + }, + lmService = Nothing, + lmConvRoleName = roleToRoleName convRoleWireMember + } + ensureConversationActionAllowed :: forall tag mem x r. ( IsConvMember mem, @@ -658,7 +694,7 @@ ensureConversationActionAllowed tag loc action conv self = do -- general action check ensureActionAllowed (sConversationActionPermission tag) self - -- check if it is a group conversation (except for rename actions) + -- check if it is a group or global conversation (except for rename actions) when (fromSing tag /= ConversationRenameTag) $ ensureGroupConversation conv @@ -789,16 +825,19 @@ notifyRemoteConversationAction loc rconvUpdate con = do -- 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 - ) => + Members + '[ Error InternalError, + ErrorS 'InvalidOperation, + ExternalAccess, + FederatorAccess, + GundeckAccess, + ProposalStore, + Input UTCTime, + Input Env, + MemberStore, + TinyLog + ] + r => Qualified UserId -> Local Conversation -> BotsAndMembers -> diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index f13e89c12f..0df2dbe353 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -24,7 +24,7 @@ -- with this program. If not, see . module Galley.API.Create ( createGroupConversation, - createSelfConversation, + createProteusSelfConversation, createOne2OneConversation, createConnectConversation, ) @@ -71,7 +71,7 @@ import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Event.Conversation import Wire.API.Federation.Error -import Wire.API.Routes.Public.Galley (ConversationResponse) +import Wire.API.Routes.Public.Galley.Conversation import Wire.API.Routes.Public.Util import Wire.API.Team import Wire.API.Team.LegalHold (LegalholdProtectee (LegalholdPlusFederationNotImplemented)) @@ -117,8 +117,7 @@ createGroupConversation lusr conn newConv = do case newConvProtocol newConv of ProtocolMLSTag -> do - haveKey <- isJust <$> getMLSRemovalKey - unless haveKey $ + unlessM (isJust <$> getMLSRemovalKey) $ -- We fail here to notify users early about this misconfiguration throw (InternalErrorWithDescription "No backend removal key is configured (See 'mlsPrivateKeyPaths' in galley's config). Refusing to create MLS conversation.") ProtocolProteusTag -> pure () @@ -127,11 +126,11 @@ createGroupConversation lusr conn newConv = do conv <- E.createConversation lcnv nc -- set creator client for MLS conversations - case (newConvProtocol newConv, newConvCreatorClient newConv) of - (ProtocolProteusTag, _) -> pure () - (ProtocolMLSTag, Just c) -> - E.addMLSClients lcnv (qUntagged lusr) (Set.singleton (c, nullKeyPackageRef)) - (ProtocolMLSTag, Nothing) -> + case (convProtocol conv, newConvCreatorClient newConv) of + (ProtocolProteus, _) -> pure () + (ProtocolMLS mlsMeta, Just c) -> + E.addMLSClients (cnvmlsGroupId mlsMeta) (qUntagged lusr) (Set.singleton (c, nullKeyPackageRef)) + (ProtocolMLS _mlsMeta, Nothing) -> throw (InvalidPayload "Missing creator_client field when creating an MLS conversation") now <- input @@ -193,12 +192,12 @@ checkCreateConvPermissions lusr newConv (Just tinfo) allUsers = do ---------------------------------------------------------------------------- -- Other kinds of conversations -createSelfConversation :: +createProteusSelfConversation :: forall r. Members '[ConversationStore, Error InternalError, P.TinyLog] r => Local UserId -> Sem r ConversationResponse -createSelfConversation lusr = do +createProteusSelfConversation lusr = do let lcnv = fmap Data.selfConv lusr c <- E.getConversation (tUnqualified lcnv) maybe (create lcnv) (conversationExisted lusr) c @@ -535,13 +534,6 @@ conversationCreated :: Sem r ConversationResponse conversationCreated lusr cnv = Created <$> conversationView lusr cnv -conversationExisted :: - Members '[Error InternalError, P.TinyLog] r => - Local UserId -> - Data.Conversation -> - Sem r ConversationResponse -conversationExisted lusr cnv = Existed <$> conversationView lusr cnv - notifyCreatedConversation :: Members '[Error InternalError, FederatorAccess, GundeckAccess, Input UTCTime, P.TinyLog] r => Maybe UTCTime -> diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 00f6fb2a33..1ce9c5f337 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -279,7 +279,7 @@ onConversationUpdated requestingDomain cu = do SConversationMessageTimerUpdateTag -> pure (Just sca, []) SConversationReceiptModeUpdateTag -> pure (Just sca, []) SConversationAccessDataTag -> pure (Just sca, []) - + SConversationSelfInviteTag -> pure (Nothing, []) unless allUsersArePresent $ P.warn $ Log.field "conversation" (toByteString' (F.cuConvId cu)) @@ -495,6 +495,8 @@ onUserDeleted origDomain udcn = do Public.ConnectConv -> pure () -- The self conv cannot be on a remote backend. Public.SelfConv -> pure () + -- The global team conv cannot be on a remote backend. + Public.GlobalTeamConv -> pure () Public.RegularConv -> do let botsAndMembers = convBotsAndMembers conv removeUser (qualifyAs lc conv) (qUntagged deletedUser) @@ -588,6 +590,8 @@ updateConversation origDomain updateRequest = do @(HasConversationActionGalleyErrors 'ConversationAccessDataTag) . fmap lcuUpdate $ updateLocalConversation @'ConversationAccessDataTag lcnv (qUntagged rusr) Nothing action + SConversationSelfInviteTag -> + throw InvalidOperation where mkResponse = fmap toResponse . runError @GalleyError . runError @NoChanges @@ -636,7 +640,7 @@ sendMLSCommitBundle remoteDomain msr = 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 + <$> postMLSCommitBundle loc (qUntagged sender) Nothing qcnv Nothing bundle sendMLSMessage :: ( Members @@ -680,7 +684,7 @@ sendMLSMessage remoteDomain msr = qcnv <- E.getConversationIdByGroupId (msgGroupId msg) >>= noteS @'ConvNotFound when (qUnqualified qcnv /= F.msrConvId msr) $ throwS @'MLSGroupConversationMismatch F.MLSMessageResponseUpdates . map lcuUpdate - <$> postMLSMessage loc (qUntagged sender) qcnv Nothing raw + <$> postMLSMessage loc (qUntagged sender) Nothing qcnv Nothing raw class ToGalleyRuntimeError (effs :: EffectRow) r where mapToGalleyError :: diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index f57d698186..730aabd767 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -103,7 +103,8 @@ import Wire.API.Routes.MultiTablePaging (mtpHasMore, mtpPagingState, mtpResults) import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.Routes.Public -import Wire.API.Routes.Public.Galley +import Wire.API.Routes.Public.Galley.Conversation +import Wire.API.Routes.Public.Galley.Feature import Wire.API.Team import Wire.API.Team.Feature import Wire.API.Team.Member @@ -302,7 +303,7 @@ type ITeamsAPIBase = Named "get-team-internal" (CanThrow 'TeamNotFound :> Get '[Servant.JSON] TeamData) :<|> Named "create-binding-team" - ( ZUser + ( ZLocalUser :> ReqBody '[Servant.JSON] BindingNewTeam :> MultiVerb1 'PUT @@ -356,6 +357,17 @@ type ITeamsAPIBase = :> CanThrow 'TooManyTeamMembersOnTeamWithLegalhold :> MultiVerb1 'GET '[Servant.JSON] (RespondEmpty 200 "User can join") ) + :<|> Named + "unchecked-update-team-member" + ( CanThrow 'AccessDenied + :> CanThrow 'InvalidPermissions + :> CanThrow 'TeamNotFound + :> CanThrow 'TeamMemberNotFound + :> CanThrow 'NotATeamMember + :> CanThrow OperationDenied + :> ReqBody '[Servant.JSON] NewTeamMember + :> MultiVerb1 'PUT '[Servant.JSON] (RespondEmpty 200 "") + ) ) :<|> Named "user-is-team-owner" @@ -485,6 +497,7 @@ iTeamsAPI = mkAPI $ \tid -> hoistAPIHandler id (base tid) <@> mkNamedAPI @"unchecked-get-team-members" (Teams.uncheckedGetTeamMembersH tid) <@> mkNamedAPI @"unchecked-get-team-member" (Teams.uncheckedGetTeamMember tid) <@> mkNamedAPI @"can-user-join-team" (Teams.canUserJoinTeam @Cassandra tid) + <@> mkNamedAPI @"unchecked-update-team-member" (Teams.uncheckedUpdateTeamMember Nothing Nothing tid) ) <@> mkNamedAPI @"user-is-team-owner" (Teams.userIsTeamOwner tid) <@> hoistAPISegment @@ -640,19 +653,20 @@ rmUser :: '[ BrigAccess, ClientStore, ConversationStore, + Error InternalError, ExternalAccess, FederatorAccess, GundeckAccess, - Input UTCTime, Input Env, + Input (Local ()), + Input UTCTime, ListItems p1 ConvId, ListItems p1 (Remote ConvId), ListItems p2 TeamId, - Input (Local ()), MemberStore, ProposalStore, - TeamStore, - P.TinyLog + P.TinyLog, + TeamStore ] r ) => @@ -690,28 +704,38 @@ rmUser lusr conn = do let qUser = qUntagged lusr cc <- getConversations ids now <- input - pp <- for cc $ \c -> case Data.convType c of - SelfConv -> pure Nothing - One2OneConv -> deleteMembers (Data.convId c) (UserList [tUnqualified lusr] []) $> Nothing - ConnectConv -> deleteMembers (Data.convId c) (UserList [tUnqualified lusr] []) $> Nothing - RegularConv - | tUnqualified lusr `isMember` Data.convLocalMembers c -> do + let deleteIfNeeded c = do + when (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 - (qUntagged (qualifyAs lusr (Data.convId c))) - (qUntagged lusr) - now - (EdMembersLeave (QualifiedUserIdList [qUser])) for_ (bucketRemote (fmap rmId (Data.convRemoteMembers c))) $ notifyRemoteMembers now qUser (Data.convId c) - pure $ - Intra.newPushLocal ListComplete (tUnqualified lusr) (Intra.ConvEvent e) (Intra.recipient <$> Data.convLocalMembers c) - <&> set Intra.pushConn conn - . set Intra.pushRoute Intra.RouteDirect - | otherwise -> pure Nothing + let e = + Event + (qUntagged (qualifyAs lusr (Data.convId c))) + (qUntagged lusr) + now + (EdMembersLeave (QualifiedUserIdList [qUser])) + pure $ + Intra.newPushLocal ListComplete (tUnqualified lusr) (Intra.ConvEvent e) (Intra.recipient <$> Data.convLocalMembers c) + <&> set Intra.pushConn conn + . set Intra.pushRoute Intra.RouteDirect + + deleteClientsFromGlobal 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] []) + for_ (bucketRemote (fmap rmId (Data.convRemoteMembers c))) $ notifyRemoteMembers now qUser (Data.convId c) + pure Nothing + + pp <- for cc $ \c -> case Data.convType c of + SelfConv -> pure Nothing + One2OneConv -> deleteMembers (Data.convId c) (UserList [tUnqualified lusr] []) $> Nothing + ConnectConv -> deleteMembers (Data.convId c) (UserList [tUnqualified lusr] []) $> Nothing + RegularConv -> deleteIfNeeded c + GlobalTeamConv -> deleteClientsFromGlobal c for_ (maybeList1 (catMaybes pp)) diff --git a/services/galley/src/Galley/API/LegalHold.hs b/services/galley/src/Galley/API/LegalHold.hs index 71288e7af5..151bc14655 100644 --- a/services/galley/src/Galley/API/LegalHold.hs +++ b/services/galley/src/Galley/API/LegalHold.hs @@ -74,7 +74,7 @@ import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Provider.Service import Wire.API.Routes.Internal.Brig.Connection -import Wire.API.Routes.Public.Galley (DisableLegalHoldForUserResponse (..), GrantConsentResult (..), RequestDeviceResult (..)) +import Wire.API.Routes.Public.Galley.LegalHold import qualified Wire.API.Team.Feature as Public import Wire.API.Team.LegalHold import qualified Wire.API.Team.LegalHold as Public diff --git a/services/galley/src/Galley/API/MLS/GroupInfo.hs b/services/galley/src/Galley/API/MLS/GroupInfo.hs index 3fe6894264..2a25d1ac5f 100644 --- a/services/galley/src/Galley/API/MLS/GroupInfo.hs +++ b/services/galley/src/Galley/API/MLS/GroupInfo.hs @@ -64,8 +64,7 @@ getGroupInfo lusr qcnvId = getGroupInfoFromLocalConv :: Members '[ ConversationStore, - MemberStore, - Input (Local ()) + MemberStore ] r => Members MLSGroupInfoStaticErrors r => diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index f8d2c08576..7080b94941 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -27,7 +27,8 @@ module Galley.API.MLS.Message where import Control.Comonad -import Control.Lens (preview, to) +import Control.Error.Util (hush) +import Control.Lens (preview) import Data.Id import Data.Json.Util import Data.List.NonEmpty (NonEmpty, nonEmpty) @@ -40,6 +41,7 @@ import Galley.API.Action import Galley.API.Error import Galley.API.MLS.KeyPackage import Galley.API.MLS.Propagate +import Galley.API.MLS.Removal import Galley.API.MLS.Types import Galley.API.MLS.Util import Galley.API.MLS.Welcome (postMLSWelcome) @@ -103,7 +105,9 @@ type MLSMessageStaticErrors = ErrorS 'MLSCommitMissingReferences, ErrorS 'MLSSelfRemovalNotAllowed, ErrorS 'MLSClientSenderUserMismatch, - ErrorS 'MLSGroupConversationMismatch + ErrorS 'MLSUnexpectedSenderClient, + ErrorS 'MLSGroupConversationMismatch, + ErrorS 'MLSMissingSenderClient ] type MLSBundleStaticErrors = @@ -119,14 +123,16 @@ postMLSMessageFromLocalUserV1 :: ErrorS 'ConvAccessDenied, ErrorS 'ConvMemberNotFound, ErrorS 'ConvNotFound, - ErrorS 'MissingLegalholdConsent, ErrorS 'MLSClientSenderUserMismatch, ErrorS 'MLSCommitMissingReferences, ErrorS 'MLSGroupConversationMismatch, + ErrorS 'MLSMissingSenderClient, ErrorS 'MLSProposalNotFound, ErrorS 'MLSSelfRemovalNotAllowed, ErrorS 'MLSStaleMessage, + ErrorS 'MLSUnexpectedSenderClient, ErrorS 'MLSUnsupportedMessage, + ErrorS 'MissingLegalholdConsent, Input (Local ()), ProposalStore, Resource, @@ -135,14 +141,15 @@ postMLSMessageFromLocalUserV1 :: r ) => Local UserId -> + Maybe ClientId -> ConnId -> RawMLS SomeMessage -> Sem r [Event] -postMLSMessageFromLocalUserV1 lusr conn smsg = case rmValue smsg of +postMLSMessageFromLocalUserV1 lusr mc conn smsg = case rmValue smsg of SomeMessage _ msg -> do qcnv <- getConversationIdByGroupId (msgGroupId msg) >>= noteS @'ConvNotFound map lcuEvent - <$> postMLSMessage lusr (qUntagged lusr) qcnv (Just conn) smsg + <$> postMLSMessage lusr (qUntagged lusr) mc qcnv (Just conn) smsg postMLSMessageFromLocalUser :: ( HasProposalEffects r, @@ -152,14 +159,16 @@ postMLSMessageFromLocalUser :: ErrorS 'ConvAccessDenied, ErrorS 'ConvMemberNotFound, ErrorS 'ConvNotFound, - ErrorS 'MissingLegalholdConsent, ErrorS 'MLSClientSenderUserMismatch, ErrorS 'MLSCommitMissingReferences, ErrorS 'MLSGroupConversationMismatch, + ErrorS 'MLSMissingSenderClient, ErrorS 'MLSProposalNotFound, ErrorS 'MLSSelfRemovalNotAllowed, ErrorS 'MLSStaleMessage, + ErrorS 'MLSUnexpectedSenderClient, ErrorS 'MLSUnsupportedMessage, + ErrorS 'MissingLegalholdConsent, Input (Local ()), ProposalStore, Resource, @@ -168,13 +177,14 @@ postMLSMessageFromLocalUser :: r ) => Local UserId -> + Maybe ClientId -> ConnId -> RawMLS SomeMessage -> Sem r MLSMessageSendingStatus -postMLSMessageFromLocalUser lusr conn msg = do +postMLSMessageFromLocalUser lusr mc conn msg = do -- FUTUREWORK: Inline the body of 'postMLSMessageFromLocalUserV1' once version -- V1 is dropped - events <- postMLSMessageFromLocalUserV1 lusr conn msg + events <- postMLSMessageFromLocalUserV1 lusr mc conn msg t <- toUTCTimeMillis <$> input pure $ MLSMessageSendingStatus events t @@ -198,14 +208,15 @@ postMLSCommitBundle :: ) => Local x -> Qualified UserId -> + Maybe ClientId -> Qualified ConvId -> Maybe ConnId -> CommitBundle -> Sem r [LocalConversationUpdate] -postMLSCommitBundle loc qusr qcnv conn rawBundle = +postMLSCommitBundle loc qusr mc qcnv conn rawBundle = foldQualified loc - (postMLSCommitBundleToLocalConv qusr conn rawBundle) + (postMLSCommitBundleToLocalConv qusr mc conn rawBundle) (postMLSCommitBundleToRemoteConv loc qusr conn rawBundle) qcnv @@ -227,15 +238,16 @@ postMLSCommitBundleFromLocalUser :: r ) => Local UserId -> + Maybe ClientId -> ConnId -> CommitBundle -> Sem r MLSMessageSendingStatus -postMLSCommitBundleFromLocalUser lusr conn bundle = do +postMLSCommitBundleFromLocalUser lusr mc conn bundle = do let msg = rmValue (cbCommitMsg bundle) qcnv <- getConversationIdByGroupId (msgGroupId msg) >>= noteS @'ConvNotFound events <- map lcuEvent - <$> postMLSCommitBundle lusr (qUntagged lusr) qcnv (Just conn) bundle + <$> postMLSCommitBundle lusr (qUntagged lusr) mc qcnv (Just conn) bundle t <- toUTCTimeMillis <$> input pure $ MLSMessageSendingStatus events t @@ -247,7 +259,6 @@ postMLSCommitBundleToLocalConv :: Error FederationError, Error InternalError, Error MLSProtocolError, - Input (Local ()), Input Opts, Input UTCTime, ProposalStore, @@ -257,22 +268,25 @@ postMLSCommitBundleToLocalConv :: r ) => Qualified UserId -> + Maybe ClientId -> Maybe ConnId -> CommitBundle -> Local ConvId -> Sem r [LocalConversationUpdate] -postMLSCommitBundleToLocalConv qusr conn bundle lcnv = do +postMLSCommitBundleToLocalConv qusr mc conn bundle lcnv = do let msg = rmValue (cbCommitMsg bundle) conv <- getLocalConvForUser qusr lcnv + mlsMeta <- Data.mlsMetadata conv & noteS @'ConvNotFound + let lconv = qualifyAs lcnv conv - cm <- lookupMLSClients lcnv + cm <- lookupMLSClients (cnvmlsGroupId mlsMeta) - senderClient <- fmap ciClient <$> getSenderClient qusr SMLSPlainText msg + senderClient <- fmap ciClient <$> getSenderIdentity qusr mc SMLSPlainText msg events <- case msgPayload msg of CommitMessage commit -> do - (groupId, action) <- getCommitData lconv (msgEpoch msg) commit + action <- getCommitData lconv mlsMeta (msgEpoch msg) commit -- check that the welcome message matches the action for_ (cbWelcome bundle) $ \welcome -> when @@ -280,18 +294,20 @@ postMLSCommitBundleToLocalConv qusr conn bundle lcnv = do /= 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 + updates <- + processCommitWithAction + qusr + senderClient + conn + lconv + mlsMeta + cm + (msgEpoch msg) + action + (msgSender msg) + commit + storeGroupInfoBundle lconv (cbGroupInfoBundle bundle) + pure updates ApplicationMessage _ -> throwS @'MLSUnsupportedMessage ProposalMessage _ -> throwS @'MLSUnsupportedMessage @@ -354,14 +370,16 @@ postMLSMessage :: ErrorS 'ConvAccessDenied, ErrorS 'ConvMemberNotFound, ErrorS 'ConvNotFound, - ErrorS 'MissingLegalholdConsent, ErrorS 'MLSClientSenderUserMismatch, ErrorS 'MLSCommitMissingReferences, ErrorS 'MLSGroupConversationMismatch, + ErrorS 'MLSMissingSenderClient, ErrorS 'MLSProposalNotFound, ErrorS 'MLSSelfRemovalNotAllowed, ErrorS 'MLSStaleMessage, + ErrorS 'MLSUnexpectedSenderClient, ErrorS 'MLSUnsupportedMessage, + ErrorS 'MissingLegalholdConsent, Input (Local ()), ProposalStore, Resource, @@ -371,17 +389,18 @@ postMLSMessage :: ) => Local x -> Qualified UserId -> + Maybe ClientId -> Qualified ConvId -> Maybe ConnId -> RawMLS SomeMessage -> Sem r [LocalConversationUpdate] -postMLSMessage loc qusr qcnv con smsg = case rmValue smsg of +postMLSMessage loc qusr mc qcnv con smsg = case rmValue smsg of SomeMessage tag msg -> do - mcid <- fmap ciClient <$> getSenderClient qusr tag msg + mSender <- fmap ciClient <$> getSenderIdentity qusr mc tag msg foldQualified loc - (postMLSMessageToLocalConv qusr mcid con smsg) - (postMLSMessageToRemoteConv loc qusr mcid con smsg) + (postMLSMessageToLocalConv qusr mSender con smsg) + (postMLSMessageToRemoteConv loc qusr mSender con smsg) qcnv -- Check that the MLS client who created the message belongs to the user who @@ -399,7 +418,7 @@ getSenderClient :: Qualified UserId -> SWireFormatTag tag -> Message tag -> - Sem r (Maybe ClientIdentity) + Sem r (Maybe ClientId) getSenderClient _ SMLSCipherText _ = pure Nothing getSenderClient _ _ msg | msgEpoch msg == Epoch 0 = pure Nothing getSenderClient qusr SMLSPlainText msg = case msgSender msg of @@ -409,7 +428,30 @@ getSenderClient qusr SMLSPlainText msg = case msgSender msg of cid <- derefKeyPackage ref when (fmap fst (cidQualifiedClient cid) /= qusr) $ throwS @'MLSClientSenderUserMismatch - pure (Just cid) + pure (Just (ciClient cid)) + +-- FUTUREWORK: once we can assume that the Z-Client header is present (i.e. +-- when v2 is dropped), remove the Maybe in the return type. +getSenderIdentity :: + ( Members + '[ ErrorS 'MLSKeyPackageRefNotFound, + ErrorS 'MLSClientSenderUserMismatch, + BrigAccess + ] + r + ) => + Qualified UserId -> + Maybe ClientId -> + SWireFormatTag tag -> + Message tag -> + Sem r (Maybe ClientIdentity) +getSenderIdentity qusr mc fmt msg = do + mSender <- getSenderClient qusr fmt msg + -- At this point, mc is the client ID of the request, while mSender is the + -- one contained in the message. We throw an error if the two don't match. + when (((==) <$> mc <*> mSender) == Just False) $ + throwS @'MLSClientSenderUserMismatch + pure (mkClientIdentity qusr <$> (mc <|> mSender)) postMLSMessageToLocalConv :: ( HasProposalEffects r, @@ -417,14 +459,15 @@ postMLSMessageToLocalConv :: '[ Error FederationError, Error InternalError, ErrorS 'ConvNotFound, - ErrorS 'MissingLegalholdConsent, ErrorS 'MLSClientSenderUserMismatch, ErrorS 'MLSCommitMissingReferences, + ErrorS 'MLSMissingSenderClient, ErrorS 'MLSProposalNotFound, ErrorS 'MLSSelfRemovalNotAllowed, ErrorS 'MLSStaleMessage, + ErrorS 'MLSUnexpectedSenderClient, ErrorS 'MLSUnsupportedMessage, - Input (Local ()), + ErrorS 'MissingLegalholdConsent, ProposalStore, Resource, TinyLog @@ -440,26 +483,26 @@ postMLSMessageToLocalConv :: postMLSMessageToLocalConv qusr senderClient con smsg lcnv = case rmValue smsg of SomeMessage tag msg -> do conv <- getLocalConvForUser qusr lcnv + mlsMeta <- Data.mlsMetadata conv & noteS @'ConvNotFound -- construct client map - cm <- lookupMLSClients lcnv + cm <- lookupMLSClients (cnvmlsGroupId mlsMeta) let lconv = qualifyAs lcnv conv -- validate message events <- case tag of SMLSPlainText -> case msgPayload msg of CommitMessage c -> - processCommit qusr senderClient con lconv cm (msgEpoch msg) (msgSender msg) c + processCommit qusr senderClient con lconv mlsMeta cm (msgEpoch msg) (msgSender msg) c ApplicationMessage _ -> throwS @'MLSUnsupportedMessage ProposalMessage prop -> - processProposal qusr conv msg prop $> mempty + processProposal qusr conv mlsMeta msg prop $> mempty SMLSCipherText -> case toMLSEnum' (msgContentType (msgPayload msg)) of Right CommitMessageTag -> throwS @'MLSUnsupportedMessage Right ProposalMessageTag -> throwS @'MLSUnsupportedMessage Right ApplicationMessageTag -> pure mempty Left _ -> throwS @'MLSUnsupportedMessage - -- forward message propagateMessage qusr lconv cm con (rmRaw smsg) pure events @@ -501,27 +544,29 @@ postMLSMessageToRemoteConv loc qusr _senderClient con smsg rcnv = do pure (LocalConversationUpdate e update) type HasProposalEffects r = - ( Member BrigAccess r, - Member ConversationStore r, - Member (Error InternalError) r, - Member (Error MLSProposalFailure) r, - Member (Error MLSProtocolError) 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 ProposalStore r, - Member TeamStore r, - Member TeamStore r, - Member TinyLog r + ( Members + '[ BrigAccess, + ConversationStore, + Error InternalError, + Error MLSProposalFailure, + Error MLSProtocolError, + ErrorS 'MLSClientMismatch, + ErrorS 'MLSKeyPackageRefNotFound, + ErrorS 'MLSUnsupportedProposal, + ExternalAccess, + FederatorAccess, + GundeckAccess, + Input Env, + Input (Local ()), + Input Opts, + Input UTCTime, + LegalHoldStore, + MemberStore, + ProposalStore, + TeamStore, + TinyLog + ] + r ) data ProposalAction = ProposalAction @@ -564,218 +609,337 @@ getCommitData :: Member TinyLog r ) => Local Data.Conversation -> + ConversationMLSData -> 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 + Sem r ProposalAction +getCommitData lconv mlsMeta epoch commit = do + let curEpoch = cnvmlsEpoch mlsMeta + groupId = cnvmlsGroupId mlsMeta + suite = cnvmlsCipherSuite mlsMeta -- check epoch number when (epoch /= curEpoch) $ throwS @'MLSStaleMessage - action <- foldMap (applyProposalRef (tUnqualified lconv) groupId epoch) (cProposals commit) - pure (groupId, action) + foldMap (applyProposalRef (tUnqualified lconv) mlsMeta groupId epoch suite) (cProposals commit) processCommit :: ( HasProposalEffects r, - Member (Error FederationError) r, - Member (Error InternalError) r, - Member (ErrorS 'ConvNotFound) r, - Member (ErrorS 'MLSClientSenderUserMismatch) 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 + Members + '[ Error FederationError, + Error InternalError, + ErrorS 'ConvNotFound, + ErrorS 'MLSClientSenderUserMismatch, + ErrorS 'MLSCommitMissingReferences, + ErrorS 'MLSMissingSenderClient, + ErrorS 'MLSProposalNotFound, + ErrorS 'MLSSelfRemovalNotAllowed, + ErrorS 'MLSStaleMessage, + ErrorS 'MLSUnexpectedSenderClient, + ErrorS 'MissingLegalholdConsent, + Input (Local ()), + ProposalStore, + BrigAccess, + Resource + ] + r ) => Qualified UserId -> Maybe ClientId -> Maybe ConnId -> Local Data.Conversation -> + ConversationMLSData -> ClientMap -> Epoch -> Sender 'MLSPlainText -> Commit -> Sem r [LocalConversationUpdate] -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 +processCommit qusr senderClient con lconv mlsMeta cm epoch sender commit = do + action <- getCommitData lconv mlsMeta epoch commit + processCommitWithAction qusr senderClient con lconv mlsMeta cm epoch action sender commit + +processExternalCommit :: + forall r. + Members + '[ BrigAccess, + ConversationStore, + Error MLSProtocolError, + ErrorS 'ConvNotFound, + ErrorS 'MLSClientSenderUserMismatch, + ErrorS 'MLSKeyPackageRefNotFound, + ErrorS 'MLSStaleMessage, + ErrorS 'MLSMissingSenderClient, + ExternalAccess, + FederatorAccess, + GundeckAccess, + Input Env, + Input UTCTime, + MemberStore, + ProposalStore, + Resource, + TinyLog + ] + r => + Qualified UserId -> + Maybe ClientId -> + Local Data.Conversation -> + ConversationMLSData -> + ClientMap -> + Epoch -> + ProposalAction -> + Maybe UpdatePath -> + Sem r () +processExternalCommit qusr mSenderClient lconv mlsMeta cm epoch action updatePath = withCommitLock (cnvmlsGroupId mlsMeta) epoch $ do + newKeyPackage <- + upLeaf + <$> note + (mlsProtocolError "External commits need an update path") + updatePath + when (paExternalInit action == mempty) $ + throw . mlsProtocolError $ + "The external commit is missing an external init proposal" + unless (paAdd action == mempty) $ + throw . mlsProtocolError $ + "The external commit must not have add proposals" + + newRef <- + kpRef' newKeyPackage + & note (mlsProtocolError "An invalid key package in the update path") + + -- validate and update mapping in brig + eithCid <- + nkpresClientIdentity + <$$> validateAndAddKeyPackageRef + NewKeyPackage + { nkpConversation = Data.convId <$> qUntagged lconv, + nkpKeyPackage = KeyPackageData (rmRaw newKeyPackage) + } + cid <- either (\errMsg -> throw (mlsProtocolError ("Tried to add invalid KeyPackage: " <> errMsg))) pure eithCid + + unless (cidQualifiedUser cid == qusr) $ + throw . mlsProtocolError $ + "The external commit attempts to add another user" + + senderClient <- noteS @'MLSMissingSenderClient mSenderClient + + unless (ciClient cid == senderClient) $ + throw . mlsProtocolError $ + "The external commit attempts to add another client of the user, it must only add itself" + + -- check if there is a key package ref in the remove proposal + remRef <- + if Map.null (paRemove action) + then pure Nothing + else do + (remCid, r) <- derefUser (paRemove action) qusr + unless (cidQualifiedUser cid == cidQualifiedUser remCid) + . throw + . mlsProtocolError + $ "The external commit attempts to remove a client from a user other than themselves" + pure (Just r) + + updateKeyPackageMapping lconv (cnvmlsGroupId mlsMeta) qusr (ciClient cid) remRef newRef + + -- increment epoch number + setConversationEpoch (Data.convId (tUnqualified lconv)) (succ epoch) + -- fetch local conversation with new epoch + lc <- qualifyAs lconv <$> getLocalConvForUser qusr (convId <$> lconv) + -- fetch backend remove proposals of the previous epoch + kpRefs <- getPendingBackendRemoveProposals (cnvmlsGroupId mlsMeta) epoch + -- requeue backend remove proposals for the current epoch + removeClientsWithClientMap lc kpRefs cm qusr + where + derefUser :: ClientMap -> Qualified UserId -> Sem r (ClientIdentity, KeyPackageRef) + derefUser (Map.toList -> l) user = case l of + [(u, s)] -> do + unless (user == u) $ + throwS @'MLSClientSenderUserMismatch + ref <- snd <$> ensureSingleton s + ci <- derefKeyPackage ref + unless (cidQualifiedUser ci == user) $ + throwS @'MLSClientSenderUserMismatch + pure (ci, ref) + _ -> throwRemProposal + ensureSingleton :: Set a -> Sem r a + ensureSingleton (Set.toList -> l) = case l of + [e] -> pure e + _ -> throwRemProposal + throwRemProposal = + throw . mlsProtocolError $ + "The external commit must have at most one remove proposal" processCommitWithAction :: forall r. ( HasProposalEffects r, - Member (Error FederationError) r, - Member (Error InternalError) r, - Member (ErrorS 'ConvNotFound) r, - Member (ErrorS 'MLSClientSenderUserMismatch) 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 + Members + '[ Error FederationError, + Error InternalError, + ErrorS 'ConvNotFound, + ErrorS 'MLSClientSenderUserMismatch, + ErrorS 'MLSCommitMissingReferences, + ErrorS 'MLSMissingSenderClient, + ErrorS 'MLSProposalNotFound, + ErrorS 'MLSSelfRemovalNotAllowed, + ErrorS 'MLSStaleMessage, + ErrorS 'MLSUnexpectedSenderClient, + ErrorS 'MissingLegalholdConsent, + Input (Local ()), + ProposalStore, + BrigAccess, + Resource + ] + r ) => Qualified UserId -> Maybe ClientId -> Maybe ConnId -> Local Data.Conversation -> + ConversationMLSData -> 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 +processCommitWithAction qusr senderClient con lconv mlsMeta cm epoch action sender commit = + case sender of + MemberSender ref -> processInternalCommit qusr senderClient con lconv mlsMeta cm epoch action ref commit + NewMemberSender -> processExternalCommit qusr senderClient lconv mlsMeta cm epoch action (cPath commit) $> [] + _ -> throw (mlsProtocolError "Unexpected sender") + +processInternalCommit :: + forall r. + ( HasProposalEffects r, + Members + [ Error FederationError, + Error InternalError, + ErrorS 'ConvNotFound, + ErrorS 'MLSClientSenderUserMismatch, + ErrorS 'MLSCommitMissingReferences, + ErrorS 'MLSMissingSenderClient, + ErrorS 'MLSProposalNotFound, + ErrorS 'MLSSelfRemovalNotAllowed, + ErrorS 'MLSStaleMessage, + ErrorS 'MLSUnexpectedSenderClient, + ErrorS 'MissingLegalholdConsent, + Input (Local ()), + ProposalStore, + BrigAccess, + Resource + ] + r + ) => + Qualified UserId -> + Maybe ClientId -> + Maybe ConnId -> + Local Data.Conversation -> + ConversationMLSData -> + ClientMap -> + Epoch -> + ProposalAction -> + KeyPackageRef -> + Commit -> + Sem r [LocalConversationUpdate] +processInternalCommit qusr senderClient con lconv mlsMeta cm epoch action senderRef commit = do self <- noteS @'ConvNotFound $ getConvMember lconv (tUnqualified lconv) qusr - let ttlSeconds :: Int = 600 -- 10 minutes - withCommitLock groupId epoch (fromIntegral ttlSeconds) $ do - checkEpoch epoch (tUnqualified lconv) - (postponedKeyPackageRefUpdate, actionWithUpdate) <- + withCommitLock (cnvmlsGroupId mlsMeta) epoch $ do + postponedKeyPackageRefUpdate <- if epoch == Epoch 0 then do - -- this is a newly created conversation, and it should contain exactly one - -- client (the creator) - case (sender, self, cmAssocs cm) of - (MemberSender currentRef, Left lm, [(qu, (creatorClient, _))]) + let cType = cnvmType . convMetadata . tUnqualified $ lconv + case (self, cType, cmAssocs cm) of + (Left _, SelfConv, []) -> do + creatorClient <- noteS @'MLSMissingSenderClient senderClient + creatorRef <- + maybe + (pure senderRef) + ( note (mlsProtocolError "Could not compute key package ref") + . kpRef' + . upLeaf + ) + $ cPath commit + addMLSClients + (cnvmlsGroupId mlsMeta) + qusr + (Set.singleton (creatorClient, creatorRef)) + (Left _, SelfConv, _) -> + -- this is a newly created conversation, and it should contain exactly one + -- client (the creator) + throwS @'MLSUnexpectedSenderClient + (Left _, GlobalTeamConv, []) -> do + creatorClient <- noteS @'MLSMissingSenderClient senderClient + creatorRef <- + maybe + (pure senderRef) + ( note (mlsProtocolError "Could not compute key package ref") + . kpRef' + . upLeaf + ) + $ cPath commit + -- add user to global conv as a member as well + lusr <- qualifyLocal (qUnqualified qusr) + void $ createMember (convId <$> lconv) lusr + addMLSClients + (cnvmlsGroupId mlsMeta) + qusr + (Set.singleton (creatorClient, creatorRef)) + (Left _, GlobalTeamConv, _) -> + throwS @'MLSUnexpectedSenderClient + (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 <- + senderRef' <- maybe - (pure currentRef) + (pure senderRef) ( note (mlsProtocolError "Could not compute key package ref") . kpRef' . upLeaf ) $ cPath commit -- register the creator client - updateKeyPackageMapping lconv qusr creatorClient Nothing senderRef + updateKeyPackageMapping lconv (cnvmlsGroupId mlsMeta) 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 _, _, _) -> + (_, _, _) -> throw (InternalErrorWithDescription "Unexpected creator client set") - -- the sender of the first commit must be a member - _ -> throw (mlsProtocolError "Unexpected sender") - pure $ (pure (), action) -- no key package ref update necessary - else case (sender, upLeaf <$> cPath commit) of - (MemberSender senderRef, Just updatedKeyPackage) -> do + pure $ pure () -- no key package ref update necessary + else case upLeaf <$> cPath commit of + Just updatedKeyPackage -> do updatedRef <- kpRef' updatedKeyPackage & note (mlsProtocolError "Could not compute key package ref") -- postpone key package ref update until other checks/processing passed case senderClient of - Just cli -> pure (updateKeyPackageMapping lconv qusr cli (Just senderRef) updatedRef, action) - Nothing -> pure (pure (), action) - (_, Nothing) -> pure (pure (), action) -- ignore commits without update path - (NewMemberSender, Just newKeyPackage) -> do - -- this is an external commit - when (paExternalInit action == mempty) - . throw - . mlsProtocolError - $ "The external commit is missing an external init proposal" - unless (paAdd action == mempty) - . throw - . mlsProtocolError - $ "The external commit must not have add proposals" - - cid <- case kpIdentity (rmValue newKeyPackage) of - Left e -> throw (mlsProtocolError $ "Failed to parse the client identity: " <> e) - Right v -> pure v - newRef <- - kpRef' newKeyPackage - & note (mlsProtocolError "An invalid key package in the update path") - - -- check if there is a key package ref in the remove proposal - remRef <- - if Map.null (paRemove action) - then pure Nothing - else do - (remCid, r) <- derefUser (paRemove action) qusr - unless (cidQualifiedUser cid == cidQualifiedUser remCid) - . throw - . mlsProtocolError - $ "The external commit attempts to remove a client from a user other than themselves" - pure (Just r) - - -- first perform checks and map the key package if valid - addKeyPackageRef - newRef - (cidQualifiedUser cid) - (ciClient cid) - (Data.convId <$> qUntagged lconv) - -- now it is safe to update the mapping without further checks - updateKeyPackageMapping lconv qusr (ciClient cid) remRef newRef - - pure (pure (), action {paRemove = mempty}) - _ -> throw (mlsProtocolError "Unexpected sender") - - -- FUTUREWORK: Resubmit backend-provided proposals when processing an - -- external commit. - -- - -- check all pending proposals are referenced in the commit. Skip the check - -- if this is an external commit. - when (sender /= NewMemberSender) $ do - allPendingProposals <- getAllPendingProposals groupId epoch - let referencedProposals = Set.fromList $ mapMaybe (\x -> preview Proposal._Ref x) (cProposals commit) - unless (all (`Set.member` referencedProposals) allPendingProposals) $ - throwS @'MLSCommitMissingReferences + Just cli -> pure (updateKeyPackageMapping lconv (cnvmlsGroupId mlsMeta) qusr cli (Just senderRef) updatedRef) + Nothing -> pure (pure ()) + Nothing -> pure (pure ()) -- ignore commits without update path + + -- check all pending proposals are referenced in the commit + allPendingProposals <- getAllPendingProposalRefs (cnvmlsGroupId mlsMeta) epoch + let referencedProposals = Set.fromList $ mapMaybe (\x -> preview Proposal._Ref x) (cProposals commit) + unless (all (`Set.member` referencedProposals) allPendingProposals) $ + throwS @'MLSCommitMissingReferences -- process and execute proposals - updates <- executeProposalAction qusr con lconv cm actionWithUpdate + updates <- executeProposalAction qusr con lconv mlsMeta 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 - where - throwRemProposal = - throw . mlsProtocolError $ - "The external commit must have at most one remove proposal" - derefUser :: ClientMap -> Qualified UserId -> Sem r (ClientIdentity, KeyPackageRef) - derefUser (Map.toList -> l) user = case l of - [(u, s)] -> do - unless (user == u) $ - throwS @'MLSClientSenderUserMismatch - ref <- snd <$> ensureSingleton s - ci <- derefKeyPackage ref - unless (cidQualifiedUser ci == user) $ - throwS @'MLSClientSenderUserMismatch - pure (ci, ref) - _ -> throwRemProposal - ensureSingleton :: Set a -> Sem r a - ensureSingleton (Set.toList -> l) = case l of - [e] -> pure e - _ -> throwRemProposal -- | Note: Use this only for KeyPackage that are already validated updateKeyPackageMapping :: Members '[BrigAccess, MemberStore] r => Local Data.Conversation -> + GroupId -> Qualified UserId -> ClientId -> Maybe KeyPackageRef -> KeyPackageRef -> Sem r () -updateKeyPackageMapping lconv qusr cid mOld new = do +updateKeyPackageMapping lconv groupId qusr cid mOld new = do let lcnv = fmap Data.convId lconv -- update actual mapping in brig case mOld of @@ -789,9 +953,9 @@ updateKeyPackageMapping lconv qusr cid mOld new = do } -- remove old (client, key package) pair - removeMLSClients lcnv qusr (Set.singleton cid) + removeMLSClients groupId qusr (Set.singleton cid) -- add new (client, key package) pair - addMLSClients lcnv qusr (Set.singleton (cid, new)) + addMLSClients groupId qusr (Set.singleton (cid, new)) applyProposalRef :: ( HasProposalEffects r, @@ -804,29 +968,29 @@ applyProposalRef :: r ) => Data.Conversation -> + ConversationMLSData -> GroupId -> Epoch -> + CipherSuiteTag -> ProposalOrRef -> Sem r ProposalAction -applyProposalRef conv groupId epoch (Ref ref) = do +applyProposalRef conv mlsMeta groupId epoch _suite (Ref ref) = do p <- getProposal groupId epoch ref >>= noteS @'MLSProposalNotFound - checkEpoch epoch conv - checkGroup groupId conv - applyProposal (convId conv) (rmValue p) -applyProposalRef conv _groupId _epoch (Inline p) = do - suite <- - preview (to convProtocol . _ProtocolMLS . to cnvmlsCipherSuite) conv - & noteS @'ConvNotFound + checkEpoch epoch mlsMeta + checkGroup groupId mlsMeta + applyProposal (convId conv) groupId (rmValue p) +applyProposalRef conv _mlsMeta groupId _epoch suite (Inline p) = do checkProposalCipherSuite suite p - applyProposal (convId conv) p + applyProposal (convId conv) groupId p applyProposal :: forall r. HasProposalEffects r => ConvId -> + GroupId -> Proposal -> Sem r ProposalAction -applyProposal convId (AddProposal kp) = do +applyProposal convId groupId (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 @@ -842,27 +1006,27 @@ applyProposal convId (AddProposal kp) = do addKeyPackageMapping :: Local ConvId -> KeyPackageRef -> KeyPackageData -> Sem r ClientIdentity addKeyPackageMapping lconv ref kpdata = do -- validate and update mapping in brig - mCid <- + eithCid <- nkpresClientIdentity <$$> validateAndAddKeyPackageRef NewKeyPackage { nkpConversation = qUntagged lconv, nkpKeyPackage = kpdata } - cid <- mCid & note (mlsProtocolError "Tried to add invalid KeyPackage") + cid <- either (\errMsg -> throw (mlsProtocolError ("Tried to add invalid KeyPackage: " <> errMsg))) pure eithCid let qcid = cidQualifiedClient cid let qusr = fst <$> qcid -- update mapping in galley - addMLSClients lconv qusr (Set.singleton (ciClient cid, ref)) + addMLSClients groupId qusr (Set.singleton (ciClient cid, ref)) pure cid -applyProposal _conv (RemoveProposal ref) = do +applyProposal _conv _groupId (RemoveProposal ref) = do qclient <- cidQualifiedClient <$> derefKeyPackage ref pure (paRemoveClient ((,ref) <$$> qclient)) -applyProposal _conv (ExternalInitProposal _) = +applyProposal _conv _groupId (ExternalInitProposal _) = -- only record the fact there was an external init proposal, but do not -- process it in any way. pure paExternalInitPresent -applyProposal _conv _ = pure mempty +applyProposal _conv _groupId _ = pure mempty checkProposalCipherSuite :: Members @@ -898,15 +1062,14 @@ processProposal :: r => Qualified UserId -> Data.Conversation -> + ConversationMLSData -> Message 'MLSPlainText -> RawMLS Proposal -> Sem r () -processProposal qusr conv msg prop = do - checkEpoch (msgEpoch msg) conv - checkGroup (msgGroupId msg) conv - suiteTag <- - preview (to convProtocol . _ProtocolMLS . to cnvmlsCipherSuite) conv - & noteS @'ConvNotFound +processProposal qusr conv mlsMeta msg prop = do + checkEpoch (msgEpoch msg) mlsMeta + checkGroup (msgGroupId msg) mlsMeta + let suiteTag = cnvmlsCipherSuite mlsMeta -- validate the proposal -- @@ -932,7 +1095,7 @@ processProposal qusr conv msg prop = do checkExternalProposalSignature suiteTag msg prop checkExternalProposalUser qusr propValue let propRef = proposalRef suiteTag prop - storeProposal (msgGroupId msg) (msgEpoch msg) propRef prop + storeProposal (msgGroupId msg) (msgEpoch msg) propRef ProposalOriginClient prop checkExternalProposalSignature :: Members @@ -1020,14 +1183,32 @@ executeProposalAction :: Qualified UserId -> Maybe ConnId -> Local Data.Conversation -> + ConversationMLSData -> ClientMap -> ProposalAction -> Sem r [LocalConversationUpdate] -executeProposalAction qusr con lconv cm action = do - cs <- preview (to convProtocol . _ProtocolMLS . to cnvmlsCipherSuite) (tUnqualified lconv) & noteS @'ConvNotFound - let ss = csSignatureScheme cs +executeProposalAction qusr con lconv mlsMeta cm action = do + let ss = csSignatureScheme (cnvmlsCipherSuite mlsMeta) newUserClients = Map.assocs (paAdd action) - removeUserClients = Map.assocs (paRemove action) + + -- Note [client removal] + -- We support two types of removals: + -- 1. when a user is removed from a group, all their clients have to be removed + -- 2. when a client is deleted, that particular client (but not necessarily + -- other clients of the same user), has to be removed. + -- + -- Type 2 requires no special processing on the backend, so here we filter + -- out all removals of that type, so that further checks and processing can + -- be applied only to type 1 removals. + removedUsers <- mapMaybe hush <$$> for (Map.assocs (paRemove action)) $ + \(qtarget, Set.map fst -> clients) -> runError @() $ do + -- fetch clients from brig + clientInfo <- Set.map ciId <$> getClientInfo lconv qtarget ss + -- if the clients being removed don't exist, consider this as a removal of + -- type 2, and skip it + when (Set.null (clientInfo `Set.intersection` clients)) $ + throw () + pure (qtarget, clients) -- FUTUREWORK: remove this check after remote admins are implemented in federation https://wearezeta.atlassian.net/browse/FS-216 foldQualified lconv (\_ -> pure ()) (\_ -> throwS @'MLSUnsupportedProposal) qusr @@ -1041,7 +1222,7 @@ executeProposalAction qusr con lconv cm action = do -- final set of clients in the conversation let clients = Set.map fst (newclients <> Map.findWithDefault mempty qtarget cm) -- get list of mls clients from brig - clientInfo <- getMLSClients lconv qtarget ss + clientInfo <- getClientInfo lconv qtarget ss let allClients = Set.map ciId clientInfo let allMLSClients = Set.map ciId (Set.filter ciMLS clientInfo) -- We check the following condition: @@ -1062,46 +1243,37 @@ executeProposalAction qusr con lconv cm action = do -- FUTUREWORK: turn this error into a proper response throwS @'MLSClientMismatch - membersToRemove <- catMaybes <$> for removeUserClients (uncurry (checkRemoval lconv ss)) + membersToRemove <- catMaybes <$> for removedUsers (uncurry checkRemoval) -- add users to the conversation and send events addEvents <- foldMap addMembers . nonEmpty . map fst $ newUserClients -- add clients in the conversation state for_ newUserClients $ \(qtarget, newClients) -> do - addMLSClients (fmap convId lconv) qtarget newClients + addMLSClients (cnvmlsGroupId mlsMeta) 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) + -- Remove clients from the conversation state. This includes client removals + -- of all types (see Note [client removal]). + for_ (Map.assocs (paRemove action)) $ \(qtarget, clients) -> do + removeMLSClients (cnvmlsGroupId mlsMeta) 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, KeyPackageRef) -> + Set ClientId -> 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 - then pure Nothing - else do - -- We only support removal of client for user. This is likely to change in the future. - -- See discussions here https://wearezeta.atlassian.net/wiki/spaces/CL/pages/612106259/Relax+constraint+between+users+and+clients+in+MLS+groups - when (clients /= allClients) $ do - -- FUTUREWORK: turn this error into a proper response - throwS @'MLSClientMismatch - when (qusr == qtarget) $ - throwS @'MLSSelfRemovalNotAllowed - pure (Just qtarget) + checkRemoval qtarget clients = do + let clientsInConv = Set.map fst (Map.findWithDefault mempty qtarget cm) + when (clients /= clientsInConv) $ do + -- FUTUREWORK: turn this error into a proper response + throwS @'MLSClientMismatch + when (qusr == qtarget) $ + throwS @'MLSSelfRemovalNotAllowed + pure (Just qtarget) existingLocalMembers :: Set (Qualified UserId) existingLocalMembers = @@ -1144,13 +1316,13 @@ executeProposalAction qusr con lconv cm action = do handleNoChanges :: Monoid a => Sem (Error NoChanges ': r) a -> Sem r a handleNoChanges = fmap fold . runError -getMLSClients :: +getClientInfo :: Members '[BrigAccess, FederatorAccess] r => Local x -> Qualified UserId -> SignatureSchemeTag -> Sem r (Set ClientInfo) -getMLSClients loc = foldQualified loc getLocalMLSClients getRemoteMLSClients +getClientInfo loc = foldQualified loc getLocalMLSClients getRemoteMLSClients getRemoteMLSClients :: Member FederatorAccess r => @@ -1168,30 +1340,23 @@ getRemoteMLSClients rusr ss = do -- | Check if the epoch number matches that of a conversation checkEpoch :: Members - '[ ErrorS 'ConvNotFound, - ErrorS 'MLSStaleMessage + '[ ErrorS 'MLSStaleMessage ] r => Epoch -> - Data.Conversation -> + ConversationMLSData -> Sem r () -checkEpoch epoch conv = do - curEpoch <- - preview (to convProtocol . _ProtocolMLS . to cnvmlsEpoch) conv - & noteS @'ConvNotFound - unless (epoch == curEpoch) $ throwS @'MLSStaleMessage +checkEpoch epoch mlsMeta = do + unless (epoch == cnvmlsEpoch mlsMeta) $ throwS @'MLSStaleMessage -- | Check if the group ID matches that of a conversation checkGroup :: Member (ErrorS 'ConvNotFound) r => GroupId -> - Data.Conversation -> + ConversationMLSData -> Sem r () -checkGroup gId conv = do - groupId <- - preview (to convProtocol . _ProtocolMLS . to cnvmlsGroupId) conv - & noteS @'ConvNotFound - unless (gId == groupId) $ throwS @'ConvNotFound +checkGroup gId mlsMeta = do + unless (gId == cnvmlsGroupId mlsMeta) $ throwS @'ConvNotFound -------------------------------------------------------------------------------- -- Error handling of proposal execution @@ -1246,14 +1411,27 @@ withCommitLock :: ) => GroupId -> Epoch -> - NominalDiffTime -> Sem r a -> Sem r a -withCommitLock gid epoch ttl action = +withCommitLock gid epoch action = bracket ( acquireCommitLock gid epoch ttl >>= \lockAcquired -> when (lockAcquired == NotAcquired) $ throwS @'MLSStaleMessage ) (const $ releaseCommitLock gid epoch) - (const action) + $ \_ -> do + -- FUTUREWORK: fetch epoch again and check that is matches + action + where + ttl = fromIntegral (600 :: Int) -- 10 minutes + +storeGroupInfoBundle :: + Member ConversationStore r => + Local Data.Conversation -> + GroupInfoBundle -> + Sem r () +storeGroupInfoBundle lconv = + setPublicGroupState (Data.convId (tUnqualified lconv)) + . toOpaquePublicGroupState + . gipGroupState diff --git a/services/galley/src/Galley/API/MLS/Removal.hs b/services/galley/src/Galley/API/MLS/Removal.hs index 0692ecfa62..f16edf2bd2 100644 --- a/services/galley/src/Galley/API/MLS/Removal.hs +++ b/services/galley/src/Galley/API/MLS/Removal.hs @@ -23,7 +23,6 @@ module Galley.API.MLS.Removal ) where -import Control.Comonad import Data.Id import qualified Data.Map as Map import Data.Qualified @@ -58,14 +57,14 @@ removeClientsWithClientMap :: ExternalAccess, FederatorAccess, GundeckAccess, - Error InternalError, ProposalStore, Input Env ] - r + r, + Traversable t ) => Local Data.Conversation -> - Set (ClientId, KeyPackageRef) -> + t KeyPackageRef -> ClientMap -> Qualified UserId -> Sem r () @@ -78,7 +77,7 @@ removeClientsWithClientMap lc cs cm qusr = do Nothing -> do warn $ Log.msg ("No backend removal key is configured (See 'mlsPrivateKeyPaths' in galley's config). Not able to remove client from MLS conversation." :: Text) Just (secKey, pubKey) -> do - for_ cs $ \(_client, kpref) -> do + for_ cs $ \kpref -> do let proposal = mkRemoveProposal kpref msg = mkSignedMessage secKey pubKey (cnvmlsGroupId meta) (cnvmlsEpoch meta) (ProposalMessage proposal) msgEncoded = encodeMLS' msg @@ -86,6 +85,7 @@ removeClientsWithClientMap lc cs cm qusr = do (cnvmlsGroupId meta) (cnvmlsEpoch meta) (proposalRef (cnvmlsCipherSuite meta) proposal) + ProposalOriginBackend proposal propagateMessage qusr lc cm Nothing msgEncoded @@ -109,9 +109,10 @@ removeClient :: 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 + for_ (cnvmlsGroupId <$> Data.mlsMetadata (tUnqualified lc)) $ \groupId -> do + cm <- lookupMLSClients groupId + let cidAndKP = Set.toList . Set.map snd . 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. -- @@ -134,7 +135,7 @@ removeUserWithClientMap :: Qualified UserId -> Sem r () removeUserWithClientMap lc cm qusr = - removeClientsWithClientMap lc (Map.findWithDefault mempty qusr cm) cm qusr + removeClientsWithClientMap lc (Set.toList . Set.map snd $ Map.findWithDefault mempty qusr cm) cm qusr -- | Send remove proposals for all clients of the user to the local conversation. removeUser :: @@ -155,5 +156,6 @@ removeUser :: Qualified UserId -> Sem r () removeUser lc qusr = do - cm <- lookupMLSClients (fmap Data.convId lc) - removeUserWithClientMap lc cm qusr + for_ (Data.mlsMetadata (tUnqualified lc)) $ \meta -> do + cm <- lookupMLSClients (cnvmlsGroupId meta) + removeUserWithClientMap lc cm qusr diff --git a/services/galley/src/Galley/API/MLS/Types.hs b/services/galley/src/Galley/API/MLS/Types.hs index f9b6cefb8e..935702c3a5 100644 --- a/services/galley/src/Galley/API/MLS/Types.hs +++ b/services/galley/src/Galley/API/MLS/Types.hs @@ -19,6 +19,7 @@ module Galley.API.MLS.Types ( ClientMap, mkClientMap, cmAssocs, + ListGlobalSelfConvs (..), ) where @@ -41,3 +42,9 @@ mkClientMap = foldr addEntry mempty cmAssocs :: ClientMap -> [(Qualified UserId, (ClientId, KeyPackageRef))] cmAssocs cm = Map.assocs cm >>= traverse toList + +-- | Inform a handler for 'POST /conversations/list-ids' if the MLS global team +-- conversation and the MLS self-conversation should be included in the +-- response. +data ListGlobalSelfConvs = ListGlobalSelf | DoNotListGlobalSelf + deriving (Eq) diff --git a/services/galley/src/Galley/API/MLS/Util.hs b/services/galley/src/Galley/API/MLS/Util.hs index 304926137c..9738b5b046 100644 --- a/services/galley/src/Galley/API/MLS/Util.hs +++ b/services/galley/src/Galley/API/MLS/Util.hs @@ -20,23 +20,31 @@ 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 Galley.Data.Conversation import qualified Galley.Data.Conversation.Types as Data import Galley.Effects import Galley.Effects.ConversationStore import Galley.Effects.MemberStore +import Galley.Effects.ProposalStore +import Galley.Types.Conversations.Members import Imports import Polysemy -import Polysemy.Input +import Polysemy.TinyLog (TinyLog) +import qualified Polysemy.TinyLog as TinyLog +import qualified System.Logger as Log +import Wire.API.Conversation hiding (Conversation) +import Wire.API.Conversation.Protocol import Wire.API.Error import Wire.API.Error.Galley +import Wire.API.MLS.GlobalTeamConversation +import Wire.API.MLS.KeyPackage +import Wire.API.MLS.Proposal +import Wire.API.MLS.Serialisation getLocalConvForUser :: Members '[ ErrorS 'ConvNotFound, ConversationStore, - Input (Local ()), MemberStore ] r => @@ -44,11 +52,67 @@ getLocalConvForUser :: Local ConvId -> Sem r Data.Conversation getLocalConvForUser qusr lcnv = do - conv <- getConversation (tUnqualified lcnv) >>= noteS @'ConvNotFound + gtc <- getGlobalTeamConversationById lcnv + conv <- case gtc of + Just conv -> do + localMembers <- getLocalMembers (qUnqualified . gtcId $ conv) + pure $ gtcToConv conv (qUnqualified qusr) localMembers + Nothing -> do + 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 + isMember' <- + foldQualified + lcnv + (fmap isJust . getLocalMember (convId conv) . tUnqualified) + (fmap isJust . getRemoteMember (convId conv)) + qusr unless isMember' $ throwS @'ConvNotFound pure conv + +getPendingBackendRemoveProposals :: + Members '[ProposalStore, TinyLog] r => + GroupId -> + Epoch -> + Sem r [KeyPackageRef] +getPendingBackendRemoveProposals gid epoch = do + proposals <- getAllPendingProposals gid epoch + catMaybes + <$> for + proposals + ( \case + (Just ProposalOriginBackend, proposal) -> case rmValue proposal of + RemoveProposal kp -> pure . Just $ kp + _ -> pure Nothing + (Just ProposalOriginClient, _) -> pure Nothing + (Nothing, _) -> do + TinyLog.warn $ Log.msg ("found pending proposal without origin, ignoring" :: ByteString) + pure Nothing + ) + +gtcToConv :: + GlobalTeamConversation -> + UserId -> + [LocalMember] -> + Conversation +gtcToConv gtc usr lm = + let mlsData = gtcMlsMetadata gtc + in Conversation + { convId = qUnqualified $ gtcId gtc, + convLocalMembers = lm, + convRemoteMembers = mempty, + convDeleted = False, + convMetadata = + ConversationMetadata + { cnvmType = GlobalTeamConv, + cnvmCreator = usr, + cnvmAccess = [SelfInviteAccess], + cnvmAccessRoles = mempty, + cnvmName = Just $ gtcName gtc, + cnvmTeam = Just $ gtcTeam gtc, + cnvmMessageTimer = Nothing, + cnvmReceiptMode = Nothing + }, + convProtocol = ProtocolMLS mlsData + } diff --git a/services/galley/src/Galley/API/Message.hs b/services/galley/src/Galley/API/Message.hs index 199129610a..a16128fa37 100644 --- a/services/galley/src/Galley/API/Message.hs +++ b/services/galley/src/Galley/API/Message.hs @@ -79,7 +79,7 @@ import Wire.API.Federation.API.Brig import Wire.API.Federation.API.Galley import Wire.API.Federation.Error import Wire.API.Message -import Wire.API.Routes.Public.Galley +import Wire.API.Routes.Public.Galley.Messaging import Wire.API.Team.LegalHold import Wire.API.Team.Member import Wire.API.User.Client diff --git a/services/galley/src/Galley/API/Public/Bot.hs b/services/galley/src/Galley/API/Public/Bot.hs new file mode 100644 index 0000000000..8c75ddbdee --- /dev/null +++ b/services/galley/src/Galley/API/Public/Bot.hs @@ -0,0 +1,26 @@ +-- 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.Public.Bot where + +import Galley.API.Update +import Galley.App +import Wire.API.Routes.API +import Wire.API.Routes.Public.Galley.Bot + +botAPI :: API BotAPI GalleyEffects +botAPI = mkNamedAPI @"post-bot-message-unqualified" postBotMessageUnqualified diff --git a/services/galley/src/Galley/API/Public/Conversation.hs b/services/galley/src/Galley/API/Public/Conversation.hs new file mode 100644 index 0000000000..78f90daf18 --- /dev/null +++ b/services/galley/src/Galley/API/Public/Conversation.hs @@ -0,0 +1,75 @@ +-- 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.Public.Conversation where + +import Galley.API.Create +import Galley.API.MLS.GroupInfo +import Galley.API.MLS.Types +import Galley.API.Query +import Galley.API.Update +import Galley.App +import Galley.Cassandra.TeamFeatures +import Wire.API.Routes.API +import Wire.API.Routes.Public.Galley.Conversation + +conversationAPI :: API ConversationAPI GalleyEffects +conversationAPI = + mkNamedAPI @"get-unqualified-conversation" getUnqualifiedConversation + <@> mkNamedAPI @"get-unqualified-conversation-legalhold-alias" getUnqualifiedConversation + <@> mkNamedAPI @"get-conversation" getConversation + -- <@> mkNamedAPI @"get-global-team-conversation" getGlobalTeamConversation + <@> mkNamedAPI @"get-conversation-roles" getConversationRoles + <@> mkNamedAPI @"get-group-info" getGroupInfo + <@> mkNamedAPI @"list-conversation-ids-unqualified" conversationIdsPageFromUnqualified + <@> mkNamedAPI @"list-conversation-ids-v2" (conversationIdsPageFromV2 DoNotListGlobalSelf) + <@> mkNamedAPI @"list-conversation-ids" conversationIdsPageFrom + <@> mkNamedAPI @"get-conversations" getConversations + <@> mkNamedAPI @"list-conversations-v1" listConversations + <@> mkNamedAPI @"list-conversations" listConversations + <@> mkNamedAPI @"get-conversation-by-reusable-code" (getConversationByReusableCode @Cassandra) + <@> mkNamedAPI @"create-group-conversation" createGroupConversation + <@> mkNamedAPI @"create-self-conversation" createProteusSelfConversation + <@> mkNamedAPI @"get-mls-self-conversation" getMLSSelfConversation + <@> mkNamedAPI @"create-one-to-one-conversation" createOne2OneConversation + <@> mkNamedAPI @"add-members-to-conversation-unqualified" addMembersUnqualified + <@> mkNamedAPI @"add-members-to-conversation-unqualified2" addMembersUnqualifiedV2 + <@> mkNamedAPI @"add-members-to-conversation" addMembers + <@> mkNamedAPI @"join-conversation-by-id-unqualified" (joinConversationById @Cassandra) + <@> mkNamedAPI @"join-conversation-by-code-unqualified" (joinConversationByReusableCode @Cassandra) + <@> mkNamedAPI @"code-check" (checkReusableCode @Cassandra) + <@> mkNamedAPI @"create-conversation-code-unqualified" (addCodeUnqualified @Cassandra) + <@> mkNamedAPI @"get-conversation-guest-links-status" (getConversationGuestLinksStatus @Cassandra) + <@> mkNamedAPI @"remove-code-unqualified" rmCodeUnqualified + <@> mkNamedAPI @"get-code" (getCode @Cassandra) + <@> mkNamedAPI @"member-typing-unqualified" isTypingUnqualified + <@> mkNamedAPI @"remove-member-unqualified" removeMemberUnqualified + <@> mkNamedAPI @"remove-member" removeMemberQualified + <@> mkNamedAPI @"update-other-member-unqualified" updateOtherMemberUnqualified + <@> mkNamedAPI @"update-other-member" updateOtherMember + <@> mkNamedAPI @"update-conversation-name-deprecated" updateUnqualifiedConversationName + <@> mkNamedAPI @"update-conversation-name-unqualified" updateUnqualifiedConversationName + <@> mkNamedAPI @"update-conversation-name" updateConversationName + <@> mkNamedAPI @"update-conversation-message-timer-unqualified" updateConversationMessageTimerUnqualified + <@> mkNamedAPI @"update-conversation-message-timer" updateConversationMessageTimer + <@> mkNamedAPI @"update-conversation-receipt-mode-unqualified" updateConversationReceiptModeUnqualified + <@> mkNamedAPI @"update-conversation-receipt-mode" updateConversationReceiptMode + <@> mkNamedAPI @"update-conversation-access-unqualified" updateConversationAccessUnqualified + <@> mkNamedAPI @"update-conversation-access" updateConversationAccess + <@> mkNamedAPI @"get-conversation-self-unqualified" getLocalSelf + <@> mkNamedAPI @"update-conversation-self-unqualified" updateUnqualifiedSelfMember + <@> mkNamedAPI @"update-conversation-self" updateSelfMember diff --git a/services/galley/src/Galley/API/Public/CustomBackend.hs b/services/galley/src/Galley/API/Public/CustomBackend.hs new file mode 100644 index 0000000000..23b79abedf --- /dev/null +++ b/services/galley/src/Galley/API/Public/CustomBackend.hs @@ -0,0 +1,26 @@ +-- 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.Public.CustomBackend where + +import Galley.API.CustomBackend +import Galley.App +import Wire.API.Routes.API +import Wire.API.Routes.Public.Galley.CustomBackend + +customBackendAPI :: API CustomBackendAPI GalleyEffects +customBackendAPI = mkNamedAPI @"get-custom-backend-by-domain" getCustomBackendByDomain diff --git a/services/galley/src/Galley/API/Public/Feature.hs b/services/galley/src/Galley/API/Public/Feature.hs new file mode 100644 index 0000000000..2d4f06ea85 --- /dev/null +++ b/services/galley/src/Galley/API/Public/Feature.hs @@ -0,0 +1,76 @@ +-- 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.Public.Feature where + +import Galley.API.Teams +import Galley.API.Teams.Features +import Galley.App +import Galley.Cassandra.TeamFeatures +import Imports +import Wire.API.Routes.API +import Wire.API.Routes.Public.Galley.Feature +import Wire.API.Team.Feature + +featureAPI :: API FeatureAPI GalleyEffects +featureAPI = + mkNamedAPI @'("get", SSOConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", LegalholdConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("put", LegalholdConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", SearchVisibilityAvailableConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("put", SearchVisibilityAvailableConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get-deprecated", SearchVisibilityAvailableConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("put-deprecated", SearchVisibilityAvailableConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @"get-search-visibility" getSearchVisibility + <@> mkNamedAPI @"set-search-visibility" (setSearchVisibility @Cassandra (featureEnabledForTeam @Cassandra @SearchVisibilityAvailableConfig)) + <@> mkNamedAPI @'("get", ValidateSAMLEmailsConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get-deprecated", ValidateSAMLEmailsConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", DigitalSignaturesConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get-deprecated", DigitalSignaturesConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", AppLockConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("put", AppLockConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", FileSharingConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("put", FileSharingConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", ClassifiedDomainsConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", ConferenceCallingConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", SelfDeletingMessagesConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("put", SelfDeletingMessagesConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", GuestLinksConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("put", GuestLinksConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", SndFactorPasswordChallengeConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> 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) + <@> mkNamedAPI @"get-all-feature-configs-for-team" (getAllFeatureConfigsForTeam @Cassandra) + <@> mkNamedAPI @'("get-config", LegalholdConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get-config", SSOConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get-config", SearchVisibilityAvailableConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get-config", ValidateSAMLEmailsConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get-config", DigitalSignaturesConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get-config", AppLockConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get-config", FileSharingConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get-config", ClassifiedDomainsConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get-config", ConferenceCallingConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get-config", SelfDeletingMessagesConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get-config", GuestLinksConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get-config", SndFactorPasswordChallengeConfig) (getFeatureStatusForUser @Cassandra) + <@> mkNamedAPI @'("get-config", MLSConfig) (getFeatureStatusForUser @Cassandra) diff --git a/services/galley/src/Galley/API/Public/LegalHold.hs b/services/galley/src/Galley/API/Public/LegalHold.hs new file mode 100644 index 0000000000..21d658d217 --- /dev/null +++ b/services/galley/src/Galley/API/Public/LegalHold.hs @@ -0,0 +1,35 @@ +-- 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.Public.LegalHold where + +import Galley.API.LegalHold +import Galley.App +import Galley.Cassandra.TeamFeatures +import Wire.API.Routes.API +import Wire.API.Routes.Public.Galley.LegalHold + +legalHoldAPI :: API LegalHoldAPI GalleyEffects +legalHoldAPI = + mkNamedAPI @"create-legal-hold-settings" (createSettings @Cassandra) + <@> mkNamedAPI @"get-legal-hold-settings" (getSettings @Cassandra) + <@> mkNamedAPI @"delete-legal-hold-settings" (removeSettingsInternalPaging @Cassandra) + <@> mkNamedAPI @"get-legal-hold" getUserStatus + <@> mkNamedAPI @"consent-to-legal-hold" grantConsent + <@> mkNamedAPI @"request-legal-hold-device" (requestDevice @Cassandra) + <@> mkNamedAPI @"disable-legal-hold-for-user" disableForUser + <@> mkNamedAPI @"approve-legal-hold-device" (approveDevice @Cassandra) diff --git a/services/galley/src/Galley/API/Public/MLS.hs b/services/galley/src/Galley/API/Public/MLS.hs new file mode 100644 index 0000000000..93bd240b77 --- /dev/null +++ b/services/galley/src/Galley/API/Public/MLS.hs @@ -0,0 +1,31 @@ +-- 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.Public.MLS where + +import Galley.API.MLS +import Galley.App +import Wire.API.Routes.API +import Wire.API.Routes.Public.Galley.MLS + +mlsAPI :: API MLSAPI GalleyEffects +mlsAPI = + mkNamedAPI @"mls-welcome-message" postMLSWelcomeFromLocalUser + <@> mkNamedAPI @"mls-message-v1" postMLSMessageFromLocalUserV1 + <@> mkNamedAPI @"mls-message" postMLSMessageFromLocalUser + <@> mkNamedAPI @"mls-commit-bundle" postMLSCommitBundleFromLocalUser + <@> mkNamedAPI @"mls-public-keys" getMLSPublicKeys diff --git a/services/galley/src/Galley/API/Public/Messaging.hs b/services/galley/src/Galley/API/Public/Messaging.hs new file mode 100644 index 0000000000..806484ae90 --- /dev/null +++ b/services/galley/src/Galley/API/Public/Messaging.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 Galley.API.Public.Messaging where + +import Galley.API.Update +import Galley.App +import Wire.API.Routes.API +import Wire.API.Routes.Public.Galley.Messaging + +messagingAPI :: API MessagingAPI GalleyEffects +messagingAPI = + mkNamedAPI @"post-otr-message-unqualified" postOtrMessageUnqualified + <@> mkNamedAPI @"post-otr-broadcast-unqualified" postOtrBroadcastUnqualified + <@> mkNamedAPI @"post-proteus-message" postProteusMessage + <@> mkNamedAPI @"post-proteus-broadcast" postProteusBroadcast diff --git a/services/galley/src/Galley/API/Public/Servant.hs b/services/galley/src/Galley/API/Public/Servant.hs index 9e32a4aa93..e7eae6adde 100644 --- a/services/galley/src/Galley/API/Public/Servant.hs +++ b/services/galley/src/Galley/API/Public/Servant.hs @@ -17,180 +17,29 @@ module Galley.API.Public.Servant (mkNamedAPI, servantSitemap) where -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 -import Galley.API.Update +import Galley.API.Public.Bot +import Galley.API.Public.Conversation +import Galley.API.Public.CustomBackend +import Galley.API.Public.Feature +import Galley.API.Public.LegalHold +import Galley.API.Public.MLS +import Galley.API.Public.Messaging +import Galley.API.Public.Team +import Galley.API.Public.TeamConversation +import Galley.API.Public.TeamMember import Galley.App -import Galley.Cassandra.TeamFeatures -import Imports import Wire.API.Routes.API import Wire.API.Routes.Public.Galley -import Wire.API.Team.Feature servantSitemap :: API ServantAPI GalleyEffects servantSitemap = - conversations - <@> teamConversations - <@> messaging - <@> bot - <@> team - <@> features - <@> mls - <@> customBackend - <@> legalHold - <@> teamMember - 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 - <@> mkNamedAPI @"list-conversations-v1" listConversations - <@> mkNamedAPI @"list-conversations" listConversations - <@> mkNamedAPI @"get-conversation-by-reusable-code" (getConversationByReusableCode @Cassandra) - <@> mkNamedAPI @"create-group-conversation" createGroupConversation - <@> mkNamedAPI @"create-self-conversation" createSelfConversation - <@> mkNamedAPI @"create-one-to-one-conversation" createOne2OneConversation - <@> mkNamedAPI @"add-members-to-conversation-unqualified" addMembersUnqualified - <@> mkNamedAPI @"add-members-to-conversation-unqualified2" addMembersUnqualifiedV2 - <@> mkNamedAPI @"add-members-to-conversation" addMembers - <@> mkNamedAPI @"join-conversation-by-id-unqualified" (joinConversationById @Cassandra) - <@> mkNamedAPI @"join-conversation-by-code-unqualified" (joinConversationByReusableCode @Cassandra) - <@> mkNamedAPI @"code-check" (checkReusableCode @Cassandra) - <@> mkNamedAPI @"create-conversation-code-unqualified" (addCodeUnqualified @Cassandra) - <@> mkNamedAPI @"get-conversation-guest-links-status" (getConversationGuestLinksStatus @Cassandra) - <@> mkNamedAPI @"remove-code-unqualified" rmCodeUnqualified - <@> mkNamedAPI @"get-code" (getCode @Cassandra) - <@> mkNamedAPI @"member-typing-unqualified" isTypingUnqualified - <@> mkNamedAPI @"remove-member-unqualified" removeMemberUnqualified - <@> mkNamedAPI @"remove-member" removeMemberQualified - <@> mkNamedAPI @"update-other-member-unqualified" updateOtherMemberUnqualified - <@> mkNamedAPI @"update-other-member" updateOtherMember - <@> mkNamedAPI @"update-conversation-name-deprecated" updateUnqualifiedConversationName - <@> mkNamedAPI @"update-conversation-name-unqualified" updateUnqualifiedConversationName - <@> mkNamedAPI @"update-conversation-name" updateConversationName - <@> mkNamedAPI @"update-conversation-message-timer-unqualified" updateConversationMessageTimerUnqualified - <@> mkNamedAPI @"update-conversation-message-timer" updateConversationMessageTimer - <@> mkNamedAPI @"update-conversation-receipt-mode-unqualified" updateConversationReceiptModeUnqualified - <@> mkNamedAPI @"update-conversation-receipt-mode" updateConversationReceiptMode - <@> mkNamedAPI @"update-conversation-access-unqualified" updateConversationAccessUnqualified - <@> mkNamedAPI @"update-conversation-access" updateConversationAccess - <@> mkNamedAPI @"get-conversation-self-unqualified" getLocalSelf - <@> mkNamedAPI @"update-conversation-self-unqualified" updateUnqualifiedSelfMember - <@> mkNamedAPI @"update-conversation-self" updateSelfMember - - teamConversations :: API TeamConversationAPI GalleyEffects - teamConversations = - mkNamedAPI @"get-team-conversation-roles" getTeamConversationRoles - <@> mkNamedAPI @"get-team-conversations" getTeamConversations - <@> mkNamedAPI @"get-team-conversation" getTeamConversation - <@> mkNamedAPI @"delete-team-conversation" deleteTeamConversation - - messaging :: API MessagingAPI GalleyEffects - messaging = - mkNamedAPI @"post-otr-message-unqualified" postOtrMessageUnqualified - <@> mkNamedAPI @"post-otr-broadcast-unqualified" postOtrBroadcastUnqualified - <@> mkNamedAPI @"post-proteus-message" postProteusMessage - <@> mkNamedAPI @"post-proteus-broadcast" postProteusBroadcast - - bot :: API BotAPI GalleyEffects - bot = mkNamedAPI @"post-bot-message-unqualified" postBotMessageUnqualified - - team = - mkNamedAPI @"create-non-binding-team" createNonBindingTeamH - <@> mkNamedAPI @"update-team" updateTeamH - <@> mkNamedAPI @"get-teams" getManyTeams - <@> 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) - <@> mkNamedAPI @'("put", LegalholdConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get", SearchVisibilityAvailableConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("put", SearchVisibilityAvailableConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get-deprecated", SearchVisibilityAvailableConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("put-deprecated", SearchVisibilityAvailableConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @"get-search-visibility" getSearchVisibility - <@> mkNamedAPI @"set-search-visibility" (setSearchVisibility @Cassandra (featureEnabledForTeam @Cassandra @SearchVisibilityAvailableConfig)) - <@> mkNamedAPI @'("get", ValidateSAMLEmailsConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get-deprecated", ValidateSAMLEmailsConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get", DigitalSignaturesConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get-deprecated", DigitalSignaturesConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get", AppLockConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("put", AppLockConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get", FileSharingConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("put", FileSharingConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get", ClassifiedDomainsConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get", ConferenceCallingConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get", SelfDeletingMessagesConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("put", SelfDeletingMessagesConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get", GuestLinksConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("put", GuestLinksConfig) (setFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("get", SndFactorPasswordChallengeConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> 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) - <@> mkNamedAPI @"get-all-feature-configs-for-team" (getAllFeatureConfigsForTeam @Cassandra) - <@> mkNamedAPI @'("get-config", LegalholdConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPI @'("get-config", SSOConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPI @'("get-config", SearchVisibilityAvailableConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPI @'("get-config", ValidateSAMLEmailsConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPI @'("get-config", DigitalSignaturesConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPI @'("get-config", AppLockConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPI @'("get-config", FileSharingConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPI @'("get-config", ClassifiedDomainsConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPI @'("get-config", ConferenceCallingConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPI @'("get-config", SelfDeletingMessagesConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPI @'("get-config", GuestLinksConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPI @'("get-config", SndFactorPasswordChallengeConfig) (getFeatureStatusForUser @Cassandra) - <@> mkNamedAPI @'("get-config", MLSConfig) (getFeatureStatusForUser @Cassandra) - - mls :: API MLSAPI GalleyEffects - mls = - 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 - customBackend = mkNamedAPI @"get-custom-backend-by-domain" getCustomBackendByDomain - - legalHold :: API LegalHoldAPI GalleyEffects - legalHold = - mkNamedAPI @"create-legal-hold-settings" (createSettings @Cassandra) - <@> mkNamedAPI @"get-legal-hold-settings" (getSettings @Cassandra) - <@> mkNamedAPI @"delete-legal-hold-settings" (removeSettingsInternalPaging @Cassandra) - <@> mkNamedAPI @"get-legal-hold" getUserStatus - <@> mkNamedAPI @"consent-to-legal-hold" grantConsent - <@> mkNamedAPI @"request-legal-hold-device" (requestDevice @Cassandra) - <@> mkNamedAPI @"disable-legal-hold-for-user" disableForUser - <@> mkNamedAPI @"approve-legal-hold-device" (approveDevice @Cassandra) - - teamMember :: API TeamMemberAPI GalleyEffects - teamMember = - mkNamedAPI @"get-team-members" getTeamMembers - <@> mkNamedAPI @"get-team-member" getTeamMember - <@> mkNamedAPI @"get-team-members-by-ids" bulkGetTeamMembers - <@> mkNamedAPI @"add-team-member" (addTeamMember @Cassandra) - <@> mkNamedAPI @"delete-team-member" deleteTeamMember - <@> mkNamedAPI @"delete-non-binding-team-member" deleteNonBindingTeamMember - <@> mkNamedAPI @"update-team-member" updateTeamMember - <@> mkNamedAPI @"get-team-members-csv" getTeamMembersCSV + conversationAPI + <@> teamConversationAPI + <@> messagingAPI + <@> botAPI + <@> teamAPI + <@> featureAPI + <@> mlsAPI + <@> customBackendAPI + <@> legalHoldAPI + <@> teamMemberAPI diff --git a/services/galley/src/Galley/API/Public/Team.hs b/services/galley/src/Galley/API/Public/Team.hs new file mode 100644 index 0000000000..9cea78cea8 --- /dev/null +++ b/services/galley/src/Galley/API/Public/Team.hs @@ -0,0 +1,31 @@ +-- 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.Public.Team where + +import Galley.API.Teams +import Galley.App +import Wire.API.Routes.API +import Wire.API.Routes.Public.Galley.Team + +teamAPI :: API TeamAPI GalleyEffects +teamAPI = + mkNamedAPI @"create-non-binding-team" createNonBindingTeamH + <@> mkNamedAPI @"update-team" updateTeamH + <@> mkNamedAPI @"get-teams" getManyTeams + <@> mkNamedAPI @"get-team" getTeamH + <@> mkNamedAPI @"delete-team" deleteTeam diff --git a/services/galley/src/Galley/API/Public/TeamConversation.hs b/services/galley/src/Galley/API/Public/TeamConversation.hs new file mode 100644 index 0000000000..359c69f1db --- /dev/null +++ b/services/galley/src/Galley/API/Public/TeamConversation.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 Galley.API.Public.TeamConversation where + +import Galley.API.Teams +import Galley.App +import Wire.API.Routes.API +import Wire.API.Routes.Public.Galley.TeamConversation + +teamConversationAPI :: API TeamConversationAPI GalleyEffects +teamConversationAPI = + mkNamedAPI @"get-team-conversation-roles" getTeamConversationRoles + <@> mkNamedAPI @"get-team-conversations" getTeamConversations + <@> mkNamedAPI @"get-team-conversation" getTeamConversation + <@> mkNamedAPI @"delete-team-conversation" deleteTeamConversation diff --git a/services/galley/src/Galley/API/Public/TeamMember.hs b/services/galley/src/Galley/API/Public/TeamMember.hs new file mode 100644 index 0000000000..af7e761c66 --- /dev/null +++ b/services/galley/src/Galley/API/Public/TeamMember.hs @@ -0,0 +1,35 @@ +-- 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.Public.TeamMember where + +import Galley.API.Teams +import Galley.App +import Galley.Cassandra.TeamFeatures +import Wire.API.Routes.API +import Wire.API.Routes.Public.Galley.TeamMember + +teamMemberAPI :: API TeamMemberAPI GalleyEffects +teamMemberAPI = + mkNamedAPI @"get-team-members" getTeamMembers + <@> mkNamedAPI @"get-team-member" getTeamMember + <@> mkNamedAPI @"get-team-members-by-ids" bulkGetTeamMembers + <@> mkNamedAPI @"add-team-member" (addTeamMember @Cassandra) + <@> mkNamedAPI @"delete-team-member" deleteTeamMember + <@> mkNamedAPI @"delete-non-binding-team-member" deleteNonBindingTeamMember + <@> mkNamedAPI @"update-team-member" updateTeamMember + <@> mkNamedAPI @"get-team-members-csv" getTeamMembersCSV diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index 852ae13115..56bbd62661 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -1,19 +1,3 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2020 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 #-} -- This file is part of the Wire Server implementation. @@ -37,8 +21,10 @@ module Galley.API.Query ( getBotConversationH, getUnqualifiedConversation, getConversation, + getGlobalTeamConversation, getConversationRoles, conversationIdsPageFromUnqualified, + conversationIdsPageFromV2, conversationIdsPageFrom, getConversations, listConversations, @@ -50,6 +36,7 @@ module Galley.API.Query ensureGuestLinksEnabled, getConversationGuestLinksStatus, ensureConvAdmin, + getMLSSelfConversation, ) where @@ -65,7 +52,11 @@ import Data.Proxy import Data.Qualified import Data.Range import qualified Data.Set as Set +import Data.Tagged import Galley.API.Error +import Galley.API.MLS.Keys +import Galley.API.MLS.Types +import Galley.API.Mapping import qualified Galley.API.Mapping as Mapping import Galley.API.Util import qualified Galley.Data.Conversation as Data @@ -77,6 +68,8 @@ import qualified Galley.Effects.ListItems as E import qualified Galley.Effects.MemberStore as E import Galley.Effects.TeamFeatureStore (FeaturePersistentConstraint) import qualified Galley.Effects.TeamFeatureStore as TeamFeatures +import qualified Galley.Effects.TeamStore as E +import Galley.Env import Galley.Options import Galley.Types.Conversations.Members import Galley.Types.Teams @@ -90,10 +83,9 @@ import Polysemy.Error import Polysemy.Input import qualified Polysemy.TinyLog as P import qualified System.Logger.Class as Logger -import Wire.API.Conversation (Access (CodeAccess), Conversation, ConversationCoverView (..), ConversationList (ConversationList), ConversationMetadata, convHasMore, convList) +import Wire.API.Conversation hiding (Member) import qualified Wire.API.Conversation as Public import Wire.API.Conversation.Code -import Wire.API.Conversation.Member hiding (Member) import Wire.API.Conversation.Role import qualified Wire.API.Conversation.Role as Public import Wire.API.Error @@ -101,6 +93,7 @@ import Wire.API.Error.Galley import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error +import qualified Wire.API.MLS.GlobalTeamConversation as Public import qualified Wire.API.Provider.Bot as Public import qualified Wire.API.Routes.MultiTablePaging as Public import Wire.API.Team.Feature as Public hiding (setStatus) @@ -120,7 +113,11 @@ getBotConversation :: Local ConvId -> Sem r Public.BotConvView getBotConversation zbot lcnv = do - (c, _) <- getConversationAndMemberWithError @'ConvNotFound (botUserId zbot) lcnv + (c, _) <- + getConversationAndMemberWithError + @'ConvNotFound + (qUntagged . qualifyAs lcnv . botUserId $ zbot) + lcnv let domain = tDomain lcnv cmems = mapMaybe (mkMember domain) (toList (Data.convLocalMembers c)) pure $ Public.botConvView (tUnqualified lcnv) (Data.convName c) cmems @@ -148,6 +145,25 @@ getUnqualifiedConversation lusr cnv = do c <- getConversationAndCheckMembership (tUnqualified lusr) (qualifyAs lusr cnv) Mapping.conversationView lusr c +getGlobalTeamConversation :: + Members + '[ ConversationStore, + ErrorS 'NotATeamMember, + MemberStore, + TeamStore + ] + r => + Local UserId -> + TeamId -> + Sem r Public.GlobalTeamConversation +getGlobalTeamConversation lusr tid = do + let ltid = qualifyAs lusr tid + void $ noteS @'NotATeamMember =<< E.getTeamMember tid (tUnqualified lusr) + E.getGlobalTeamConversation ltid >>= \case + Nothing -> + E.createGlobalTeamConversation (qualifyAs lusr tid) + Just conv -> pure conv + getConversation :: forall r. Members @@ -287,12 +303,24 @@ getConversationRoles lusr cnv = do pure $ Public.ConversationRolesList wireConvRoles conversationIdsPageFromUnqualified :: - Member (ListItems LegacyPaging ConvId) r => + Members + [ ListItems LegacyPaging ConvId, + ConversationStore, + MemberStore, + TeamStore + ] + r => Local UserId -> Maybe ConvId -> Maybe (Range 1 1000 Int32) -> Sem r (Public.ConversationList ConvId) conversationIdsPageFromUnqualified lusr start msize = do + void $ + E.getUserTeams (tUnqualified lusr) >>= \tids -> + runError @InternalError $ + runError @(Tagged 'NotATeamMember ()) + (for_ tids $ \tid -> getGlobalTeamConversation lusr tid) + let size = fromMaybe (toRange (Proxy @1000)) msize ids <- E.listItems (tUnqualified lusr) start size pure $ @@ -308,15 +336,27 @@ conversationIdsPageFromUnqualified lusr start msize = do -- -- - After local conversations, remote conversations are listed ordered -- - lexicographically by their domain and then by their id. -conversationIdsPageFrom :: +-- +-- FUTUREWORK: Move the body of this function to 'conversationIdsPageFrom' once +-- support for V2 is dropped. +conversationIdsPageFromV2 :: forall p r. ( p ~ CassandraPaging, - Members '[ListItems p ConvId, ListItems p (Remote ConvId)] r + Members + '[ ConversationStore, + Error InternalError, + Input Env, + ListItems p ConvId, + ListItems p (Remote ConvId), + P.TinyLog + ] + r ) => + ListGlobalSelfConvs -> Local UserId -> Public.GetPaginatedConversationIds -> Sem r Public.ConvIdsPage -conversationIdsPageFrom lusr Public.GetMultiTablePageRequest {..} = do +conversationIdsPageFromV2 listGlobalSelf lusr Public.GetMultiTablePageRequest {..} = do let localDomain = tDomain lusr case gmtprState of Just (Public.ConversationPagingState Public.PagingRemotes stateBS) -> @@ -337,11 +377,17 @@ conversationIdsPageFrom lusr Public.GetMultiTablePageRequest {..} = do <$> E.listItems (tUnqualified lusr) pagingState size let remainingSize = fromRange size - fromIntegral (length (Public.mtpResults localPage)) if Public.mtpHasMore localPage || remainingSize <= 0 - then pure localPage {Public.mtpHasMore = True} -- We haven't checked the remotes yet, so has_more must always be True here. + then -- We haven't checked the remotes yet, so has_more must always be True here. + pure (filterOut localPage) {Public.mtpHasMore = True} else do -- remainingSize <= size and remainingSize >= 1, so it is safe to convert to Range remotePage <- remotesOnly Nothing (unsafeRange remainingSize) - pure $ remotePage {Public.mtpResults = Public.mtpResults localPage <> Public.mtpResults remotePage} + pure $ + remotePage + { Public.mtpResults = + Public.mtpResults (filterOut localPage) + <> Public.mtpResults remotePage + } remotesOnly :: Maybe C.PagingState -> @@ -360,6 +406,53 @@ conversationIdsPageFrom lusr Public.GetMultiTablePageRequest {..} = do mtpPagingState = Public.ConversationPagingState table (LBS.toStrict . C.unPagingState <$> pwsState) } + -- MLS self-conversation of this user + selfConvId = mlsSelfConvId (tUnqualified lusr) + isNotSelfConv = (/= selfConvId) . qUnqualified + + -- If this is an old client making a request (i.e., a V1 or V2 client), make + -- sure to filter out the MLS global team conversation and the MLS + -- self-conversation. + -- + -- FUTUREWORK: This is yet to be implemented for global team conversations. + filterOut :: ConvIdsPage -> ConvIdsPage + filterOut page | listGlobalSelf == ListGlobalSelf = page + filterOut page = + page + { Public.mtpResults = filter isNotSelfConv $ Public.mtpResults page + } + +-- | Lists conversation ids for the logged in user in a paginated way. +-- +-- Pagination requires an order, in this case the order is defined as: +-- +-- - First all the local conversations are listed ordered by their id +-- +-- - After local conversations, remote conversations are listed ordered +-- - lexicographically by their domain and then by their id. +conversationIdsPageFrom :: + forall p r. + ( p ~ CassandraPaging, + Members + '[ ConversationStore, + Error InternalError, + Input Env, + ListItems p ConvId, + ListItems p (Remote ConvId), + P.TinyLog + ] + r + ) => + Local UserId -> + Public.GetPaginatedConversationIds -> + Sem r Public.ConvIdsPage +conversationIdsPageFrom lusr state = do + -- NOTE: Getting the MLS self-conversation creates it in case it does not + -- exist yet. This is to ensure it is automatically listed without needing to + -- create it separately. + void $ getMLSSelfConversation lusr + conversationIdsPageFromV2 ListGlobalSelf lusr state + getConversations :: Members '[Error InternalError, ListItems LegacyPaging ConvId, ConversationStore, P.TinyLog] r => Local UserId -> @@ -606,6 +699,39 @@ getConversationGuestLinksFeatureStatus mbTid = do mbLockStatus <- TeamFeatures.getFeatureLockStatus @db (Proxy @GuestLinksConfig) tid pure $ computeFeatureConfigForTeamUser mbConfigNoLock mbLockStatus defaultStatus +-- | Get an MLS self conversation. In case it does not exist, it is partially +-- created in the database. The part that is not written is the epoch number; +-- the number is inserted only upon the first commit. With this we avoid race +-- conditions where two clients concurrently try to create or update the self +-- conversation, where the only thing that can be updated is bumping the epoch +-- number. +getMLSSelfConversation :: + forall r. + Members + '[ ConversationStore, + Error InternalError, + Input Env, + P.TinyLog + ] + r => + Local UserId -> + Sem r Conversation +getMLSSelfConversation lusr = do + let selfConvId = mlsSelfConvId usr + mconv <- E.getConversation selfConvId + cnv <- maybe create pure mconv + conversationView lusr cnv + where + usr = tUnqualified lusr + create :: Sem r Data.Conversation + create = do + unlessM (isJust <$> getMLSRemovalKey) $ + throw (InternalErrorWithDescription noKeyMsg) + E.createMLSSelfConversation lusr + noKeyMsg = + "No backend removal key is configured (See 'mlsPrivateKeyPaths'" + <> "in galley's config). Refusing to create MLS conversation." + ------------------------------------------------------------------------------- -- Helpers diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index 32caec47d1..2a48570e0c 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -50,6 +50,7 @@ module Galley.API.Teams uncheckedGetTeamMember, uncheckedGetTeamMembersH, uncheckedDeleteTeamMember, + uncheckedUpdateTeamMember, userIsTeamOwner, canUserJoinTeam, ensureNotTooLargeForLegalHold, @@ -135,14 +136,14 @@ import Wire.API.Federation.Error import qualified Wire.API.Message as Conv import qualified Wire.API.Notification as Public import Wire.API.Routes.MultiTablePaging (MultiTablePage (MultiTablePage), MultiTablePagingState (mtpsState)) -import Wire.API.Routes.Public.Galley +import Wire.API.Routes.Public.Galley.TeamMember import Wire.API.Team import qualified Wire.API.Team as Public import Wire.API.Team.Conversation import qualified Wire.API.Team.Conversation as Public import Wire.API.Team.Export (TeamExportUser (..)) import Wire.API.Team.Feature -import Wire.API.Team.Member (HardTruncationLimit, ListType (ListComplete, ListTruncated), NewTeamMember, TeamMember, TeamMemberList, TeamMemberListOptPerms, TeamMemberOptPerms, TeamMembersPage (..), TeamMembersPagingState, hardTruncationLimit, invitation, nPermissions, nUserId, newTeamMemberList, ntmNewTeamMember, permissions, setOptionalPerms, setOptionalPermsMany, teamMemberListType, teamMemberPagingState, teamMembers, tmdAuthPassword, userId) +import Wire.API.Team.Member import qualified Wire.API.Team.Member as Public import Wire.API.Team.Permission (Perm (..), Permissions (..), SPerm (..), copy, fullPermissions, self) import Wire.API.Team.Role @@ -219,21 +220,25 @@ lookupTeam zusr tid = do else pure Nothing createNonBindingTeamH :: - forall r. - ( Member BrigAccess r, - Member (ErrorS 'UserBindingExists) r, - Member (ErrorS 'NotConnected) r, - Member GundeckAccess r, - Member (Input UTCTime) r, - Member P.TinyLog r, - Member TeamStore r, - Member WaiRoutes r - ) => - UserId -> + Members + '[ ConversationStore, + ErrorS 'NotConnected, + ErrorS 'UserBindingExists, + GundeckAccess, + Input UTCTime, + MemberStore, + P.TinyLog, + TeamStore, + WaiRoutes, + BrigAccess + ] + r => + Local UserId -> ConnId -> Public.NonBindingNewTeam -> Sem r TeamId -createNonBindingTeamH zusr zcon (Public.NonBindingNewTeam body) = do +createNonBindingTeamH lusr zcon (Public.NonBindingNewTeam body) = do + let zusr = tUnqualified lusr let owner = Public.mkTeamMember zusr fullPermissions Nothing LH.defUserLegalHoldStatus let others = filter ((zusr /=) . view userId) @@ -254,15 +259,23 @@ createNonBindingTeamH zusr zcon (Public.NonBindingNewTeam body) = do (body ^. newTeamIconKey) NonBinding finishCreateTeam team owner others (Just zcon) - pure (team ^. teamId) + pure $ team ^. teamId createBindingTeam :: - Members '[GundeckAccess, Input UTCTime, TeamStore] r => + Members + '[ GundeckAccess, + Input UTCTime, + MemberStore, + TeamStore, + ConversationStore + ] + r => TeamId -> - UserId -> + Local UserId -> BindingNewTeam -> Sem r TeamId -createBindingTeam tid zusr (BindingNewTeam body) = do +createBindingTeam tid lusr (BindingNewTeam body) = do + let zusr = tUnqualified lusr let owner = Public.mkTeamMember zusr fullPermissions Nothing LH.defUserLegalHoldStatus team <- E.createTeam (Just tid) zusr (body ^. newTeamName) (body ^. newTeamIcon) (body ^. newTeamIconKey) Binding @@ -766,7 +779,7 @@ uncheckedAddTeamMember tid nmem = do billingUserIds <- Journal.getBillingUserIds tid $ Just $ newTeamMemberList (ntmNewTeamMember nmem : mems ^. teamMembers) (mems ^. teamMemberListType) Journal.teamUpdate tid (sizeBeforeAdd + 1) billingUserIds -updateTeamMember :: +uncheckedUpdateTeamMember :: forall r. Members '[ BrigAccess, @@ -783,13 +796,13 @@ updateTeamMember :: TeamStore ] r => - Local UserId -> - ConnId -> + Maybe (Local UserId) -> + Maybe ConnId -> TeamId -> NewTeamMember -> Sem r () -updateTeamMember lzusr zcon tid newMember = do - let zusr = tUnqualified lzusr +uncheckedUpdateTeamMember mlzusr mZcon tid newMember = do + let mZusr = tUnqualified <$> mlzusr let targetMember = ntmNewTeamMember newMember let targetId = targetMember ^. userId targetPermissions = targetMember ^. permissions @@ -797,36 +810,18 @@ updateTeamMember lzusr zcon tid newMember = do Log.field "targets" (toByteString targetId) . Log.field "action" (Log.val "Teams.updateTeamMember") - -- get the team and verify permissions team <- fmap tdTeam $ E.getTeam tid >>= noteS @'TeamNotFound - user <- - E.getTeamMember tid zusr - >>= permissionCheck SetMemberPermissions - -- user may not elevate permissions - targetPermissions `ensureNotElevated` user previousMember <- E.getTeamMember tid targetId >>= noteS @'TeamMemberNotFound - when - ( downgradesOwner previousMember targetPermissions - && not (canDowngradeOwner user previousMember) - ) - $ throwS @'AccessDenied -- update target in Cassandra E.setTeamMemberPermissions (previousMember ^. permissions) tid targetId targetPermissions updatedMembers <- getTeamMembersForFanout tid updateJournal team updatedMembers - updatePeers zusr targetId targetMember targetPermissions updatedMembers + updatePeers mZusr targetId targetMember targetPermissions updatedMembers where - canDowngradeOwner = canDeleteMember - - downgradesOwner :: TeamMember -> Permissions -> Bool - downgradesOwner previousMember targetPermissions = - permissionsRole (previousMember ^. permissions) == Just RoleOwner - && permissionsRole targetPermissions /= Just RoleOwner - updateJournal :: Team -> TeamMemberList -> Sem r () updateJournal team mems = do when (team ^. teamBinding == Binding) $ do @@ -834,7 +829,7 @@ updateTeamMember lzusr zcon tid newMember = do billingUserIds <- Journal.getBillingUserIds tid $ Just mems Journal.teamUpdate tid size billingUserIds - updatePeers :: UserId -> UserId -> TeamMember -> Permissions -> TeamMemberList -> Sem r () + updatePeers :: Maybe UserId -> UserId -> TeamMember -> Permissions -> TeamMemberList -> Sem r () updatePeers zusr targetId targetMember targetPermissions updatedMembers = do -- inform members of the team about the change -- some (privileged) users will be informed about which change was applied @@ -845,8 +840,63 @@ updateTeamMember lzusr zcon tid newMember = do now <- input let ePriv = newEvent tid now privilegedUpdate -- push to all members (user is privileged) - let pushPriv = newPushLocal (updatedMembers ^. teamMemberListType) zusr (TeamEvent ePriv) $ privilegedRecipients - for_ pushPriv $ \p -> E.push1 $ p & pushConn ?~ zcon + let pushPriv = newPush (updatedMembers ^. teamMemberListType) zusr (TeamEvent ePriv) $ privilegedRecipients + for_ pushPriv (\p -> E.push1 (p & pushConn .~ mZcon)) + +updateTeamMember :: + forall r. + Members + '[ BrigAccess, + ErrorS 'AccessDenied, + ErrorS 'InvalidPermissions, + ErrorS 'TeamNotFound, + ErrorS 'TeamMemberNotFound, + ErrorS 'NotATeamMember, + ErrorS OperationDenied, + GundeckAccess, + Input Opts, + Input UTCTime, + P.TinyLog, + TeamStore + ] + r => + Local UserId -> + ConnId -> + TeamId -> + NewTeamMember -> + Sem r () +updateTeamMember lzusr zcon tid newMember = do + let zusr = tUnqualified lzusr + let targetMember = ntmNewTeamMember newMember + let targetId = targetMember ^. userId + targetPermissions = targetMember ^. permissions + P.debug $ + Log.field "targets" (toByteString targetId) + . Log.field "action" (Log.val "Teams.updateTeamMember") + + -- get the team and verify permissions + user <- + E.getTeamMember tid zusr + >>= permissionCheck SetMemberPermissions + + -- user may not elevate permissions + targetPermissions `ensureNotElevated` user + previousMember <- + E.getTeamMember tid targetId >>= noteS @'TeamMemberNotFound + when + ( downgradesOwner previousMember targetPermissions + && not (canDowngradeOwner user previousMember) + ) + $ throwS @'AccessDenied + + uncheckedUpdateTeamMember (Just lzusr) (Just zcon) tid newMember + where + canDowngradeOwner = canDeleteMember + + downgradesOwner :: TeamMember -> Permissions -> Bool + downgradesOwner previousMember targetPermissions = + permissionsRole (previousMember ^. permissions) == Just RoleOwner + && permissionsRole targetPermissions /= Just RoleOwner deleteTeamMember :: Members diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index 0ee2281513..9cb4a81096 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -128,7 +128,7 @@ import Wire.API.Federation.API.Galley import Wire.API.Federation.Error import Wire.API.Message import Wire.API.Provider.Service (ServiceRef) -import Wire.API.Routes.Public.Galley +import Wire.API.Routes.Public.Galley.Messaging import Wire.API.Routes.Public.Util (UpdateResult (..)) import Wire.API.ServantProto (RawProto (..)) import Wire.API.Team.Feature hiding (setStatus) @@ -1571,6 +1571,8 @@ addBot lusr zcon b = do unless (tUnqualified lusr `isMember` users) $ throwS @'ConvNotFound ensureGroupConversation c self <- getSelfMemberFromLocals (tUnqualified lusr) users + -- Note that in brig from where this internal handler is called, we additionally check for conversation admin role. + -- Remember to change this if we ever want to allow non admins to add bots. ensureActionAllowed SAddConversationMember self unless (any ((== b ^. addBotId) . botMemId) bots) $ do let botId = qualifyAs lusr (botUserId (b ^. addBotId)) @@ -1587,7 +1589,8 @@ rmBotH :: Input (Local ()), Input UTCTime, MemberStore, - WaiRoutes + WaiRoutes, + ErrorS ('ActionDenied 'RemoveConversationMember) ] r => UserId ::: Maybe ConnId ::: JsonRequest RemoveBot -> @@ -1605,7 +1608,8 @@ rmBot :: ExternalAccess, GundeckAccess, Input UTCTime, - MemberStore + MemberStore, + ErrorS ('ActionDenied 'RemoveConversationMember) ] r => Local UserId -> @@ -1615,10 +1619,17 @@ rmBot :: rmBot lusr zcon b = do c <- E.getConversation (b ^. rmBotConv) >>= noteS @'ConvNotFound - let lcnv = qualifyAs lusr (Data.convId c) + let (bots, users) = localBotsAndUsers (Data.convLocalMembers c) unless (tUnqualified lusr `isMember` Data.convLocalMembers c) $ throwS @'ConvNotFound - let (bots, users) = localBotsAndUsers (Data.convLocalMembers c) + -- A bot can remove itself (which will internally be triggered when a service is deleted), + -- otherwise we have to check for the correct permissions + unless (botUserId (b ^. rmBotId) == tUnqualified lusr) $ do + -- Note that in brig from where this internal handler is called, we additionally check for conversation admin role. + -- Remember to change this if we ever want to allow non admins to remove bots. + self <- getSelfMemberFromLocals (tUnqualified lusr) users + ensureActionAllowed SRemoveConversationMember self + let lcnv = qualifyAs lusr (Data.convId c) if not (any ((== b ^. rmBotId) . botMemId) bots) then pure Unchanged else do diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index 24891e22c6..4234d409e5 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -36,6 +36,8 @@ import Data.Singletons import qualified Data.Text as T import Data.Time import Galley.API.Error +import Galley.API.MLS.Util +import Galley.API.Mapping import qualified Galley.Data.Conversation as Data import Galley.Data.Services (BotMember, newBotMember) import qualified Galley.Data.Types as DataTypes @@ -63,6 +65,7 @@ import qualified Network.Wai.Utilities as Wai import Polysemy import Polysemy.Error import Polysemy.Input +import qualified Polysemy.TinyLog as P import Wire.API.Connection import Wire.API.Conversation hiding (Member) import qualified Wire.API.Conversation as Public @@ -74,6 +77,8 @@ import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error +import Wire.API.Routes.Public.Galley.Conversation +import Wire.API.Routes.Public.Util import Wire.API.Team.Member import Wire.API.Team.Role import Wire.API.User (VerificationAction) @@ -188,7 +193,7 @@ ensureActionAllowed action self = case isActionAllowed (fromSing action) (convMe ensureGroupConversation :: Member (ErrorS 'InvalidOperation) r => Data.Conversation -> Sem r () ensureGroupConversation conv = do let ty = Data.convType conv - when (ty /= RegularConv) $ throwS @'InvalidOperation + unless (ty `elem` [RegularConv, GlobalTeamConv]) $ throwS @'InvalidOperation -- | Ensure that the set of actions provided are not "greater" than the user's -- own. This is used to ensure users cannot "elevate" allowed actions @@ -504,7 +509,7 @@ getConversationAndCheckMembership uid lcnv = do (conv, _) <- getConversationAndMemberWithError @'ConvAccessDenied - uid + (qUntagged $ qualifyAs lcnv uid) lcnv pure conv @@ -513,18 +518,27 @@ getConversationWithError :: Member (ErrorS 'ConvNotFound) r ) => Local ConvId -> + UserId -> Sem r Data.Conversation -getConversationWithError lcnv = - getConversation (tUnqualified lcnv) >>= noteS @'ConvNotFound +getConversationWithError lcnv uid = + let cid = tUnqualified lcnv + in getConversation cid >>= \case + Just c -> pure c + Nothing -> do + gtc <- noteS @'ConvNotFound =<< getGlobalTeamConversationById lcnv + pure $ gtcToConv gtc uid mempty getConversationAndMemberWithError :: forall e uid mem r. - (Members '[ConversationStore, ErrorS 'ConvNotFound, ErrorS e] r, IsConvMemberId uid mem) => + ( Members '[ConversationStore, ErrorS 'ConvNotFound, ErrorS e] r, + IsConvMemberId uid mem, + uid ~ Qualified UserId + ) => uid -> Local ConvId -> Sem r (Data.Conversation, mem) getConversationAndMemberWithError usr lcnv = do - c <- getConversationWithError lcnv + c <- getConversationWithError lcnv (qUnqualified usr) member <- noteS @e $ getConvMember lcnv c usr pure (c, member) @@ -838,6 +852,13 @@ ensureMemberLimit old new = do when (length old + length new > maxSize) $ throwS @'TooManyMembers +conversationExisted :: + Members '[Error InternalError, P.TinyLog] r => + Local UserId -> + Data.Conversation -> + Sem r ConversationResponse +conversationExisted lusr cnv = Existed <$> conversationView lusr cnv + -------------------------------------------------------------------------------- -- Handling remote errors diff --git a/services/galley/src/Galley/Cassandra.hs b/services/galley/src/Galley/Cassandra.hs index f01b95a4f3..ea4d501ef6 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 = 75 +schemaVersion = 77 diff --git a/services/galley/src/Galley/Cassandra/Access.hs b/services/galley/src/Galley/Cassandra/Access.hs index 05c566bfd1..9357320d95 100644 --- a/services/galley/src/Galley/Cassandra/Access.hs +++ b/services/galley/src/Galley/Cassandra/Access.hs @@ -31,6 +31,7 @@ defAccess SelfConv (Just (Set [])) = [PrivateAccess] defAccess ConnectConv (Just (Set [])) = [PrivateAccess] defAccess One2OneConv (Just (Set [])) = [PrivateAccess] defAccess RegularConv (Just (Set [])) = defRegularConvAccess +defAccess GlobalTeamConv s = maybe [SelfInviteAccess] fromSet s defAccess _ (Just (Set (x : xs))) = x : xs privateOnly :: Set Access diff --git a/services/galley/src/Galley/Cassandra/Client.hs b/services/galley/src/Galley/Cassandra/Client.hs index 25fb2a44d2..2b7f1c4d9a 100644 --- a/services/galley/src/Galley/Cassandra/Client.hs +++ b/services/galley/src/Galley/Cassandra/Client.hs @@ -40,9 +40,10 @@ import Polysemy.Input import qualified UnliftIO updateClient :: Bool -> UserId -> ClientId -> Client () -updateClient add usr cls = do +updateClient add usr cid = do + -- add or remove client let q = if add then Cql.addMemberClient else Cql.rmMemberClient - retry x5 $ write (q cls) (params LocalQuorum (Identity usr)) + retry x5 $ write (q cid) (params LocalQuorum (Identity usr)) -- Do, at most, 16 parallel lookups of up to 128 users each lookupClients :: [UserId] -> Client Clients diff --git a/services/galley/src/Galley/Cassandra/Conversation.hs b/services/galley/src/Galley/Cassandra/Conversation.hs index 81f25951d4..de72cc1264 100644 --- a/services/galley/src/Galley/Cassandra/Conversation.hs +++ b/services/galley/src/Galley/Cassandra/Conversation.hs @@ -19,6 +19,7 @@ module Galley.Cassandra.Conversation ( createConversation, deleteConversation, interpretConversationStoreToCassandra, + getGlobalTeamConversationById, ) where @@ -43,6 +44,7 @@ import Galley.Data.Conversation import Galley.Data.Conversation.Types import Galley.Effects.ConversationStore (ConversationStore (..)) import Galley.Types.Conversations.Members +import Galley.Types.ToUserRole import Galley.Types.UserList import Imports import Polysemy @@ -53,9 +55,67 @@ import qualified UnliftIO import Wire.API.Conversation hiding (Conversation, Member) import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite +import Wire.API.MLS.GlobalTeamConversation import Wire.API.MLS.Group import Wire.API.MLS.PublicGroupState +createMLSSelfConversation :: + Local UserId -> + Client Conversation +createMLSSelfConversation lusr = do + let cnv = mlsSelfConvId . tUnqualified $ lusr + usr = tUnqualified lusr + nc = + NewConversation + { ncMetadata = + (defConversationMetadata usr) {cnvmType = SelfConv}, + ncUsers = ulFromLocals [toUserRole usr], + ncProtocol = ProtocolMLSTag + } + meta = ncMetadata nc + gid = convToGroupId . qualifyAs lusr $ cnv + -- FUTUREWORK: Stop hard-coding the cipher suite + -- + -- 'CipherSuite 1' corresponds to + -- 'MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519'. + cs = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + proto = + ProtocolMLS + ConversationMLSData + { cnvmlsGroupId = gid, + cnvmlsEpoch = Epoch 0, + cnvmlsCipherSuite = cs + } + retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + addPrepQuery + Cql.insertMLSSelfConv + ( cnv, + cnvmType meta, + cnvmCreator meta, + Cql.Set (cnvmAccess meta), + Cql.Set (toList (cnvmAccessRoles meta)), + cnvmName meta, + cnvmTeam meta, + cnvmMessageTimer meta, + cnvmReceiptMode meta, + Just gid, + Just cs + ) + addPrepQuery Cql.insertGroupId (gid, cnv, tDomain lusr) + + (lmems, rmems) <- addMembers cnv (ncUsers nc) + pure + Conversation + { convId = cnv, + convLocalMembers = lmems, + convRemoteMembers = rmems, + convDeleted = False, + convMetadata = meta, + convProtocol = proto + } + createConversation :: Local ConvId -> NewConversation -> Client Conversation createConversation lcnv nc = do let meta = ncMetadata nc @@ -129,7 +189,8 @@ conversationMeta conv = (toConvMeta =<<) <$> retry x1 (query1 Cql.selectConv (params LocalQuorum (Identity conv))) where - toConvMeta (t, c, a, r, r', n, i, _, mt, rm, _, _, _, _) = do + toConvMeta (t, mc, a, r, r', n, i, _, mt, rm, _, _, _, _) = do + c <- mc let mbAccessRolesV2 = Set.fromList . Cql.fromSet <$> r' accessRoles = maybeRole t $ parseAccessRoles r mbAccessRolesV2 pure $ ConversationMetadata t c (defAccess t a) accessRoles n i mt rm @@ -191,6 +252,70 @@ getConversation conv = do <*> UnliftIO.wait cdata runMaybeT $ conversationGC =<< maybe mzero pure mbConv +getGlobalTeamConversation :: + Local TeamId -> + Client (Maybe GlobalTeamConversation) +getGlobalTeamConversation qtid = + let cid = qualifyAs qtid (globalTeamConv (tUnqualified qtid)) + in getGlobalTeamConversationById cid + +getGlobalTeamConversationById :: + Local ConvId -> + Client (Maybe GlobalTeamConversation) +getGlobalTeamConversationById lconv = do + let cid = tUnqualified lconv + mconv <- retry x1 (query1 Cql.selectGlobalTeamConv (params LocalQuorum (Identity cid))) + pure $ toGlobalConv mconv + where + toGlobalConv mconv = do + (muid, mname, mtid, mgid, mepoch, mcs) <- mconv + tid <- mtid + name <- mname + mlsData <- ConversationMLSData <$> mgid <*> (mepoch <|> Just (Epoch 0)) <*> mcs + + pure $ + GlobalTeamConversation + (qUntagged lconv) + mlsData + muid + [SelfInviteAccess] + name + tid + +createGlobalTeamConversation :: + Local TeamId -> + Client GlobalTeamConversation +createGlobalTeamConversation tid = do + let lconv = qualifyAs tid (globalTeamConv $ tUnqualified tid) + gid = convToGroupId lconv + cs = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + addPrepQuery + Cql.insertGlobalTeamConv + ( tUnqualified lconv, + Cql.Set [SelfInviteAccess], + "Global team conversation", + tUnqualified tid, + Just gid, + Just cs + ) + addPrepQuery Cql.insertTeamConv (tUnqualified tid, tUnqualified lconv) + addPrepQuery Cql.insertGroupId (gid, tUnqualified lconv, tDomain lconv) + pure $ + GlobalTeamConversation + (qUntagged lconv) + ( ConversationMLSData + gid + (Epoch 0) + cs + ) + Nothing + [SelfInviteAccess] + "Global team conversation" + (tUnqualified tid) + -- | "Garbage collect" a 'Conversation', i.e. if the conversation is -- marked as deleted, actually remove it from the database and return -- 'Nothing'. @@ -266,16 +391,23 @@ toProtocol :: toProtocol Nothing _ _ _ = Just ProtocolProteus toProtocol (Just ProtocolProteusTag) _ _ _ = Just ProtocolProteus toProtocol (Just ProtocolMLSTag) mgid mepoch mcs = - ProtocolMLS <$> (ConversationMLSData <$> mgid <*> mepoch <*> mcs) + ProtocolMLS + <$> ( ConversationMLSData + <$> mgid + -- If there is no epoch in the database, assume the epoch is 0 + <*> (mepoch <|> Just (Epoch 0)) + <*> mcs + ) toConv :: ConvId -> [LocalMember] -> [RemoteMember] -> - Maybe (ConvType, UserId, Maybe (Cql.Set Access), Maybe AccessRoleLegacy, Maybe (Cql.Set AccessRoleV2), Maybe Text, Maybe TeamId, Maybe Bool, Maybe Milliseconds, Maybe ReceiptMode, Maybe ProtocolTag, Maybe GroupId, Maybe Epoch, Maybe CipherSuiteTag) -> + Maybe (ConvType, Maybe UserId, Maybe (Cql.Set Access), Maybe AccessRoleLegacy, Maybe (Cql.Set AccessRoleV2), Maybe Text, Maybe TeamId, Maybe Bool, Maybe Milliseconds, Maybe ReceiptMode, Maybe ProtocolTag, Maybe GroupId, Maybe Epoch, Maybe CipherSuiteTag) -> Maybe Conversation toConv cid ms remoteMems mconv = do - (cty, uid, acc, role, roleV2, nme, ti, del, timer, rm, ptag, mgid, mep, mcs) <- mconv + (cty, muid, acc, role, roleV2, nme, ti, del, timer, rm, ptag, mgid, mep, mcs) <- mconv + uid <- muid let mbAccessRolesV2 = Set.fromList . Cql.fromSet <$> roleV2 accessRoles = maybeRole cty $ parseAccessRoles role mbAccessRolesV2 proto <- toProtocol ptag mgid mep mcs @@ -314,7 +446,11 @@ interpretConversationStoreToCassandra :: interpretConversationStoreToCassandra = interpret $ \case CreateConversationId -> Id <$> embed nextRandom CreateConversation loc nc -> embedClient $ createConversation loc nc + CreateMLSSelfConversation lusr -> embedClient $ createMLSSelfConversation lusr GetConversation cid -> embedClient $ getConversation cid + GetGlobalTeamConversation tid -> embedClient $ getGlobalTeamConversation tid + GetGlobalTeamConversationById lconv -> embedClient $ getGlobalTeamConversationById lconv + CreateGlobalTeamConversation tid -> embedClient $ createGlobalTeamConversation tid GetConversationIdByGroupId gId -> embedClient $ lookupGroupId gId GetConversations cids -> localConversations cids GetConversationMetadata cid -> embedClient $ conversationMeta cid diff --git a/services/galley/src/Galley/Cassandra/Conversation/Members.hs b/services/galley/src/Galley/Cassandra/Conversation/Members.hs index a4d4622e8c..85b37b634e 100644 --- a/services/galley/src/Galley/Cassandra/Conversation/Members.hs +++ b/services/galley/src/Galley/Cassandra/Conversation/Members.hs @@ -45,7 +45,7 @@ import Imports hiding (Set) import Polysemy import Polysemy.Input import qualified UnliftIO -import Wire.API.Conversation.Member hiding (Member) +import Wire.API.Conversation import Wire.API.Conversation.Role import Wire.API.MLS.KeyPackage import Wire.API.Provider.Service @@ -117,9 +117,32 @@ removeRemoteMembersFromLocalConv cnv victims = do addPrepQuery Cql.removeRemoteMember (cnv, domain, uid) members :: ConvId -> Client [LocalMember] -members conv = - fmap (mapMaybe toMember) . retry x1 $ - query Cql.selectMembers (params LocalQuorum (Identity conv)) +members conv = do + mconv <- retry x1 $ query1 Cql.selectConv (params LocalQuorum (Identity conv)) + case mconv of + Just (GlobalTeamConv, _, _, _, _, _, Just tid, _, _, _, _, _, _, _) -> do + res <- + retry x1 $ + query + Cql.selectTeamMembers + (params LocalQuorum (Identity tid)) + let uids = mapMaybe fst' $ res + pure $ mapMaybe toMemberFromId uids + _ -> + fmap (mapMaybe toMember) . retry x1 $ + query Cql.selectMembers (params LocalQuorum (Identity conv)) + where + fst' (a, _, _, _, _) = Just a + +toMemberFromId :: UserId -> Maybe LocalMember +toMemberFromId usr = + Just $ + LocalMember + { lmId = usr, + lmService = Nothing, + lmStatus = toMemberStatus (Nothing, Nothing, Nothing, Nothing, Nothing, Nothing), + lmConvRoleName = roleNameWireMember + } toMemberStatus :: ( -- otr muted @@ -202,9 +225,15 @@ member :: ConvId -> UserId -> Client (Maybe LocalMember) -member cnv usr = - (toMember =<<) - <$> retry x1 (query1 Cql.selectMember (params LocalQuorum (cnv, usr))) +member conv usr = do + mconv <- retry x1 $ query1 Cql.selectConv (params LocalQuorum (Identity conv)) + case mconv of + Just (GlobalTeamConv, _, _, _, _, _, _, _, _, _, _, _, _, _) -> + pure $ toMemberFromId usr + _ -> do + fmap (toMember =<<) $ + retry x1 $ + query1 Cql.selectMember (params LocalQuorum (conv, usr)) -- | Set local users as belonging to a remote conversation. This is invoked by a -- remote galley when users from the current backend are added to conversations @@ -341,26 +370,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, KeyPackageRef) -> Client () -addMLSClients lcnv (Qualified usr domain) cs = retry x5 . batch $ do +addMLSClients :: GroupId -> Qualified UserId -> Set.Set (ClientId, KeyPackageRef) -> Client () +addMLSClients groupId (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) + addPrepQuery Cql.addMLSClient (groupId, domain, usr, c, kpr) -removeMLSClients :: Local ConvId -> Qualified UserId -> Set.Set ClientId -> Client () -removeMLSClients lcnv (Qualified usr domain) cs = retry x5 . batch $ do +removeMLSClients :: GroupId -> Qualified UserId -> Set.Set ClientId -> Client () +removeMLSClients groupId (Qualified usr domain) cs = retry x5 . batch $ do setType BatchLogged setConsistency LocalQuorum for_ cs $ \c -> - addPrepQuery Cql.removeMLSClient (tUnqualified lcnv, domain, usr, c) + addPrepQuery Cql.removeMLSClient (groupId, domain, usr, c) -lookupMLSClients :: Local ConvId -> Client ClientMap -lookupMLSClients lcnv = +lookupMLSClients :: GroupId -> Client ClientMap +lookupMLSClients groupId = mkClientMap <$> retry x5 - (query Cql.lookupMLSClients (params LocalQuorum (Identity (tUnqualified lcnv)))) + (query Cql.lookupMLSClients (params LocalQuorum (Identity groupId))) interpretMemberStoreToCassandra :: Members '[Embed IO, Input ClientState] r => diff --git a/services/galley/src/Galley/Cassandra/Instances.hs b/services/galley/src/Galley/Cassandra/Instances.hs index 4860fdd3e1..e9e9764561 100644 --- a/services/galley/src/Galley/Cassandra/Instances.hs +++ b/services/galley/src/Galley/Cassandra/Instances.hs @@ -56,12 +56,14 @@ instance Cql ConvType where toCql SelfConv = CqlInt 1 toCql One2OneConv = CqlInt 2 toCql ConnectConv = CqlInt 3 + toCql GlobalTeamConv = CqlInt 4 fromCql (CqlInt i) = case i of 0 -> pure RegularConv 1 -> pure SelfConv 2 -> pure One2OneConv 3 -> pure ConnectConv + 4 -> pure GlobalTeamConv n -> Left $ "unexpected conversation-type: " ++ show n fromCql _ = Left "conv-type: int expected" @@ -72,12 +74,14 @@ instance Cql Access where toCql InviteAccess = CqlInt 2 toCql LinkAccess = CqlInt 3 toCql CodeAccess = CqlInt 4 + toCql SelfInviteAccess = CqlInt 5 fromCql (CqlInt i) = case i of 1 -> pure PrivateAccess 2 -> pure InviteAccess 3 -> pure LinkAccess 4 -> pure CodeAccess + 5 -> pure SelfInviteAccess n -> Left $ "Unexpected Access value: " ++ show n fromCql _ = Left "Access value: int expected" @@ -182,13 +186,14 @@ instance Cql Public.EnforceAppLock where instance Cql ProtocolTag where ctype = Tagged IntColumn - toCql ProtocolProteusTag = CqlInt 0 - toCql ProtocolMLSTag = CqlInt 1 + toCql = CqlInt . fromIntegral . fromEnum - fromCql (CqlInt i) = case i of - 0 -> pure ProtocolProteusTag - 1 -> pure ProtocolMLSTag - n -> Left $ "unexpected protocol: " ++ show n + fromCql (CqlInt i) = do + let i' = fromIntegral i + if i' < fromEnum @ProtocolTag minBound + || i' > fromEnum @ProtocolTag maxBound + then Left $ "unexpected protocol: " ++ show i + else Right $ toEnum i' fromCql _ = Left "protocol: int expected" instance Cql GroupId where diff --git a/services/galley/src/Galley/Cassandra/Proposal.hs b/services/galley/src/Galley/Cassandra/Proposal.hs index eb5e5d9dd2..7b5089631c 100644 --- a/services/galley/src/Galley/Cassandra/Proposal.hs +++ b/services/galley/src/Galley/Cassandra/Proposal.hs @@ -15,7 +15,11 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Galley.Cassandra.Proposal (interpretProposalStoreToCassandra) where +module Galley.Cassandra.Proposal + ( interpretProposalStoreToCassandra, + ProposalOrigin (..), + ) +where import Cassandra import Data.Timeout @@ -41,23 +45,28 @@ interpretProposalStoreToCassandra :: interpretProposalStoreToCassandra = interpret $ embedClient . \case - StoreProposal groupId epoch ref raw -> + StoreProposal groupId epoch ref origin raw -> retry x5 $ - write (storeQuery defaultTTL) (params LocalQuorum (groupId, epoch, ref, raw)) + write (storeQuery defaultTTL) (params LocalQuorum (groupId, epoch, ref, origin, raw)) GetProposal groupId epoch ref -> runIdentity <$$> retry x1 (query1 getQuery (params LocalQuorum (groupId, epoch, ref))) + GetAllPendingProposalRefs groupId epoch -> + runIdentity <$$> retry x1 (query getAllPendingRef (params LocalQuorum (groupId, epoch))) GetAllPendingProposals groupId epoch -> - runIdentity <$$> retry x1 (query getAllPending (params LocalQuorum (groupId, epoch))) + retry x1 (query getAllPending (params LocalQuorum (groupId, epoch))) -storeQuery :: Timeout -> PrepQuery W (GroupId, Epoch, ProposalRef, RawMLS Proposal) () +storeQuery :: Timeout -> PrepQuery W (GroupId, Epoch, ProposalRef, ProposalOrigin, RawMLS Proposal) () storeQuery ttl = fromString $ - "insert into mls_proposal_refs (group_id, epoch, ref, proposal)\ - \ values (?, ?, ?, ?) using ttl " + "insert into mls_proposal_refs (group_id, epoch, ref, origin, proposal)\ + \ values (?, ?, ?, ?, ?) using ttl " <> show (ttl #> Second) getQuery :: PrepQuery R (GroupId, Epoch, ProposalRef) (Identity (RawMLS Proposal)) getQuery = "select proposal from mls_proposal_refs where group_id = ? and epoch = ? and ref = ?" -getAllPending :: PrepQuery R (GroupId, Epoch) (Identity ProposalRef) -getAllPending = "select ref from mls_proposal_refs where group_id = ? and epoch = ?" +getAllPendingRef :: PrepQuery R (GroupId, Epoch) (Identity ProposalRef) +getAllPendingRef = "select ref from mls_proposal_refs where group_id = ? and epoch = ?" + +getAllPending :: PrepQuery R (GroupId, Epoch) (Maybe ProposalOrigin, RawMLS Proposal) +getAllPending = "select origin, proposal from mls_proposal_refs where group_id = ? and epoch = ?" diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index 9e50d5808e..ce8ec54973 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -25,6 +25,7 @@ import Data.Json.Util import Data.LegalHold import Data.Misc import qualified Data.Text.Lazy as LT +import Galley.Cassandra.Instances () import Galley.Data.Scope import Galley.Types.Teams.Intra import Imports @@ -197,9 +198,40 @@ updateTeamSplashScreen = "update team set splash_screen = ? where team = ?" -- Conversations ------------------------------------------------------------ -selectConv :: PrepQuery R (Identity ConvId) (ConvType, UserId, Maybe (C.Set Access), Maybe AccessRoleLegacy, Maybe (C.Set AccessRoleV2), Maybe Text, Maybe TeamId, Maybe Bool, Maybe Milliseconds, Maybe ReceiptMode, Maybe ProtocolTag, Maybe GroupId, Maybe Epoch, Maybe CipherSuiteTag) +selectConv :: + PrepQuery + R + (Identity ConvId) + ( ConvType, + Maybe UserId, + Maybe (C.Set Access), + Maybe AccessRoleLegacy, + Maybe (C.Set AccessRoleV2), + Maybe Text, + Maybe TeamId, + Maybe Bool, + Maybe Milliseconds, + Maybe ReceiptMode, + Maybe ProtocolTag, + Maybe GroupId, + Maybe Epoch, + Maybe CipherSuiteTag + ) selectConv = "select type, creator, access, access_role, access_roles_v2, name, team, deleted, message_timer, receipt_mode, protocol, group_id, epoch, cipher_suite from conversation where conv = ?" +selectGlobalTeamConv :: + PrepQuery + R + (Identity ConvId) + ( Maybe UserId, + Maybe Text, + Maybe TeamId, + Maybe GroupId, + Maybe Epoch, + Maybe CipherSuiteTag + ) +selectGlobalTeamConv = "select creator, name, team, group_id, epoch, cipher_suite from conversation where conv = ?" + selectReceiptMode :: PrepQuery R (Identity ConvId) (Identity (Maybe ReceiptMode)) selectReceiptMode = "select receipt_mode from conversation where conv = ?" @@ -209,6 +241,37 @@ isConvDeleted = "select deleted from conversation where conv = ?" insertConv :: PrepQuery W (ConvId, ConvType, UserId, C.Set Access, C.Set AccessRoleV2, Maybe Text, Maybe TeamId, Maybe Milliseconds, Maybe ReceiptMode, ProtocolTag, Maybe GroupId, Maybe Epoch, Maybe CipherSuiteTag) () insertConv = "insert into conversation (conv, type, creator, access, access_roles_v2, name, team, message_timer, receipt_mode, protocol, group_id, epoch, cipher_suite) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" +insertMLSSelfConv :: + PrepQuery + W + ( ConvId, + ConvType, + UserId, + C.Set Access, + C.Set AccessRoleV2, + Maybe Text, + Maybe TeamId, + Maybe Milliseconds, + Maybe ReceiptMode, + Maybe GroupId, + Maybe CipherSuiteTag + ) + () +insertMLSSelfConv = + fromString $ + "insert into conversation (conv, type, creator, access, \ + \ access_roles_v2, name, team, message_timer, receipt_mode,\ + \ protocol, group_id, cipher_suite) values \ + \ (?, ?, ?, ?, ?, ?, ?, ?, ?, " + <> show (fromEnum ProtocolMLSTag) + <> ", ?, ?)" + +insertGlobalTeamConv :: PrepQuery W (ConvId, C.Set Access, Text, TeamId, Maybe GroupId, Maybe CipherSuiteTag) () +insertGlobalTeamConv = "insert into conversation (conv, type, access, name, team, group_id, cipher_suite) values (?, 4, ?, ?, ?, ?, ?)" + +setGlobalTeamConvCreator :: PrepQuery W (UserId, ConvId) () +setGlobalTeamConvCreator = "update conversation set creator = ? where conv = ?" + updateConvAccess :: PrepQuery W (C.Set Access, C.Set AccessRoleV2, ConvId) () updateConvAccess = "update conversation set access = ?, access_roles_v2 = ? where conv = ?" @@ -375,14 +438,14 @@ rmMemberClient c = -- MLS Clients -------------------------------------------------------------- -addMLSClient :: PrepQuery W (ConvId, Domain, UserId, ClientId, KeyPackageRef) () -addMLSClient = "insert into member_client (conv, user_domain, user, client, key_package_ref) values (?, ?, ?, ?, ?)" +addMLSClient :: PrepQuery W (GroupId, Domain, UserId, ClientId, KeyPackageRef) () +addMLSClient = "insert into mls_group_member_client (group_id, 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 = ?" +removeMLSClient :: PrepQuery W (GroupId, Domain, UserId, ClientId) () +removeMLSClient = "delete from mls_group_member_client where group_id = ? and user_domain = ? and user = ? and client = ?" -lookupMLSClients :: PrepQuery R (Identity ConvId) (Domain, UserId, ClientId, KeyPackageRef) -lookupMLSClients = "select user_domain, user, client, key_package_ref from member_client where conv = ?" +lookupMLSClients :: PrepQuery R (Identity GroupId) (Domain, UserId, ClientId, KeyPackageRef) +lookupMLSClients = "select user_domain, user, client, key_package_ref from mls_group_member_client where group_id = ?" 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 1dc85be7a7..52d900f45f 100644 --- a/services/galley/src/Galley/Cassandra/Team.hs +++ b/services/galley/src/Galley/Cassandra/Team.hs @@ -14,6 +14,7 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . +{-# LANGUAGE LambdaCase #-} module Galley.Cassandra.Team ( interpretTeamStoreToCassandra, @@ -157,23 +158,23 @@ createTeam t uid (fromRange -> n) i k b = do listBillingTeamMembers :: TeamId -> Client [UserId] listBillingTeamMembers tid = - fmap runIdentity - <$> retry x1 (query Cql.listBillingTeamMembers (params LocalQuorum (Identity tid))) + runIdentity + <$$> retry x1 (query Cql.listBillingTeamMembers (params LocalQuorum (Identity tid))) getTeamName :: TeamId -> Client (Maybe Text) getTeamName tid = - fmap runIdentity - <$> retry x1 (query1 Cql.selectTeamName (params LocalQuorum (Identity tid))) + runIdentity + <$$> retry x1 (query1 Cql.selectTeamName (params LocalQuorum (Identity tid))) teamConversation :: TeamId -> ConvId -> Client (Maybe TeamConversation) teamConversation t c = - fmap (newTeamConversation . runIdentity) - <$> retry x1 (query1 Cql.selectTeamConv (params LocalQuorum (t, c))) + newTeamConversation . runIdentity + <$$> retry x1 (query1 Cql.selectTeamConv (params LocalQuorum (t, c))) getTeamConversations :: TeamId -> Client [TeamConversation] getTeamConversations t = - map (newTeamConversation . runIdentity) - <$> retry x1 (query Cql.selectTeamConvs (params LocalQuorum (Identity t))) + newTeamConversation . runIdentity + <$$> retry x1 (query Cql.selectTeamConvs (params LocalQuorum (Identity t))) teamIdsFrom :: UserId -> Maybe TeamId -> Range 1 100 Int32 -> Client (ResultSet TeamId) teamIdsFrom usr range (fromRange -> max) = @@ -185,7 +186,7 @@ teamIdsFrom usr range (fromRange -> max) = teamIdsForPagination :: UserId -> Maybe TeamId -> Range 1 100 Int32 -> Client (Page TeamId) teamIdsForPagination usr range (fromRange -> max) = - fmap runIdentity <$> case range of + runIdentity <$$> case range of Just c -> paginate Cql.selectUserTeamsFrom (paramsP LocalQuorum (usr, c) max) Nothing -> paginate Cql.selectUserTeams (paramsP LocalQuorum (Identity usr) max) diff --git a/services/galley/src/Galley/Data/Conversation.hs b/services/galley/src/Galley/Data/Conversation.hs index 71f3dcd5ac..3f83ecc9da 100644 --- a/services/galley/src/Galley/Data/Conversation.hs +++ b/services/galley/src/Galley/Data/Conversation.hs @@ -23,6 +23,7 @@ module Galley.Data.Conversation -- * Utilities isConvDeleted, selfConv, + globalTeamConv, localOne2OneConvId, convAccess, convAccessData, @@ -58,6 +59,9 @@ isConvDeleted = convDeleted selfConv :: UserId -> ConvId selfConv uid = Id (toUUID uid) +globalTeamConv :: TeamId -> ConvId +globalTeamConv tid = Id (toUUID tid) + -- | We deduce the conversation ID by adding the 4 components of the V4 UUID -- together pairwise, and then setting the version bits (v4) and variant bits -- (variant 2). This means that we always know what the UUID is for a diff --git a/services/galley/src/Galley/Data/Conversation/Types.hs b/services/galley/src/Galley/Data/Conversation/Types.hs index b93bd616c5..a8ad662a21 100644 --- a/services/galley/src/Galley/Data/Conversation/Types.hs +++ b/services/galley/src/Galley/Data/Conversation/Types.hs @@ -43,3 +43,9 @@ data NewConversation = NewConversation ncUsers :: UserList (UserId, RoleName), ncProtocol :: ProtocolTag } + +mlsMetadata :: Conversation -> Maybe ConversationMLSData +mlsMetadata conv = + case convProtocol conv of + ProtocolProteus -> Nothing + ProtocolMLS meta -> pure meta diff --git a/services/galley/src/Galley/Effects/BrigAccess.hs b/services/galley/src/Galley/Effects/BrigAccess.hs index 5257a591f7..570672a0b9 100644 --- a/services/galley/src/Galley/Effects/BrigAccess.hs +++ b/services/galley/src/Galley/Effects/BrigAccess.hs @@ -132,7 +132,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) + ValidateAndAddKeyPackageRef :: NewKeyPackage -> BrigAccess m (Either Text 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 8a4b699a72..f1d9f37495 100644 --- a/services/galley/src/Galley/Effects/ConversationStore.hs +++ b/services/galley/src/Galley/Effects/ConversationStore.hs @@ -24,9 +24,13 @@ module Galley.Effects.ConversationStore -- * Create conversation createConversationId, createConversation, + createMLSSelfConversation, -- * Read conversation getConversation, + getGlobalTeamConversation, + getGlobalTeamConversationById, + createGlobalTeamConversation, getConversationIdByGroupId, getConversations, getConversationMetadata, @@ -67,13 +71,20 @@ import Imports import Polysemy import Wire.API.Conversation hiding (Conversation, Member) import Wire.API.MLS.Epoch +import Wire.API.MLS.GlobalTeamConversation import Wire.API.MLS.PublicGroupState data ConversationStore m a where CreateConversationId :: ConversationStore m ConvId CreateConversation :: Local ConvId -> NewConversation -> ConversationStore m Conversation + CreateMLSSelfConversation :: + Local UserId -> + ConversationStore m Conversation DeleteConversation :: ConvId -> ConversationStore m () GetConversation :: ConvId -> ConversationStore m (Maybe Conversation) + GetGlobalTeamConversation :: Local TeamId -> ConversationStore m (Maybe GlobalTeamConversation) + GetGlobalTeamConversationById :: Local ConvId -> ConversationStore m (Maybe GlobalTeamConversation) + CreateGlobalTeamConversation :: Local TeamId -> ConversationStore m GlobalTeamConversation GetConversationIdByGroupId :: GroupId -> ConversationStore m (Maybe (Qualified ConvId)) GetConversations :: [ConvId] -> ConversationStore m [Conversation] GetConversationMetadata :: ConvId -> ConversationStore m (Maybe ConversationMetadata) diff --git a/services/galley/src/Galley/Effects/MemberStore.hs b/services/galley/src/Galley/Effects/MemberStore.hs index 1bd42b5533..f9f5b57d50 100644 --- a/services/galley/src/Galley/Effects/MemberStore.hs +++ b/services/galley/src/Galley/Effects/MemberStore.hs @@ -57,6 +57,7 @@ import Galley.Types.UserList import Imports import Polysemy import Wire.API.Conversation.Member hiding (Member) +import Wire.API.MLS.Group import Wire.API.MLS.KeyPackage import Wire.API.Provider.Service @@ -74,10 +75,10 @@ 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, KeyPackageRef) -> MemberStore m () - RemoveMLSClients :: Local ConvId -> Qualified UserId -> Set ClientId -> MemberStore m () + AddMLSClients :: GroupId -> Qualified UserId -> Set (ClientId, KeyPackageRef) -> MemberStore m () + RemoveMLSClients :: GroupId -> Qualified UserId -> Set ClientId -> MemberStore m () LookupMLSClients :: - Local ConvId -> + GroupId -> MemberStore m (Map (Qualified UserId) (Set (ClientId, KeyPackageRef))) makeSem ''MemberStore diff --git a/services/galley/src/Galley/Effects/ProposalStore.hs b/services/galley/src/Galley/Effects/ProposalStore.hs index 4bbd86c871..4dfd599317 100644 --- a/services/galley/src/Galley/Effects/ProposalStore.hs +++ b/services/galley/src/Galley/Effects/ProposalStore.hs @@ -31,6 +31,7 @@ data ProposalStore m a where GroupId -> Epoch -> ProposalRef -> + ProposalOrigin -> RawMLS Proposal -> ProposalStore m () GetProposal :: @@ -38,9 +39,13 @@ data ProposalStore m a where Epoch -> ProposalRef -> ProposalStore m (Maybe (RawMLS Proposal)) - GetAllPendingProposals :: + GetAllPendingProposalRefs :: GroupId -> Epoch -> ProposalStore m [ProposalRef] + GetAllPendingProposals :: + GroupId -> + Epoch -> + ProposalStore m [(Maybe ProposalOrigin, RawMLS Proposal)] makeSem ''ProposalStore diff --git a/services/galley/src/Galley/Intra/Client.hs b/services/galley/src/Galley/Intra/Client.hs index d21233576b..80caf13870 100644 --- a/services/galley/src/Galley/Intra/Client.hs +++ b/services/galley/src/Galley/Intra/Client.hs @@ -41,6 +41,7 @@ import Data.Misc import Data.Qualified import qualified Data.Set as Set import Data.Text.Encoding +import Data.Text.Lazy (toStrict) import Galley.API.Error import Galley.Effects import Galley.Env @@ -53,6 +54,7 @@ import qualified Network.HTTP.Types as HTTP import Network.HTTP.Types.Method import Network.HTTP.Types.Status import Network.Wai.Utilities.Error hiding (Error) +import qualified Network.Wai.Utilities.Error as Error import Polysemy import Polysemy.Error import Polysemy.Input @@ -227,7 +229,7 @@ updateKeyPackageRef keyPackageRef = . expect2xx ) -validateAndAddKeyPackageRef :: NewKeyPackage -> App (Maybe NewKeyPackageResult) +validateAndAddKeyPackageRef :: NewKeyPackage -> App (Either Text NewKeyPackageResult) validateAndAddKeyPackageRef nkp = do res <- call @@ -238,6 +240,8 @@ validateAndAddKeyPackageRef nkp = do ) 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 + | statusCode `div` 100 == 2 -> Right <$> parseResponse (mkError status502 "server-error") res + | statusCode `div` 100 == 4 -> do + err <- parseResponse (mkError status502 "server-error") res + pure (Left ("Error validating keypackage: " <> toStrict (Error.label err) <> ": " <> toStrict (Error.message err))) | otherwise -> throwM (mkError status502 "server-error" "Unexpected http status returned from /i/mls/key-packages/add") diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 3f30b17bed..785c099b9a 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -1786,12 +1786,14 @@ listConvIdsOk = do connectUsers alice (singleton bob) void $ postO2OConv alice bob (Just "gossip") let paginationOpts = GetPaginatedConversationIds Nothing (toRange (Proxy @5)) + -- Each of the users has a Proteus self-conversation, an MLS self-conversation + -- and the one-to-one coversation. listConvIds alice paginationOpts !!! do const 200 === statusCode - const (Right 2) === fmap length . decodeQualifiedConvIdList + const (Right 3) === fmap length . decodeQualifiedConvIdList listConvIds bob paginationOpts !!! do const 200 === statusCode - const (Right 2) === fmap length . decodeQualifiedConvIdList + const (Right 3) === fmap length . decodeQualifiedConvIdList paginateConvListIds :: TestM () paginateConvListIds = do @@ -1802,7 +1804,7 @@ paginateConvListIds = do now <- liftIO getCurrentTime fedGalleyClient <- view tsFedGalleyClient - replicateM_ 197 $ + replicateM_ 196 $ postConv alice [bob, eve] (Just "gossip") [] Nothing Nothing !!! const 201 === statusCode @@ -1838,9 +1840,9 @@ paginateConvListIds = do } runFedClient @"on-conversation-updated" fedGalleyClient deeDomain cu - -- 1 self conv + 2 convs with bob and eve + 197 local convs + 25 convs on - -- chad.example.com + 31 on dee.example = 256 convs. Getting them 16 at a time - -- should get all them in 16 times. + -- 1 Proteus self conv + 1 MLS self conv + 2 convs with bob and eve + 196 + -- local convs + 25 convs on chad.example.com + 31 on dee.example = 256 convs. + -- Getting them 16 at a time should get all them in 16 times. foldM_ (getChunkedConvs 16 0 alice) Nothing [16, 15 .. 0 :: Int] -- This test ensures to setup conversations so that a page would end exactly @@ -1856,9 +1858,9 @@ paginateConvListIdsPageEndingAtLocalsAndDomain = do now <- liftIO getCurrentTime fedGalleyClient <- view tsFedGalleyClient - -- With page size 16, 29 group convs + 2 one-to-one convs + 1 self conv, we - -- get 32 convs. The 2nd page should end here. - replicateM_ 29 $ + -- With page size 16, 28 group convs + 2 one-to-one convs + 1 Proteus self + -- conv + 1 MLS self conv, we get 32 convs. The 2nd page should end here. + replicateM_ 28 $ postConv alice [bob, eve] (Just "gossip") [] Nothing Nothing !!! const 201 === statusCode @@ -2075,7 +2077,19 @@ postConvQualifiedFederationNotEnabled = do -- FUTUREWORK: figure out how to use functions in the TestM monad inside withSettingsOverrides and remove this duplication postConvHelper :: (MonadIO m, MonadHttp m) => (Request -> Request) -> UserId -> [Qualified UserId] -> m ResponseLBS postConvHelper g zusr newUsers = do - let conv = NewConv [] newUsers (checked "gossip") (Set.fromList []) Nothing Nothing Nothing Nothing roleNameWireAdmin ProtocolProteusTag Nothing + let conv = + NewConv + [] + newUsers + (checked "gossip") + (Set.fromList []) + Nothing + Nothing + Nothing + Nothing + roleNameWireAdmin + ProtocolProteusTag + Nothing post $ g . path "/conversations" . zUser zusr . zConn "conn" . zType "access" . json conv postSelfConvOk :: TestM () @@ -2104,7 +2118,19 @@ postConvO2OFailWithSelf :: TestM () postConvO2OFailWithSelf = do g <- viewGalley alice <- randomUser - let inv = NewConv [alice] [] Nothing mempty Nothing Nothing Nothing Nothing roleNameWireAdmin ProtocolProteusTag Nothing + 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 const 403 === statusCode const (Just "invalid-op") === fmap label . responseJsonUnsafe diff --git a/services/galley/test/integration/API/Federation.hs b/services/galley/test/integration/API/Federation.hs index 89ef4c2ebd..3a186a9a56 100644 --- a/services/galley/test/integration/API/Federation.hs +++ b/services/galley/test/integration/API/Federation.hs @@ -1150,3 +1150,5 @@ getConvAction tquery (SomeConversationAction tag action) = (SConversationAccessDataTag, _) -> Nothing (SConversationRemoveMembersTag, SConversationRemoveMembersTag) -> Just action (SConversationRemoveMembersTag, _) -> Nothing + (SConversationSelfInviteTag, SConversationSelfInviteTag) -> Just action + (SConversationSelfInviteTag, _) -> Nothing diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 94a9dbd548..0de8fba481 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -20,11 +20,13 @@ module API.MLS (tests) where import API.MLS.Util -import API.Util +-- import API.SQS +import API.Util as Util import Bilge hiding (head) import Bilge.Assert import Cassandra -import Control.Lens (view) +-- import Control.Error.Util (hush) +import Control.Lens (view) -- , (^.)) import qualified Control.Monad.State as State import Crypto.Error import qualified Crypto.PubKey.Ed25519 as Ed25519 @@ -45,6 +47,8 @@ import Data.String.Conversions import qualified Data.Text as T import Data.Time import Federator.MockServer hiding (withTempMockFederator) +-- import Galley.Data.Conversation +-- import Galley.Options import Imports import qualified Network.Wai.Utilities.Error as Wai import Test.QuickCheck (Arbitrary (arbitrary), generate) @@ -61,11 +65,16 @@ import Wire.API.Conversation.Role import Wire.API.Error.Galley 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.GlobalTeamConversation +-- import Wire.API.MLS.Group import Wire.API.MLS.Keys import Wire.API.MLS.Serialisation import Wire.API.MLS.Welcome import Wire.API.Message +import Wire.API.Routes.MultiTablePaging +-- import Wire.API.Team (teamCreator) import Wire.API.User.Client tests :: IO TestSetup -> TestTree @@ -115,7 +124,8 @@ tests s = "External commit" [ test s "non-member attempts to join a conversation" testExternalCommitNotMember, test s "join a conversation with the same client" testExternalCommitSameClient, - test s "join a conversation with a new client" testExternalCommitNewClient + test s "join a conversation with a new client" testExternalCommitNewClient, + test s "join a conversation with a new client and resend backend proposals" testExternalCommitNewClientResendBackendProposal ], testGroup "Application Message" @@ -182,8 +192,27 @@ tests s = testGroup "CommitBundle" [ test s "add user with a commit bundle" testAddUserWithBundle, - test s "add user with a commit bundle to a remote conversation" testAddUserToRemoveConvWithBundle, + test s "add user with a commit bundle to a remote conversation" testAddUserToRemoteConvWithBundle, test s "remote user posts commit bundle" testRemoteUserPostsCommitBundle + ], + -- testGroup + -- "GlobalTeamConv" + -- [ test s "Non-existing team returns 403" testGetGlobalTeamConvNonExistant, + -- test s "Non member of team returns 403" testGetGlobalTeamConvNonMember, + -- test s "Global team conversation is created on get if not present" (testGetGlobalTeamConv s), + -- test s "Can't leave global team conversation" testGlobalTeamConversationLeave, + -- test s "Send message in global team conversation" testGlobalTeamConversationMessage, + -- test s "Listing convs includes global team conversation" testConvListIncludesGlobal, + -- test s "Listing convs includes global team conversation for new users" testConvListIncludesGlobalForNewUsers, + -- test s "Listing convs before calling GET on global team conversation still includes it" testConvListIncludesGlobalBeforeGet + -- ], + testGroup + "Self conversation" + [ test s "create a self conversation" testSelfConversation, + test s "do not list a self conversation below v3" $ testSelfConversationList True, + test s "list a self conversation automatically from v3" $ testSelfConversationList False, + test s "attempt to add another user to a conversation fails" testSelfConversationOtherUser, + test s "attempt to leave fails" testSelfConversationLeave ] ] @@ -240,7 +269,7 @@ testSenderNotInConversation = do -- send the message as bob, who is not in the conversation err <- responseJsonError - =<< postMessage (qUnqualified bob) (mpMessage message) + =<< postMessage bob1 (mpMessage message) do err <- responseJsonError - =<< postMessage (ciUser (mpSender commit)) (mpMessage commit) + =<< postMessage (mpSender commit) (mpMessage commit) >= sendAndConsumeCommit @@ -798,7 +828,7 @@ testRemoveClientsIncomplete = do err <- responseJsonError - =<< postMessage (qUnqualified alice) (mpMessage commit) + =<< postMessage alice1 (mpMessage commit) getGroupInfo (ciUser alice1) qcnv mp <- createExternalCommit bob1 (Just pgs) qcnv bundle <- createBundle mp - postCommitBundle (ciUser (mpSender mp)) bundle + postCommitBundle (mpSender mp) bundle !!! const 404 === statusCode testExternalCommitSameClient :: TestM () @@ -1020,6 +1050,61 @@ testExternalCommitNewClient = do -- the list of members should be [alice1, bob1] +-- | Check that external backend proposals are replayed after external commits +-- AND that (external) client proposals are NOT replayed by the backend in the +-- same case (since this is up to the clients). +testExternalCommitNewClientResendBackendProposal :: TestM () +testExternalCommitNewClientResendBackendProposal = do + [alice, bob] <- createAndConnectUsers (replicate 2 Nothing) + + runMLSTest $ do + [alice1, bob1, bob2] <- traverse createMLSClient [alice, bob, bob] + forM_ [bob1, bob2] uploadNewKeyPackage + (_, qcnv) <- setupMLSGroup alice1 + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + Just (_, kpBob2) <- find (\(ci, _) -> ci == bob2) <$> getClientsFromGroupState alice1 bob + + mlsBracket [alice1, bob1] $ \[wsA, wsB] -> do + liftTest $ + deleteClient (qUnqualified bob) (ciClient bob2) (Just defPassword) + !!! statusCode === const 200 + WS.assertMatchN_ (5 # WS.Second) [wsB] $ + wsAssertClientRemoved (ciClient bob2) + + State.modify $ \mls -> + mls + { mlsMembers = Set.difference (mlsMembers mls) (Set.fromList [bob2]) + } + + WS.assertMatchN_ (5 # WS.Second) [wsA, wsB] $ + wsAssertBackendRemoveProposalWithEpoch bob qcnv kpBob2 (Epoch 1) + + [bob3, bob4] <- for [bob, bob] $ \qusr' -> do + ci <- createMLSClient qusr' + WS.assertMatchN_ (5 # WS.Second) [wsB] $ + wsAssertClientAdded (ciClient ci) + pure ci + + void $ + createExternalAddProposal bob3 + >>= sendAndConsumeMessage + WS.assertMatchN_ (5 # WS.Second) [wsA, wsB] $ + void . wsAssertAddProposal bob qcnv + + mp <- createExternalCommit bob4 Nothing qcnv + ecEvents <- sendAndConsumeCommitBundle mp + liftIO $ + assertBool "No events after external commit expected" (null ecEvents) + WS.assertMatchN_ (5 # WS.Second) [wsA, wsB] $ + wsAssertMLSMessage qcnv bob (mpMessage mp) + + -- The backend proposals for bob2 are replayed, but the external add + -- proposal for bob3 has to replayed by the client and is thus not found + -- here. + WS.assertMatchN_ (5 # WS.Second) [wsA, wsB] $ + wsAssertBackendRemoveProposalWithEpoch bob qcnv kpBob2 (Epoch 2) + WS.assertNoEvent (2 # WS.Second) [wsA, wsB] + testAppMessage :: TestM () testAppMessage = do users@(alice : _) <- createAndConnectUsers (replicate 4 Nothing) @@ -1277,7 +1362,7 @@ propNonExistingConv = do createGroup alice1 "test_group" [prop] <- createAddProposals alice1 [bob] - postMessage (ciUser alice1) (mpMessage prop) !!! do + postMessage alice1 (mpMessage prop) !!! do const 404 === statusCode const (Just "no-conversation") === fmap Wai.label . responseJsonError @@ -1294,7 +1379,7 @@ propExistingConv = do propInvalidEpoch :: TestM () propInvalidEpoch = do - users@[alice, bob, charlie, dee] <- createAndConnectUsers (replicate 4 Nothing) + users@[_alice, bob, charlie, dee] <- createAndConnectUsers (replicate 4 Nothing) runMLSTest $ do [alice1, bob1, charlie1, dee1] <- traverse createMLSClient users void $ setupMLSGroup alice1 @@ -1310,7 +1395,7 @@ propInvalidEpoch = do [prop] <- createAddProposals alice1 [dee] err <- responseJsonError - =<< postMessage (qUnqualified alice) (mpMessage prop) + =<< postMessage alice1 (mpMessage prop) >= sendAndConsumeCommit prop <- createExternalAddProposal bob2 - postMessage (qUnqualified charlie) (mpMessage prop) + postMessage charlie1 (mpMessage prop) !!! do const 422 === statusCode const (Just "mls-unsupported-proposal") === fmap Wai.label . responseJsonError @@ -1464,7 +1549,7 @@ testExternalAddProposalWrongClient = do -- charlie attempts to join with an external add proposal testExternalAddProposalWrongUser :: TestM () testExternalAddProposalWrongUser = do - users@[_, bob, charlie] <- createAndConnectUsers (replicate 3 Nothing) + users@[_, bob, _charlie] <- createAndConnectUsers (replicate 3 Nothing) runMLSTest $ do -- setup clients @@ -1477,7 +1562,7 @@ testExternalAddProposalWrongUser = do >>= sendAndConsumeCommit prop <- createExternalAddProposal charlie1 - postMessage (qUnqualified charlie) (mpMessage prop) + postMessage charlie1 (mpMessage prop) !!! do const 404 === statusCode const (Just "no-conversation") === fmap Wai.label . responseJsonError @@ -1535,7 +1620,7 @@ propUnsupported = do -- we cannot use sendAndConsumeMessage here, because openmls does not yet -- support AppAck proposals - postMessage (ciUser alice1) msgData !!! const 201 === statusCode + postMessage alice1 msgData !!! const 201 === statusCode testBackendRemoveProposalLocalConvLocalUser :: TestM () testBackendRemoveProposalLocalConvLocalUser = do @@ -1977,7 +2062,7 @@ testDeleteMLSConv :: TestM () testDeleteMLSConv = do localDomain <- viewFederationDomain -- c <- view tsCannon - (tid, aliceUnq, [bobUnq]) <- API.Util.createBindingTeamWithMembers 2 + (tid, aliceUnq, [bobUnq]) <- Util.createBindingTeamWithMembers 2 let alice = Qualified aliceUnq localDomain bob = Qualified bobUnq localDomain @@ -1992,8 +2077,8 @@ testDeleteMLSConv = do deleteTeamConv tid (qUnqualified qcnv) aliceUnq !!! statusCode === const 200 -testAddUserToRemoveConvWithBundle :: TestM () -testAddUserToRemoveConvWithBundle = do +testAddUserToRemoteConvWithBundle :: TestM () +testAddUserToRemoteConvWithBundle = do let aliceDomain = Domain "faraway.example.com" [alice, bob, charlie] <- createAndConnectUsers [Just (domainText aliceDomain), Nothing, Nothing] @@ -2075,3 +2160,287 @@ testRemoteUserPostsCommitBundle = do MLSMessageResponseError MLSUnsupportedProposal <- runFedClient @"send-mls-commit-bundle" fedGalleyClient (Domain bobDomain) msr pure () + +-- testGetGlobalTeamConvNonExistant :: TestM () +-- testGetGlobalTeamConvNonExistant = do +-- uid <- randomUser +-- tid <- randomId +-- -- authorisation fails b/c not a team member +-- getGlobalTeamConv uid tid !!! const 403 === statusCode +-- +-- testGetGlobalTeamConvNonMember :: TestM () +-- testGetGlobalTeamConvNonMember = do +-- owner <- randomUser +-- tid <- createBindingTeamInternal "sample-team" owner +-- team <- getTeam owner tid +-- assertQueue "create team" tActivate +-- liftIO $ assertEqual "owner" owner (team ^. teamCreator) +-- assertQueueEmpty +-- +-- -- authorisation fails b/c not a team member +-- uid <- randomUser +-- getGlobalTeamConv uid tid !!! const 403 === statusCode +-- +-- testGetGlobalTeamConv :: IO TestSetup -> TestM () +-- testGetGlobalTeamConv setup = do +-- owner <- randomUser +-- tid <- createBindingTeamInternal "sample-team" owner +-- team <- getTeam owner tid +-- assertQueue "create team" tActivate +-- liftIO $ assertEqual "owner" owner (team ^. teamCreator) +-- assertQueueEmpty +-- +-- s <- liftIO setup +-- let domain = s ^. tsGConf . optSettings . setFederationDomain +-- +-- let response = getGlobalTeamConv owner tid response +-- let convoId = globalTeamConv tid +-- lconv = toLocalUnsafe domain convoId +-- expected = +-- GlobalTeamConversation +-- (qUntagged lconv) +-- ( ConversationMLSData +-- (convToGroupId lconv) +-- (Epoch 0) +-- MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 +-- ) +-- Nothing +-- [SelfInviteAccess] +-- "Global team conversation" +-- tid +-- +-- let cm = Aeson.decode rs :: Maybe GlobalTeamConversation +-- liftIO $ assertEqual "conversation metadata" cm (Just expected) +-- +-- testConvListIncludesGlobal :: TestM () +-- testConvListIncludesGlobal = do +-- aliceQ <- randomQualifiedUser +-- let alice = qUnqualified aliceQ +-- tid <- createBindingTeamInternal "sample-team" alice +-- team <- getTeam alice tid +-- assertQueue "create team" tActivate +-- liftIO $ assertEqual "alice" alice (team ^. teamCreator) +-- assertQueueEmpty +-- +-- -- global team conv doesn't yet include user +-- let paginationOpts = GetPaginatedConversationIds Nothing (toRange (Proxy @5)) +-- listConvIds alice paginationOpts !!! do +-- const 200 === statusCode +-- const (Just [globalTeamConv tid]) =/~= (hush . (<$$>) qUnqualified . decodeQualifiedConvIdList) +-- +-- -- add user to conv +-- runMLSTest $ do +-- alice1 <- createMLSClient aliceQ +-- +-- let response = getGlobalTeamConv alice tid response +-- let (Just gtc) = Aeson.decode rs :: Maybe GlobalTeamConversation +-- gid = cnvmlsGroupId $ gtcMlsMetadata gtc +-- +-- void $ uploadNewKeyPackage alice1 +-- +-- -- create mls group +-- createGroup alice1 gid +-- void $ createAddCommit alice1 [] >>= sendAndConsumeCommitBundle +-- +-- -- Now we should have the user as part of that conversation also in the backend +-- listConvIds alice paginationOpts !!! do +-- const 200 === statusCode +-- const (Just [globalTeamConv tid]) =~= (hush . (<$$>) qUnqualified . decodeQualifiedConvIdList) +-- +-- testConvListIncludesGlobalBeforeGet :: TestM () +-- testConvListIncludesGlobalBeforeGet = do +-- (tid, alice, []) <- Util.createBindingTeamWithMembers 1 +-- let paginationOpts = GetPaginatedConversationIds Nothing (toRange (Proxy @5)) +-- listConvIds alice paginationOpts !!! do +-- const 200 === statusCode +-- const (Just [globalTeamConv tid]) =~= (hush . (<$$>) qUnqualified . decodeQualifiedConvIdList) +-- +-- testConvListIncludesGlobalForNewUsers :: TestM () +-- testConvListIncludesGlobalForNewUsers = do +-- localDomain <- viewFederationDomain +-- -- c <- view tsCannon +-- (tid, alice, [bob]) <- Util.createBindingTeamWithMembers 2 +-- let aliceQ = Qualified alice localDomain +-- bobQ = Qualified bob localDomain +-- +-- runMLSTest $ do +-- [alice1, bob1] <- traverse createMLSClient [aliceQ, bobQ] +-- void $ uploadNewKeyPackage bob1 +-- +-- void $ setupMLSGroup alice1 +-- void $ createAddCommit alice1 [bobQ] >>= sendAndConsumeCommitBundle +-- +-- let paginationOpts = GetPaginatedConversationIds Nothing (toRange (Proxy @5)) +-- listConvIds alice paginationOpts !!! do +-- const 200 === statusCode +-- const (Just [globalTeamConv tid]) =~= (hush . (<$$>) qUnqualified . decodeQualifiedConvIdList) +-- +-- listConvIds bob paginationOpts !!! do +-- const 200 === statusCode +-- const (Just [globalTeamConv tid]) =~= (hush . (<$$>) qUnqualified . decodeQualifiedConvIdList) +-- +-- testGlobalTeamConversationMessage :: TestM () +-- testGlobalTeamConversationMessage = do +-- alice <- randomQualifiedUser +-- let aliceUnq = qUnqualified alice +-- +-- tid <- createBindingTeamInternal "sample-team" aliceUnq +-- team <- getTeam aliceUnq tid +-- assertQueue "create team" tActivate +-- liftIO $ assertEqual "owner" aliceUnq (team ^. teamCreator) +-- assertQueueEmpty +-- +-- runMLSTest $ do +-- clients@[alice1, alice2, alice3] <- traverse createMLSClient (replicate 3 alice) +-- +-- let response = getGlobalTeamConv aliceUnq tid response +-- let (Just gtc) = Aeson.decode rs :: Maybe GlobalTeamConversation +-- qcnv = gtcId gtc +-- gid = cnvmlsGroupId $ gtcMlsMetadata gtc +-- +-- traverse_ uploadNewKeyPackage clients +-- +-- createGroup alice1 gid +-- void $ createAddCommit alice1 [] >>= sendAndConsumeCommitBundle +-- +-- pgs <- +-- LBS.toStrict . fromJust . responseBody +-- <$> getGroupInfo (ciUser alice1) qcnv +-- void $ createExternalCommit alice2 (Just pgs) qcnv >>= sendAndConsumeCommitBundle +-- +-- -- FUTUREWORK: add tests for race conditions when adding two commits with same epoch? +-- -- TODO(elland): test racing conditions for get global team conv +-- pgs' <- +-- LBS.toStrict . fromJust . responseBody +-- <$> getGroupInfo (ciUser alice1) qcnv +-- void $ createExternalCommit alice3 (Just pgs') qcnv >>= sendAndConsumeCommitBundle +-- +-- do +-- message <- createApplicationMessage alice1 "some text" +-- +-- mlsBracket [alice2, alice3] $ \wss -> do +-- events <- sendAndConsumeMessage message +-- liftIO $ events @?= [] +-- liftIO $ +-- WS.assertMatchN_ (5 # WS.Second) wss $ +-- wsAssertMLSMessage qcnv alice (mpMessage message) +-- +-- do +-- message <- createApplicationMessage alice2 "some text new" +-- +-- mlsBracket [alice1, alice3] $ \wss -> do +-- events <- sendAndConsumeMessage message +-- liftIO $ events @?= [] +-- liftIO $ +-- WS.assertMatchN_ (5 # WS.Second) wss $ +-- wsAssertMLSMessage qcnv alice (mpMessage message) +-- +-- testGlobalTeamConversationLeave :: TestM () +-- testGlobalTeamConversationLeave = do +-- alice <- randomQualifiedUser +-- let aliceUnq = qUnqualified alice +-- +-- tid <- createBindingTeamInternal "sample-team" aliceUnq +-- team <- getTeam aliceUnq tid +-- assertQueue "create team" tActivate +-- liftIO $ assertEqual "owner" aliceUnq (team ^. teamCreator) +-- assertQueueEmpty +-- +-- runMLSTest $ do +-- alice1 <- createMLSClient alice +-- +-- let response = getGlobalTeamConv aliceUnq tid response +-- let (Just gtc) = Aeson.decode rs :: Maybe GlobalTeamConversation +-- gid = cnvmlsGroupId $ gtcMlsMetadata gtc +-- +-- void $ uploadNewKeyPackage alice1 +-- createGroup alice1 gid +-- void $ createAddCommit alice1 [] >>= sendAndConsumeCommitBundle +-- mlsBracket [alice1] $ \wss -> do +-- liftTest $ +-- deleteMemberQualified (qUnqualified alice) alice (gtcId gtc) +-- !!! do +-- const 403 === statusCode +-- const (Just "invalid-op") === fmap Wai.label . responseJsonError +-- WS.assertNoEvent (1 # WS.Second) wss + +testSelfConversation :: TestM () +testSelfConversation = do + alice <- randomQualifiedUser + runMLSTest $ do + creator : others <- traverse createMLSClient (replicate 3 alice) + traverse_ uploadNewKeyPackage others + void $ setupMLSSelfGroup creator + commit <- createAddCommit creator [alice] + welcome <- assertJust (mpWelcome commit) + mlsBracket others $ \wss -> do + void $ sendAndConsumeCommitBundle commit + WS.assertMatchN_ (5 # Second) wss $ + wsAssertMLSWelcome alice welcome + WS.assertNoEvent (1 # WS.Second) wss + +-- | The MLS self-conversation should be available even without explicitly +-- creating it by calling `GET /conversations/mls-self` starting from version 3 +-- of the client API and should not be listed in versions less than 3. +testSelfConversationList :: Bool -> TestM () +testSelfConversationList isBelowV3 = do + let (errMsg, justOrNothing, listCnvs) = + if isBelowV3 + then ("The MLS self-conversation is listed", isNothing, listConvIdsV2) + else ("The MLS self-conversation is not listed", isJust, listConvIds) + alice <- randomUser + do + mMLSSelf <- findSelfConv alice listCnvs + liftIO $ assertBool errMsg (justOrNothing mMLSSelf) + + -- make sure that the self-conversation is not listed below V3 even once it + -- has been created. + unless isBelowV3 $ do + mMLSSelf <- findSelfConv alice listConvIdsV2 + liftIO $ assertBool errMsg (isNothing mMLSSelf) + where + paginationOpts = GetPaginatedConversationIds Nothing (toRange (Proxy @100)) + + isMLSSelf u conv = mlsSelfConvId u == qUnqualified conv + + findSelfConv u listEndpoint = do + convIds :: ConvIdsPage <- + responseJsonError + =<< listEndpoint u paginationOpts + ) Nothing $ guard . isMLSSelf u <$> mtpResults convIds + +testSelfConversationOtherUser :: TestM () +testSelfConversationOtherUser = do + users@[_alice, bob] <- createAndConnectUsers [Nothing, Nothing] + runMLSTest $ do + [alice1, bob1] <- traverse createMLSClient users + void $ uploadNewKeyPackage bob1 + void $ setupMLSSelfGroup alice1 + commit <- createAddCommit alice1 [bob] + mlsBracket [alice1, bob1] $ \wss -> do + postMessage (mpSender commit) (mpMessage commit) + !!! do + const 403 === statusCode + const (Just "invalid-op") === fmap Wai.label . responseJsonError + WS.assertNoEvent (1 # WS.Second) wss + +testSelfConversationLeave :: TestM () +testSelfConversationLeave = do + alice <- randomQualifiedUser + runMLSTest $ do + clients@(creator : others) <- traverse createMLSClient (replicate 3 alice) + traverse_ uploadNewKeyPackage others + (_, qcnv) <- setupMLSSelfGroup creator + void $ createAddCommit creator [alice] >>= sendAndConsumeCommit + mlsBracket clients $ \wss -> do + liftTest $ + deleteMemberQualified (qUnqualified alice) alice qcnv + !!! do + const 403 === statusCode + const (Just "invalid-op") === fmap Wai.label . responseJsonError + WS.assertNoEvent (1 # WS.Second) wss diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index 438bbd698d..59d9e89be1 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -107,7 +107,7 @@ postMessage :: MonadHttp m, HasGalley m ) => - UserId -> + ClientIdentity -> ByteString -> m ResponseLBS postMessage sender msg = do @@ -115,7 +115,8 @@ postMessage sender msg = do post ( galley . paths ["mls", "messages"] - . zUser sender + . zUser (ciUser sender) + . zClient (ciClient sender) . zConn "conn" . content "message/mls" . bytes msg @@ -129,7 +130,7 @@ postCommitBundle :: MonadHttp m, HasGalley m ) => - UserId -> + ClientIdentity -> ByteString -> m ResponseLBS postCommitBundle sender bundle = do @@ -137,7 +138,8 @@ postCommitBundle sender bundle = do post ( galley . paths ["mls", "commit-bundles"] - . zUser sender + . zUser (ciUser sender) + . zClient (ciClient sender) . zConn "conn" . content "application/x-protobuf" . bytes bundle @@ -392,19 +394,17 @@ 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 +-- | Create a conversation from a provided action and then create a +-- corresponding group. +setupMLSGroupWithConv :: + HasCallStack => + MLSTest Conversation -> + ClientIdentity -> + MLSTest (GroupId, Qualified ConvId) +setupMLSGroupWithConv convAction creator = do ownDomain <- liftTest viewFederationDomain liftIO $ assertEqual "creator is not local" (ciDomain creator) ownDomain - conv <- - responseJsonError - =<< liftTest - ( postConvQualified - (ciUser creator) - (defNewMLSConv (ciClient creator)) - ) - ClientIdentity -> MLSTest (GroupId, Qualified ConvId) +setupMLSGroup creator = setupMLSGroupWithConv action creator + where + action = + responseJsonError + =<< liftTest + ( postConvQualified + (ciUser creator) + (defNewMLSConv (ciClient creator)) + ) + ClientIdentity -> MLSTest (GroupId, Qualified ConvId) +setupMLSSelfGroup creator = setupMLSGroupWithConv action creator + where + action = + responseJsonError + =<< liftTest + (getSelfConv (ciUser creator)) + GroupId -> MLSTest () createGroup cid gid = do State.gets mlsGroupId >>= \case @@ -619,13 +642,13 @@ createAddCommitWithKeyPackages qcid clientsAndKeyPackages = do { mlsNewMembers = Set.fromList (map fst clientsAndKeyPackages) } - welcome <- liftIO $ BS.readFile welcomeFile + welcome <- liftIO $ readWelcome welcomeFile pgs <- liftIO $ BS.readFile pgsFile pure $ MessagePackage { mpSender = qcid, mpMessage = commit, - mpWelcome = Just welcome, + mpWelcome = welcome, mpPublicGroupState = Just pgs } @@ -810,7 +833,7 @@ sendAndConsumeMessage :: HasCallStack => MessagePackage -> MLSTest [Event] sendAndConsumeMessage mp = do events <- fmap mmssEvents . responseJsonError - =<< postMessage (ciUser (mpSender mp)) (mpMessage mp) + =<< postMessage (mpSender mp) (mpMessage mp) Either Text CommitBundle +mkBundle :: HasCallStack => MessagePackage -> Either Text CommitBundle mkBundle mp = do commitB <- decodeMLS' (mpMessage mp) welcomeB <- traverse decodeMLS' (mpWelcome mp) @@ -850,7 +873,7 @@ mkBundle mp = do CommitBundle commitB welcomeB $ GroupInfoBundle UnencryptedGroupInfo TreeFull pgsB -createBundle :: MonadIO m => MessagePackage -> m ByteString +createBundle :: (HasCallStack, MonadIO m) => MessagePackage -> m ByteString createBundle mp = do bundle <- either (liftIO . assertFailure . T.unpack) pure $ @@ -866,7 +889,7 @@ sendAndConsumeCommitBundle mp = do events <- fmap mmssEvents . responseJsonError - =<< postCommitBundle (ciUser (mpSender mp)) bundle + =<< postCommitBundle (mpSender mp) bundle + TestM ResponseLBS +getSelfConv u = do + g <- viewGalley + get $ + g + . paths ["/conversations", "mls-self"] + . zUser u + . zConn "conn" + . zType "access" diff --git a/services/galley/test/integration/API/Teams.hs b/services/galley/test/integration/API/Teams.hs index fce7e7f3c3..379d159881 100644 --- a/services/galley/test/integration/API/Teams.hs +++ b/services/galley/test/integration/API/Teams.hs @@ -171,7 +171,7 @@ tests s = [ test s "message" (postCryptoBroadcastMessage bcast), test s "filtered only, too large team" (postCryptoBroadcastMessageFilteredTooLargeTeam bcast), test s "report missing in body" (postCryptoBroadcastMessageReportMissingBody bcast), - test s "redundant/missing" (postCryptoBroadcastMessage2 bcast), + test s "redundant or missing" (postCryptoBroadcastMessage2 bcast), test s "no-team" (postCryptoBroadcastMessageNoTeam bcast), test s "100 (or max conns)" (postCryptoBroadcastMessage100OrMaxConns bcast) ] @@ -192,16 +192,17 @@ testCreateTeam = do testGetTeams :: TestM () testGetTeams = do owner <- Util.randomUser - Util.getTeams owner [] >>= checkTeamList Nothing + let getTeams' = Util.getTeams owner + getTeams' [] >>= checkTeamList Nothing tid <- Util.createBindingTeamInternal "foo" owner <* assertQueue "create team" tActivate wrongTid <- (Util.randomUser >>= Util.createBindingTeamInternal "foobar") <* assertQueue "create team" tActivate - Util.getTeams owner [] >>= checkTeamList (Just tid) - Util.getTeams owner [("size", Just "1")] >>= checkTeamList (Just tid) - Util.getTeams owner [("ids", Just $ toByteString' tid)] >>= checkTeamList (Just tid) - Util.getTeams owner [("ids", Just $ toByteString' tid <> "," <> toByteString' wrongTid)] >>= checkTeamList (Just tid) + getTeams' [] >>= checkTeamList (Just tid) + getTeams' [("size", Just "1")] >>= checkTeamList (Just tid) + getTeams' [("ids", Just $ toByteString' tid)] >>= checkTeamList (Just tid) + getTeams' [("ids", Just $ toByteString' tid <> "," <> toByteString' wrongTid)] >>= checkTeamList (Just tid) -- these two queries do not yield responses that are equivalent to the old wai route API - Util.getTeams owner [("ids", Just $ toByteString' wrongTid)] >>= checkTeamList (Just tid) - Util.getTeams owner [("start", Just $ toByteString' tid)] >>= checkTeamList (Just tid) + getTeams' [("ids", Just $ toByteString' wrongTid)] >>= checkTeamList (Just tid) + getTeams' [("start", Just $ toByteString' tid)] >>= checkTeamList (Just tid) where checkTeamList :: Maybe TeamId -> TeamList -> TestM () checkTeamList mbTid tl = liftIO $ do diff --git a/services/galley/test/integration/API/Teams/Feature.hs b/services/galley/test/integration/API/Teams/Feature.hs index 4246598847..205505d51a 100644 --- a/services/galley/test/integration/API/Teams/Feature.hs +++ b/services/galley/test/integration/API/Teams/Feature.hs @@ -7,7 +7,6 @@ -- 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 @@ -15,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 head" #-} module API.Teams.Feature (tests) where @@ -49,7 +51,7 @@ import Test.Hspec (expectationFailure) import Test.QuickCheck (Gen, generate, suchThat) import Test.Tasty import qualified Test.Tasty.Cannon as WS -import Test.Tasty.HUnit (assertFailure, (@?=)) +import Test.Tasty.HUnit (assertBool, assertFailure, (@?=)) import TestHelpers (eventually, test) import TestSetup import Wire.API.Conversation.Protocol (ProtocolTag (ProtocolMLSTag, ProtocolProteusTag)) @@ -503,7 +505,7 @@ testSimpleFlagTTLOverride defaultValue ttl ttlAfter = do getFeatureConfig expectedStatus expectedTtl = eventually $ do actual <- Util.getFeatureConfig @cfg member liftIO $ Public.wsStatus actual @?= expectedStatus - liftIO $ Public.wsTTL actual @?= expectedTtl + liftIO $ checkTtl (Public.wsTTL actual) expectedTtl getFlagInternal :: HasCallStack => Public.FeatureStatus -> TestM () getFlagInternal expected = eventually $ do @@ -536,6 +538,19 @@ testSimpleFlagTTLOverride defaultValue ttl ttlAfter = do Just (FeatureTTLSeconds i) -> i <= upper unless check $ error ("expected ttl <= " <> show upper <> ", got " <> show storedTTL) + checkTtl :: FeatureTTL -> FeatureTTL -> IO () + checkTtl (FeatureTTLSeconds actualTtl) (FeatureTTLSeconds expectedTtl) = + assertBool + ("expected the actual TTL to be greater than 0 and equal to or no more than 2 seconds less than " <> show expectedTtl <> ", but it was " <> show actualTtl) + ( actualTtl > 0 + && actualTtl <= expectedTtl + && abs (fromIntegral @Word @Int actualTtl - fromIntegral @Word @Int expectedTtl) <= 2 + ) + checkTtl FeatureTTLUnlimited FeatureTTLUnlimited = pure () + checkTtl FeatureTTLUnlimited _ = assertFailure "expected the actual TTL to be unlimited, but it was limited" + checkTtl _ FeatureTTLUnlimited = assertFailure "expected the actual TTL to be limited, but it was unlimited" + + toMicros :: Word -> Int toMicros secs = fromIntegral secs * 1000000 assertFlagForbidden $ getTeamFeatureFlag @cfg nonMember tid @@ -745,17 +760,20 @@ testSimpleFlagWithLockStatus defaultStatus defaultLockStatus = do setFlagWithGalley :: Public.FeatureStatus -> TestM () setFlagWithGalley statusValue = putTeamFeatureFlagWithGalley @cfg galley owner tid (Public.WithStatusNoLock statusValue (Public.trivialConfig @cfg) Public.FeatureTTLUnlimited) - !!! statusCode === const 200 + !!! statusCode + === const 200 assertSetStatusForbidden :: Public.FeatureStatus -> TestM () assertSetStatusForbidden statusValue = putTeamFeatureFlagWithGalley @cfg galley owner tid (Public.WithStatusNoLock statusValue (Public.trivialConfig @cfg) Public.FeatureTTLUnlimited) - !!! statusCode === const 409 + !!! statusCode + === const 409 setLockStatus :: Public.LockStatus -> TestM () setLockStatus lockStatus = Util.setLockStatusInternal @cfg galley tid lockStatus - !!! statusCode === const 200 + !!! statusCode + === const 200 assertFlagForbidden $ getTeamFeatureFlag @cfg nonMember tid @@ -838,7 +856,8 @@ testSelfDeletingMessages = do galley tid (settingWithoutLockStatus stat tout) - !!! statusCode === const expectedStatusCode + !!! statusCode + === const expectedStatusCode -- internal, public (/team/:tid/features), and team-agnostic (/feature-configs). checkGet :: HasCallStack => FeatureStatus -> Int32 -> Public.LockStatus -> TestM () @@ -856,7 +875,8 @@ testSelfDeletingMessages = do checkSetLockStatus status = do Util.setLockStatusInternal @Public.SelfDeletingMessagesConfig galley tid status - !!! statusCode === const 200 + !!! statusCode + === const 200 -- test that the default lock status comes from `galley.yaml`. -- use this to change `galley.integration.yaml` locally and manually test that conf file @@ -971,7 +991,8 @@ testAllFeatures = do galley <- viewGalley -- this sets the guest links config to its default value thereby creating a row for the team in galley.team_features putTeamFeatureFlagInternal @Public.GuestLinksConfig galley tid (Public.WithStatusNoLock FeatureStatusEnabled Public.GuestLinksConfig Public.FeatureTTLUnlimited) - !!! statusCode === const 200 + !!! statusCode + === const 200 getAllTeamFeatures member tid !!! do statusCode === const 200 responseJsonMaybe === const (Just (expected FeatureStatusEnabled defLockStatus {- determined by default in galley -})) @@ -1060,7 +1081,8 @@ testFeatureNoConfigMultiSearchVisibilityInbound = do r <- getFeatureStatusMulti @Public.SearchVisibilityInboundConfig (Multi.TeamFeatureNoConfigMultiRequest [team1, team2]) - Public.WithStatusNoLock MLSConfig -> TestM () setForTeam wsnl = putTeamFeatureFlagWithGalley @MLSConfig galley owner tid wsnl - !!! statusCode === const 200 + !!! statusCode + === const 200 setForTeamInternal :: HasCallStack => Public.WithStatusNoLock MLSConfig -> TestM () setForTeamInternal wsnl = diff --git a/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs b/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs index ab4302d2df..69b722e4b7 100644 --- a/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs +++ b/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs @@ -1,19 +1,3 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2020 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 . {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} {-# OPTIONS_GHC -Wno-orphans #-} diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index c51bbc9025..577e7bab3b 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -15,6 +15,9 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} +{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} + +{-# HLINT ignore "Use head" #-} module API.Util where @@ -141,13 +144,14 @@ import Wire.API.User.Client.Prekey -- API Operations addPrefix :: Request -> Request -addPrefix r = r {HTTP.path = "v" <> toHeader latestVersion <> "/" <> removeSlash (HTTP.path r)} +addPrefix = addPrefixAtVersion maxBound + +addPrefixAtVersion :: Version -> Request -> Request +addPrefixAtVersion v r = r {HTTP.path = "v" <> toHeader v <> "/" <> 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 @@ -275,9 +279,17 @@ createBindingTeamInternalNoActivate name owner = do tid <- randomId let nt = BindingNewTeam $ newNewTeam (unsafeRange name) DefaultIcon _ <- - put (g . paths ["/i/teams", toByteString' tid] . zUser owner . zConn "conn" . zType "access" . json nt) Text -> UserId -> Currency.Alpha -> TestM TeamId @@ -606,7 +618,18 @@ createTeamConvAccessRaw u tid us name acc role mtimer convRole = do 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 + NewConv + us + [] + (name >>= checked) + (fromMaybe (Set.fromList []) acc) + role + (Just tinfo) + mtimer + Nothing + (fromMaybe roleNameWireAdmin convRole) + ProtocolProteusTag + Nothing post ( g . path "/conversations" @@ -673,7 +696,18 @@ createOne2OneTeamConv :: UserId -> UserId -> Maybe Text -> TeamId -> TestM Respo createOne2OneTeamConv u1 u2 n tid = do g <- viewGalley let conv = - NewConv [u2] [] (n >>= checked) mempty Nothing (Just $ ConvTeamInfo tid) Nothing Nothing roleNameWireAdmin ProtocolProteusTag Nothing + 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 postConv :: @@ -729,7 +763,19 @@ 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 <- viewGalley - let conv = NewConv us [] (name >>= checked) (Set.fromList a) r (Just (ConvTeamInfo tid)) mtimer Nothing roleNameWireAdmin ProtocolProteusTag Nothing + 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 deleteTeamConv :: (HasGalley m, MonadIO m, MonadHttp m) => TeamId -> ConvId -> UserId -> m ResponseLBS @@ -766,7 +812,19 @@ 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 <- viewGalley - let conv = NewConv us [] (name >>= checked) (Set.fromList a) r Nothing mtimer (Just rcpt) roleNameWireAdmin ProtocolProteusTag Nothing + 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 @@ -777,7 +835,19 @@ postSelfConv u = do postO2OConv :: UserId -> UserId -> Maybe Text -> TestM ResponseLBS postO2OConv u1 u2 n = do g <- viewGalley - let conv = NewConv [u2] [] (n >>= checked) mempty Nothing Nothing Nothing Nothing roleNameWireAdmin ProtocolProteusTag Nothing + 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 @@ -1018,6 +1088,20 @@ getConv u c = do . zConn "conn" . zType "access" +getGlobalTeamConv :: + (MonadIO m, MonadHttp m, HasGalley m, HasCallStack) => + UserId -> + TeamId -> + m ResponseLBS +getGlobalTeamConv u tid = do + g <- viewGalley + get $ + g + . paths ["teams", toByteString' tid, "conversations", "global"] + . zUser u + . zConn "conn" + . zType "access" + getConvQualified :: (MonadIO m, MonadHttp m, HasGalley m, HasCallStack) => UserId -> Qualified ConvId -> m ResponseLBS getConvQualified u (Qualified conv domain) = do g <- viewGalley @@ -1030,7 +1114,8 @@ getConvQualified u (Qualified conv domain) = do getConvIds :: UserId -> Maybe (Either [ConvId] ConvId) -> Maybe Int32 -> TestM ResponseLBS getConvIds u r s = do - g <- viewGalley + -- The endpoint is removed starting V3 + g <- fmap (addPrefixAtVersion V2 .) (view tsUnversionedGalley) get $ g . path "/conversations/ids" @@ -1048,6 +1133,15 @@ listConvIds u paginationOpts = do . zUser u . json paginationOpts +listConvIdsV2 :: UserId -> GetPaginatedConversationIds -> TestM ResponseLBS +listConvIdsV2 u paginationOpts = do + g <- fmap (addPrefixAtVersion V2 .) (view tsUnversionedGalley) + post $ + g + . path "/conversations/list-ids" + . zUser u + . json paginationOpts + -- | Does not page through conversation list listRemoteConvs :: Domain -> UserId -> TestM [Qualified ConvId] listRemoteConvs remoteDomain uid = do @@ -1656,6 +1750,18 @@ wsAssertClientRemoved cid n = do etype @?= Just "user.client-remove" fmap ClientId eclient @?= Just cid +wsAssertClientAdded :: + HasCallStack => + ClientId -> + Notification -> + IO () +wsAssertClientAdded 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-add" + fmap ClientId eclient @?= Just cid + assertMLSMessageEvent :: HasCallStack => Qualified ConvId -> @@ -1830,6 +1936,9 @@ decodeQualifiedConvIdList = fmap mtpResults . responseJsonEither @ConvIdsPage zUser :: UserId -> Request -> Request zUser = header "Z-User" . toByteString' +zClient :: ClientId -> Request -> Request +zClient = header "Z-Client" . toByteString' + zBot :: UserId -> Request -> Request zBot = header "Z-Bot" . toByteString' @@ -2856,6 +2965,14 @@ wsAssertConvReceiptModeUpdate conv usr new n = do evtFrom e @?= usr evtData e @?= EdConvReceiptModeUpdate (ConversationReceiptModeUpdate new) +wsAssertBackendRemoveProposalWithEpoch :: HasCallStack => Qualified UserId -> Qualified ConvId -> KeyPackageRef -> Epoch -> Notification -> IO ByteString +wsAssertBackendRemoveProposalWithEpoch fromUser convId kpref epoch n = do + bs <- wsAssertBackendRemoveProposal fromUser convId kpref n + let msg = fromRight (error "Failed to parse Message 'MLSPlaintext") $ decodeMLS' @(Message 'MLSPlainText) bs + let tbs = rmValue . msgTBS $ msg + tbsMsgEpoch tbs @?= epoch + pure bs + wsAssertBackendRemoveProposal :: HasCallStack => Qualified UserId -> Qualified ConvId -> KeyPackageRef -> Notification -> IO ByteString wsAssertBackendRemoveProposal fromUser convId kpref n = do let e = List1.head (WS.unpackPayload n) diff --git a/services/galley/test/unit/Test/Galley/Intra/User.hs b/services/galley/test/unit/Test/Galley/Intra/User.hs index c6bec86487..1138e79f0a 100644 --- a/services/galley/test/unit/Test/Galley/Intra/User.hs +++ b/services/galley/test/unit/Test/Galley/Intra/User.hs @@ -20,7 +20,6 @@ module Test.Galley.Intra.User where --- import Debug.Trace (traceShow) import Galley.Intra.User (chunkify) import Imports import Test.QuickCheck diff --git a/services/gundeck/default.nix b/services/gundeck/default.nix index 6b191b5027..3b4c5dc779 100644 --- a/services/gundeck/default.nix +++ b/services/gundeck/default.nix @@ -2,24 +2,93 @@ # This file is generated by running hack/bin/generate-local-nix-packages.sh and # must be regenerated whenever local packages are added or removed, or # dependencies are added or removed. -{ mkDerivation, aeson, aeson-pretty, amazonka, amazonka-sns -, amazonka-sqs, async, attoparsec, auto-update, base -, base16-bytestring, bilge, bytestring, bytestring-conversion -, cassandra-util, containers, criterion, data-default, errors -, exceptions, extended, extra, gitignoreSource, gundeck-types -, hedis, HsOpenSSL, http-client, http-client-tls, http-types -, imports, kan-extensions, lens, lens-aeson, lib, metrics-core -, metrics-wai, MonadRandom, mtl, multiset, network, network-uri -, optparse-applicative, psqueues, QuickCheck, quickcheck-instances -, quickcheck-state-machine, random, raw-strings-qq, resourcet -, retry, safe, safe-exceptions, scientific, servant, servant-server -, servant-swagger, servant-swagger-ui, streaming-commons -, string-conversions, swagger, swagger2, tagged, tasty, tasty-hunit -, tasty-quickcheck, text, time, tinylog, tls, tree-diff -, types-common, types-common-aws, unix, unliftio -, unordered-containers, uuid, wai, wai-extra, wai-middleware-gunzip -, wai-predicates, wai-routing, wai-utilities, warp, warp-tls -, websockets, wire-api, yaml +{ mkDerivation +, aeson +, aeson-pretty +, amazonka +, amazonka-sns +, amazonka-sqs +, async +, attoparsec +, auto-update +, base +, base16-bytestring +, bilge +, bytestring +, bytestring-conversion +, cassandra-util +, containers +, criterion +, data-default +, errors +, exceptions +, extended +, extra +, gitignoreSource +, gundeck-types +, hedis +, HsOpenSSL +, http-client +, http-client-tls +, http-types +, imports +, kan-extensions +, lens +, lens-aeson +, lib +, metrics-core +, metrics-wai +, MonadRandom +, mtl +, multiset +, network +, network-uri +, optparse-applicative +, psqueues +, QuickCheck +, quickcheck-instances +, quickcheck-state-machine +, random +, raw-strings-qq +, resourcet +, retry +, safe +, safe-exceptions +, scientific +, servant +, servant-server +, servant-swagger +, servant-swagger-ui +, streaming-commons +, string-conversions +, swagger +, swagger2 +, tagged +, tasty +, tasty-hunit +, tasty-quickcheck +, text +, time +, tinylog +, tls +, tree-diff +, types-common +, types-common-aws +, unix +, unliftio +, unordered-containers +, uuid +, wai +, wai-extra +, wai-middleware-gunzip +, wai-predicates +, wai-routing +, wai-utilities +, warp +, warp-tls +, websockets +, wire-api +, yaml }: mkDerivation { pname = "gundeck"; @@ -28,41 +97,169 @@ mkDerivation { isLibrary = true; isExecutable = true; libraryHaskellDepends = [ - aeson amazonka amazonka-sns amazonka-sqs async attoparsec - auto-update base bilge bytestring bytestring-conversion - cassandra-util containers data-default errors exceptions extended - extra gundeck-types hedis HsOpenSSL http-client http-client-tls - http-types imports lens lens-aeson metrics-core metrics-wai mtl - network network-uri optparse-applicative psqueues resourcet retry - safe-exceptions servant servant-server servant-swagger - servant-swagger-ui swagger swagger2 text time tinylog tls - types-common types-common-aws unliftio unordered-containers uuid - wai wai-extra wai-middleware-gunzip wai-predicates wai-routing - wai-utilities wire-api yaml + aeson + amazonka + amazonka-sns + amazonka-sqs + async + attoparsec + auto-update + base + bilge + bytestring + bytestring-conversion + cassandra-util + containers + data-default + errors + exceptions + extended + extra + gundeck-types + hedis + HsOpenSSL + http-client + http-client-tls + http-types + imports + lens + lens-aeson + metrics-core + metrics-wai + mtl + network + network-uri + optparse-applicative + psqueues + resourcet + retry + safe-exceptions + servant + servant-server + servant-swagger + servant-swagger-ui + swagger + swagger2 + text + time + tinylog + tls + types-common + types-common-aws + unliftio + unordered-containers + uuid + wai + wai-extra + wai-middleware-gunzip + wai-predicates + wai-routing + wai-utilities + wire-api + yaml ]; executableHaskellDepends = [ - aeson async base base16-bytestring bilge bytestring - bytestring-conversion cassandra-util containers exceptions extended - gundeck-types HsOpenSSL http-client http-client-tls imports - kan-extensions lens lens-aeson metrics-wai mtl network network-uri - optparse-applicative random raw-strings-qq retry safe - streaming-commons tagged tasty tasty-hunit text time tinylog - types-common unix unordered-containers uuid wai wai-utilities warp - warp-tls websockets wire-api yaml + aeson + async + base + base16-bytestring + bilge + bytestring + bytestring-conversion + cassandra-util + containers + exceptions + extended + gundeck-types + HsOpenSSL + http-client + http-client-tls + imports + kan-extensions + lens + lens-aeson + metrics-wai + mtl + network + network-uri + optparse-applicative + random + raw-strings-qq + retry + safe + streaming-commons + tagged + tasty + tasty-hunit + text + time + tinylog + types-common + unix + unordered-containers + uuid + wai + wai-utilities + warp + warp-tls + websockets + wire-api + yaml ]; testHaskellDepends = [ - aeson aeson-pretty amazonka async base bytestring containers - exceptions extended gundeck-types HsOpenSSL imports lens - metrics-wai MonadRandom mtl multiset network-uri QuickCheck - quickcheck-instances quickcheck-state-machine scientific - string-conversions tasty tasty-hunit tasty-quickcheck text time - tinylog tree-diff types-common unordered-containers uuid - wai-utilities wire-api + aeson + aeson-pretty + amazonka + async + base + bytestring + containers + exceptions + extended + gundeck-types + HsOpenSSL + imports + lens + metrics-wai + MonadRandom + mtl + multiset + network-uri + QuickCheck + quickcheck-instances + quickcheck-state-machine + scientific + string-conversions + tasty + tasty-hunit + tasty-quickcheck + text + time + tinylog + tree-diff + types-common + unordered-containers + uuid + wai-utilities + wire-api ]; benchmarkHaskellDepends = [ - aeson amazonka base bytestring criterion extended gundeck-types - HsOpenSSL imports lens random text time types-common - unordered-containers uuid + aeson + amazonka + base + bytestring + criterion + extended + gundeck-types + HsOpenSSL + imports + lens + random + text + time + types-common + unordered-containers + uuid ]; description = "Push Notification Hub"; license = lib.licenses.agpl3Only; diff --git a/services/nginz/Dockerfile b/services/nginz/Dockerfile deleted file mode 100644 index e608fe9624..0000000000 --- a/services/nginz/Dockerfile +++ /dev/null @@ -1,154 +0,0 @@ -# Requires docker >= 17.05 (requires support for multi-stage builds) -FROM alpine:3.15 as libzauth-builder - -# Compile libzauth -COPY libs/libzauth /src/libzauth -RUN cd /src/libzauth/libzauth-c \ - && apk add --no-cache make bash cargo libsodium-dev \ - && make install - -# Nginz container -FROM alpine:3.15 - -# Install libzauth -COPY --from=libzauth-builder /usr/local/include/zauth.h /usr/local/include/zauth.h -COPY --from=libzauth-builder /usr/local/lib/libzauth.so /usr/local/lib/libzauth.so -COPY --from=libzauth-builder /usr/local/lib/pkgconfig/libzauth.pc /usr/local/lib/pkgconfig/libzauth.pc - -COPY services/nginz/third_party /src/third_party - -ENV CONFIG --prefix=/etc/nginx \ - --sbin-path=/usr/sbin/nginx \ - --modules-path=/usr/lib/nginx/modules \ - --conf-path=/etc/nginx/nginx.conf \ - --error-log-path=/var/log/nginx/error.log \ - --http-log-path=/var/log/nginx/access.log \ - --pid-path=/var/run/nginx.pid \ - --lock-path=/var/run/nginx.lock \ - --http-client-body-temp-path=/var/cache/nginx/client_temp \ - --http-proxy-temp-path=/var/cache/nginx/proxy_temp \ - --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \ - --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \ - --http-scgi-temp-path=/var/cache/nginx/scgi_temp \ - --user=nginx \ - --group=nginx \ - --with-http_ssl_module \ - --with-http_v2_module \ - --with-http_stub_status_module \ - --with-http_realip_module \ - --with-http_gunzip_module \ - --add-module=/src/third_party/nginx-zauth-module \ - --add-module=/src/third_party/headers-more-nginx-module \ - --add-module=/src/third_party/nginx-module-vts - -################# similar block as upstream ######################################## -# see https://github.com/nginxinc/docker-nginx/blob/master/stable/alpine/Dockerfile -# This uses dockerfile logic from before 1.16 -#################################################################################### - -ENV NGINX_VERSION 1.22.1 - -RUN apk update - -RUN apk add -vv --virtual .build-deps \ - libsodium-dev \ - llvm-libunwind-dev \ - gcc \ - libc-dev \ - make \ - openssl-dev \ - pcre-dev \ - zlib-dev \ - linux-headers \ - curl \ - gnupg1 \ - libxslt-dev \ - gd-dev \ - geoip-dev - -# This line checks whether the 'apk add' succeeded, sometimes it doesn't work. -RUN curl -h - -RUN set -x \ - && addgroup -g 101 -S nginx \ - && adduser -S -D -H -u 101 -h /var/cache/nginx -s /sbin/nologin -G nginx -g nginx nginx \ - && export GPG_KEYS=13C82A63B603576156E30A4EA0EA981B66B0D967 \ - && curl -fSL https://nginx.org/download/nginx-$NGINX_VERSION.tar.gz -o nginx.tar.gz \ - && curl -fSL https://nginx.org/download/nginx-$NGINX_VERSION.tar.gz.asc -o nginx.tar.gz.asc \ - && found=''; \ - for server in \ - ha.pool.sks-keyservers.net \ - hkp://keyserver.ubuntu.com:80 \ - hkp://p80.pool.sks-keyservers.net:80 \ - pgp.mit.edu \ - ; do \ - echo "Fetching GPG key $GPG_KEYS from $server"; \ - gpg --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$GPG_KEYS" && found=yes && break; \ - done; \ - test -z "$found" && echo >&2 "error: failed to fetch GPG key $GPG_KEYS" && exit 1; \ - gpg --batch --verify nginx.tar.gz.asc nginx.tar.gz \ - && rm -rf "$GNUPGHOME" nginx.tar.gz.asc \ - && mkdir -p /usr/src \ - && tar -zxC /usr/src -f nginx.tar.gz \ - && rm nginx.tar.gz \ - && cd /usr/src/nginx-$NGINX_VERSION \ - && ./configure $CONFIG --with-debug \ - && make -j$(getconf _NPROCESSORS_ONLN) \ - && mv objs/nginx objs/nginx-debug \ - && ./configure $CONFIG \ - && make -j$(getconf _NPROCESSORS_ONLN) \ - && make install \ - && rm -rf /etc/nginx/html/ \ - && mkdir /etc/nginx/conf.d/ \ - && mkdir -p /usr/share/nginx/html/ \ - && install -m644 html/index.html /usr/share/nginx/html/ \ - && install -m644 html/50x.html /usr/share/nginx/html/ \ - && install -m755 objs/nginx-debug /usr/sbin/nginx-debug \ - && ln -s ../../usr/lib/nginx/modules /etc/nginx/modules \ - && strip /usr/sbin/nginx* \ - && rm -rf /usr/src/nginx-$NGINX_VERSION \ - \ - # Bring in gettext so we can get `envsubst`, then throw - # the rest away. To do this, we need to install `gettext` - # then move `envsubst` out of the way so `gettext` can - # be deleted completely, then move `envsubst` back. - && apk add --no-cache --virtual .gettext gettext \ - && mv /usr/bin/envsubst /tmp/ \ - \ - && runDepsTmp="$( \ - scanelf --needed --nobanner --format '%n#p' /usr/sbin/nginx /usr/lib/nginx/modules/*.so /tmp/envsubst \ - | tr ',' '\n' \ - | sort -u \ - | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ - )" \ - \ - # exclude libzauth from runDeps - && runDeps=${runDepsTmp/so:libzauth.so/''} \ - && apk add --no-cache --virtual .nginx-rundeps $runDeps \ - && apk del .build-deps \ - && apk del .gettext \ - && mv /tmp/envsubst /usr/local/bin/ \ - \ - # Bring in tzdata so users could set the timezones through the environment - # variables - && apk add --no-cache tzdata \ - \ - # forward request and error logs to docker log collector - && ln -sf /dev/stdout /var/log/nginx/access.log \ - && ln -sf /dev/stderr /var/log/nginx/error.log - -################# wire/nginz specific ###################### - -# Fix file permissions -RUN mkdir -p /var/cache/nginx/client_temp && chown -R nginx:nginx /var/cache/nginx - -RUN apk add --no-cache inotify-tools dumb-init bash curl && \ - # add libzauth runtime dependencies back in - apk add --no-cache libsodium llvm-libunwind libgcc && \ - # add openssl runtime dependencies for TLS/SSL certificate support - apk add --no-cache openssl - -COPY services/nginz/nginz_reload.sh /usr/bin/nginz_reload.sh - -ENTRYPOINT ["/usr/bin/dumb-init", "--"] -CMD ["/usr/bin/nginz_reload.sh", "-g", "daemon off;", "-c", "/etc/wire/nginz/conf/nginx.conf"] diff --git a/services/nginz/Makefile b/services/nginz/Makefile deleted file mode 100644 index 1803bdb8a6..0000000000 --- a/services/nginz/Makefile +++ /dev/null @@ -1,133 +0,0 @@ -LANG := en_US.UTF-8 -SHELL := /usr/bin/env bash -NAME := nginz -NGINX_VERSION = 1.22.1 -NGINZ_VERSION ?= -SWAGGER_VERSION:= 2.2.10 -SHELL := /usr/bin/env bash -DIST := build -BIN := src/objs/nginx -ifeq ($(DEBUG), 1) -WITH_DEBUG = --with-debug -endif -DOCKER_REGISTRY ?= quay.io -DOCKER_USER ?= quay.io/wire -DOCKER_TAG ?= local - -DEST_PATH ?= /opt/nginz -# Use a folder that can be written to since errors during startup do not respect -# your config and will use the `LOG_PATH` defined here -LOG_PATH ?= /var/log/nginz -CONF_PATH ?= /etc/nginz -PID_PATH ?= /var/run - -# You may need to use this if you have some dependencies in non-standard -# locations. For macOS, we use Brew default directories for OpenSSL (if they -# exist). These variables can be always overridden when running the -# Makefile, though. -ifeq ($(wildcard /usr/local/opt/openssl/.),) - EXTRA_CC_INC ?= - EXTRA_CC_LIB ?= -else - EXTRA_CC_INC ?= -I/usr/local/opt/openssl/include - EXTRA_CC_LIB ?= -L/usr/local/opt/openssl/lib -endif - -# Where should we look for packages, locally or globally? -EXTRA_PKG_PATH := $(shell [ -w /usr/local ] && echo /usr/local || echo "$(HOME)/.wire-dev")/lib/pkgconfig - -CONFIG_OPTIONS = \ - --prefix=$(DEST_PATH) \ - $(WITH_DEBUG) \ - --with-cc-opt="-std=gnu99 $(EXTRA_CC_INC)" \ - --with-ld-opt="$(EXTRA_CC_LIB)" \ - --error-log-path=$(LOG_PATH)/error.log \ - --http-log-path=$(LOG_PATH)/access.log \ - --conf-path=$(CONF_PATH)/nginx.conf \ - --pid-path=$(PID_PATH) - -ADDITIONAL_MODULES = \ - --with-http_ssl_module \ - --with-http_v2_module \ - --with-http_stub_status_module \ - --with-http_realip_module \ - --with-http_gunzip_module \ - --add-module=../third_party/nginx-zauth-module \ - --add-module=../third_party/headers-more-nginx-module \ - --add-module=../third_party/nginx-module-vts - -guard-%: - @ if [ "${${*}}" = "" ]; then \ - echo "Environment variable $* not set"; \ - exit 1; \ - fi - -default: compile - -.PHONY: clean -clean: - -rm -rf src $(DIST) .metadata zwagger-ui/swagger-ui - -.PHONY: compile -compile: $(BIN) - mkdir -p ../../dist - cp src/objs/nginx ../../dist/ - -$(BIN): src zwagger-ui/swagger-ui integration-test/conf/nginz/zwagger-ui - PKG_CONFIG_PATH=$(EXTRA_PKG_PATH) pkg-config --exists libzauth || { echo -e "\n\033[0;31m The 'libzauth' library was not found\033[0m\n pkg-config path = $(EXTRA_PKG_PATH)\n\n Attempting to install it...\n" && $(MAKE) libzauth; } - git submodule update --init - (cd src; PKG_CONFIG_PATH=$(EXTRA_PKG_PATH) ./configure $(CONFIG_OPTIONS) $(ADDITIONAL_MODULES)) - make -C src - -$(DIST): - mkdir -p $(DIST) - -# -# Dependencies -# - -NGINX_BUNDLE=nginx-$(NGINX_VERSION).tar.gz -SWAGGER_BUNDLE=swagger-$(SWAGGER_VERSION).tar.gz - -.PHONY: integration-test/conf/nginz/zwagger-ui -integration-test/conf/nginz/zwagger-ui: zwagger-ui/swagger-ui - cp -r "zwagger-ui/." integration-test/conf/nginz/zwagger-ui/ - -.PHONY: zwagger-ui/swagger-ui -zwagger-ui/swagger-ui: $(SWAGGER_BUNDLE) - tar zxf $(SWAGGER_BUNDLE) - rm -rf zwagger-ui/swagger-ui - mv -v swagger-ui-$(SWAGGER_VERSION)/dist zwagger-ui/swagger-ui - touch zwagger-ui/swagger-ui - rm -rf swagger-ui-$(SWAGGER_VERSION) - -$(SWAGGER_BUNDLE): - curl -L https://github.com/swagger-api/swagger-ui/archive/v$(SWAGGER_VERSION).tar.gz -o $(SWAGGER_BUNDLE) - -src: $(NGINX_BUNDLE) - #Find keys on https://nginx.org/en/pgp_keys.html - gpg --verify $(NGINX_BUNDLE).asc $(NGINX_BUNDLE) - tar zxf $(NGINX_BUNDLE) - rm -rf src && mv nginx-$(NGINX_VERSION) src - -$(NGINX_BUNDLE): - curl -O https://nginx.org/download/$(NGINX_BUNDLE).asc - curl -O https://nginx.org/download/$(NGINX_BUNDLE) - -.PHONY: docker -docker: - git submodule update --init - docker build -t $(DOCKER_USER)/nginz:$(DOCKER_TAG) -f Dockerfile ../.. - docker tag $(DOCKER_USER)/nginz:$(DOCKER_TAG) $(DOCKER_USER)/nginz:latest - if test -n "$$DOCKER_PUSH"; then docker login $(DOCKER_REGISTRY); docker push $(DOCKER_USER)/nginz:$(DOCKER_TAG); docker push $(DOCKER_USER)/nginz:latest; fi; - -.PHONY: libzauth -libzauth: - $(MAKE) -C ../../libs/libzauth install - -# a target to start the locally-compiled docker image (tagged 'local') -# using the configuration in wire-server/deploy/services-demo -# can aid when updating nginx versions and configuration -.PHONY: docker-run-demo-local -docker-run-demo: - docker run --network=host -it -v $$(pwd)/../../deploy/services-demo:/configs --entrypoint /usr/sbin/nginx quay.io/wire/nginz:local -p /configs -c /configs/conf/nginz/nginx-docker.conf diff --git a/services/nginz/README.md b/services/nginz/README.md deleted file mode 100644 index fb1832268a..0000000000 --- a/services/nginz/README.md +++ /dev/null @@ -1,76 +0,0 @@ -# NGINX build with extra modules - -## Compile natively - -To build nginz natively, you will need to have the usual C compiler toolchains installed, along with the following dependencies: - -* gpg (needed to verify nginx's signatures) -* openssl -* libossp-uuid -* libpcre3 -* [libzauth](../../libs/libzauth) - * depends on the rust compiler, libsodium23 - -### Alpine -If you're on alpine, see the [Dockerfile](Dockerfile) for the precise dependency names. - -### Ubuntu / Debian (backports / testing / unstable) - -_Note_: Debian packages are only used as part of wire's infrastructure, and as such, you do not need to install them to run the integration tests or the demo. - -_Note_: Debian stable does not contain a new enough version of libsodium. you must get it from backports, testing, or unstable. - -_Note_: On some Ubuntu versions, upstart is installed in addition to systemd, causing runit to fail with an error like "Unable to connect to Upstart: Failed to connect to socket". Luckily, there is [a simple fix](https://forum.peppermintos.com/index.php?topic=5210.0). - -#### Build Dependencies: -```bash -sudo apt install libossp-uuid-dev libpcre3-dev libsodium23 runit gnupg -``` - -#### Building -```bash -make -``` - -### Compile with docker - -`make docker` - -### Generic -If you're on another platform, the names of the dependencies might differ slightly. - -Once you have all necessary dependencies, `make` in this directory should work. - -## Common problems while compiling - -``` -gpg: Can't check signature: public key not found -``` - -This means that you haven't imported the public key that was used to sign nginx. Look for the keys at https://nginx.org/en/pgp_keys.html and make sure to import ALL of them with: - -`gpg --import ` - -Alternatively, you can ask GPG to find the key by its ID (printed in the error message): - -`gpg --recv-keys KEY_ID` - ---- - -``` -checking for OpenSSL library ... not found -[...] -./configure: error: SSL modules require the OpenSSL library. -You can either do not enable the modules, or install the OpenSSL library -into the system, or build the OpenSSL library statically from the source -with nginx by using --with-openssl= option. -``` - -openssl is required to compile nginx and it may be installed in a "non-standard" path in your system. Once you are sure you have installed it, look for `EXTRA_CC_INC` and `EXTRA_CC_LIB` in the `Makefile` and point them to the correct location in your system. - -If you are using macOS and you used `brew` to install openssl, the `Makefile` already contains the right paths so you should not be seeing that error. - -## How to run it - -Have a look at our demo config in [./integration-test/conf/nginz/](./integration-test/conf/nginz/) - diff --git a/services/nginz/integration-test/conf/nginz/nginx.conf b/services/nginz/integration-test/conf/nginz/nginx.conf index 10e7a546ab..24155e2bbd 100644 --- a/services/nginz/integration-test/conf/nginz/nginx.conf +++ b/services/nginz/integration-test/conf/nginz/nginx.conf @@ -497,25 +497,7 @@ http { include common_response_with_zauth.conf; proxy_pass http://spar; } - - # Stern Endpoints - - # We add a `/stern` suffix to the URL to resolve clashes with non-Stern endpoints. - rewrite ^/backoffice/api-docs/stern /stern/api-docs?base_url=http://127.0.0.1:8080/stern/ break; - - location /stern/api-docs { - include common_response_no_zauth.conf; - # We don't use an `upstream` for stern, since running stern is optional. - proxy_pass http://127.0.0.1:8091; - } - - location /stern { - include common_response_no_zauth.conf; - # We don't use an `upstream` for stern, since running stern is optional. - # The trailing slash matters, as it makes sure the `/stern` prefix is removed. - proxy_pass http://127.0.0.1:8091/; - } - + # # Swagger Resource Listing # diff --git a/services/nginz/integration-test/conf/nginz/zwagger-ui/backoffice/api-docs/resources.json b/services/nginz/integration-test/conf/nginz/zwagger-ui/backoffice/api-docs/resources.json deleted file mode 100644 index db64e09127..0000000000 --- a/services/nginz/integration-test/conf/nginz/zwagger-ui/backoffice/api-docs/resources.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "Version": "1.0", - "swaggerVersion": "1.2", - "apis": [ - { - "path": "/stern", - "description": "Back Office" - } - ], - "info": { - "description": "The Back Office can only be used if Stern is running. It usually shouldn't be running, and if it is, make sure it can only be reached by admins, as it allows unauthorized access to endpoints. For more details see `tools/stern/README.md` in the `wire-server` repository." - } -} diff --git a/services/nginz/integration-test/conf/nginz/zwagger-ui/index.html b/services/nginz/integration-test/conf/nginz/zwagger-ui/index.html index 921da15b8c..2409c26180 100644 --- a/services/nginz/integration-test/conf/nginz/zwagger-ui/index.html +++ b/services/nginz/integration-test/conf/nginz/zwagger-ui/index.html @@ -43,7 +43,6 @@
-
@@ -54,9 +53,6 @@ - -