diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 707b353c19..bb50982db8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,8 +11,8 @@ jobs: - uses: actions/checkout@v2 with: submodules: true - - uses: cachix/install-nix-action@v14.1 - - uses: cachix/cachix-action@v10 + - uses: cachix/install-nix-action@v20 + - uses: cachix/cachix-action@v12 with: name: wire-server signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}' @@ -32,8 +32,8 @@ jobs: - uses: actions/checkout@v2 with: submodules: true - - uses: cachix/install-nix-action@v14.1 - - uses: cachix/cachix-action@v10 + - uses: cachix/install-nix-action@v20 + - uses: cachix/cachix-action@v12 with: name: wire-server signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}' diff --git a/.gitignore b/.gitignore index 413128240b..6ebb996e24 100644 --- a/.gitignore +++ b/.gitignore @@ -59,9 +59,6 @@ spar.integration-aws.yaml integration-aws.yaml DOCKER_ID* swagger-ui -!charts/nginz/static/swagger-ui - -services/nginz/integration-test/conf/nginz/zwagger-ui/* deploy/dockerephemeral/build/airdock_base-all/ deploy/dockerephemeral/build/airdock_base/ @@ -112,3 +109,6 @@ result-* /integration-ca-key.pem /integration-ca.pem + +services/nginz/third_party/headers-more-nginx-module +services/nginz/third_party/nginx-module-vts diff --git a/CHANGELOG.md b/CHANGELOG.md index f5014c421e..b186828f8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,116 @@ +# [2023-03-06] (Chart Release 4.32.0) + +## Release notes + + +* In (the unlikely) case your server config file contains `setWhitelist:`, you need to change this before the upgrade! It used to refer to a whitelisting service, which is now replaced with a local list of allowed domains and phone numbers. See [docs](https://docs.wire.com/developer/reference/user/activation.html?highlight=whitelist#phone-email-whitelist) for details. Migration path: add new config fields; upgrade, remove old config fields. (#3043) + +* The coturn Helm chart has been promoted to *beta* level stability. (#3078) + + +## API changes + + +* API v3 is now supported. The new MLS endpoints introduced in API v3 have been removed, and are now only available under v4. (#3122) + + +## Features + + +* Add internal endpoints of `cargohold`, `galley`, `legalhold` and `spar` to the Swagger docs for internal endpoints. (#3007) + +* The coturn container image included in the coturn Helm chart was updated to + version `4.6.0-wireapp.4`. + + With this version of coturn, the Prometheus metrics endpoint has been + updated, and the `turn_active_allocations` metric label has been *renamed* to + `turn_total_allocations`. (#3078) + +* Better error message for invalid ID in a credential when uploading MLS key packages (#3102) + +* Add Swagger documentation for internal endpoints. It's reachable at the path `/v/api-internal/swagger{-ui,.json}`. (#3003) + +* Render one Swagger page per internal endpoint. This superseeds the previous Swagger docs page for all internal endpoints. (#3094) + +* Feature flag for Outlook calendar integration (#3025) + +* Team feature setting for MLS end-to-end identity was added and server setting `setEnableMls` is exposed via new authorized endpoint `GET /system/settings` (#3082) + + +## Bug fixes and other updates + + +* The container image used for handling online TLS certificate updates in the + coturn Helm chart was updated to a version with metadata compatible with + containerd. (#3078) + +* Fix a bug in the helm chart's nginx-ingress-services / federator Ingress resource introduced in the last release. (#3034) + +* Typing indicators not working accross federated backends (#3118) + + +## Documentation + + +* Extend the docs on the federation error type (#3045) + +* Update SAML/SCIM docs (#3038) + + +## Internal changes + + +* - use exponential backoff for retrying requests to Amazon + - also retry in case of server-side rate limiting by Amazon (#3121) + +* Make brig-schema a little faster by merging the first 34 schema migrations on fresh installations. (#3099) + +* Deflake integration test: metrics (#3053) + +* Document in code a function that sends remote Proteus messages (#PR_NOT_FOUND) + +* Lower the log level of federator inotify (#3056) + +* use Wai's settings for graceful shutdown (#3069) + +* CI integration setup time should be reduced: tweak the way cassandra-ephemeral is started (#3052) + +* charts: Mark all service/secret/configmap test resources to be re-created by defining them as helm hooks (#3037, #3049) + +* New integration test script with support for running end2end tests locally (#3062) + +* Bump nixpkgs to latest commit on nixpkgs-unstable branch (#3084) + +* Add config to allow to run helm tests for different services in parallel; improve integration test output logs (#3040) + +* Run brig and galley integration tests concurrently (#2825) + +* Add wrapper for bitnami/postgresql chart. (#3012) + +* Branch on performAction tags for finer-grained CallsFed constraints (#3030) + +* Fixed broken stern endpoint `POST i/user/meta-info` (#3035) + +* Make stern fail on startup if supported backend api version needs bumping (#3035) + +* Automatically track CallsFed constraints via a GHC plugin (#3083) + +* Rust library `rusty-jwt-tools` upgraded to latest version (#3112) + +* Enabling warnings for redundant constraints and removing the redundant + constraints. (#3009) + +* Migrate `/teams/notifications` to use the Servant library. (#3020) + +* Split polysemy `Members` constraints into multiple `Member` constraints (#3093) + + +## Federation changes + + +* Use `HsOpenSSL` instead of `tls` for federation communication. (#3051) + + # [2023-01-26] (Chart Release 4.31.0) ## Release notes diff --git a/Makefile b/Makefile index 28ba398972..21bd392ad4 100644 --- a/Makefile +++ b/Makefile @@ -18,8 +18,9 @@ fake-aws fake-aws-s3 fake-aws-sqs aws-ingress fluent-bit kibana backoffice \ calling-test demo-smtp elasticsearch-curator elasticsearch-external \ elasticsearch-ephemeral minio-external cassandra-external \ nginx-ingress-controller nginx-ingress-services reaper sftd restund coturn \ -inbucket k8ssandra-test-cluster +inbucket k8ssandra-test-cluster postgresql KIND_CLUSTER_NAME := wire-server +HELM_PARALLELISM ?= 1 # 1 for sequential tests; 6 for all-parallel tests package ?= all EXE_SCHEMA := ./dist/$(package)-schema @@ -90,6 +91,15 @@ endif ci: c db-migrate ./hack/bin/cabal-run-integration.sh $(package) +.PHONY: sanitize-pr +sanitize-pr: + ./hack/bin/generate-local-nix-packages.sh + make formatf-all + make hlint-inplace-all + make git-add-cassandra-schema + @git diff-files --quiet -- || ( echo "There are unstaged changes, please take a look, consider committing them, and try again."; exit 1 ) + @git diff-index --quiet --cached HEAD -- || ( echo "There are staged changes, please take a look, consider committing them, and try again."; exit 1 ) + .PHONY: cabal-fmt cabal-fmt: ./hack/bin/cabal-fmt.sh $(package) @@ -235,11 +245,11 @@ db-migrate-package: # Usage: # -# Reset all keyspaces -# make db-reset +# Migrate all keyspaces and reset the ES index +# make db-migrate # -# Reset keyspace for only one service, say galley: -# make db-reset package=galley +# Migrate keyspace for only one service, say galley: +# make db-migrate package=galley .PHONY: db-reset db-reset: c @echo "Make sure you have ./deploy/dockerephemeral/run.sh running in another window!" @@ -248,31 +258,42 @@ ifeq ($(package), all) ./dist/galley-schema --keyspace galley_test --replication-factor 1 --reset ./dist/gundeck-schema --keyspace gundeck_test --replication-factor 1 --reset ./dist/spar-schema --keyspace spar_test --replication-factor 1 --reset +ifeq ($(INTEGRATION_FEDERATION_TESTS), 1) + ./dist/brig-schema --keyspace brig_test2 --replication-factor 1 --reset + ./dist/galley-schema --keyspace galley_test2 --replication-factor 1 --reset + ./dist/gundeck-schema --keyspace gundeck_test2 --replication-factor 1 --reset + ./dist/spar-schema --keyspace spar_test2 --replication-factor 1 --reset +endif else $(EXE_SCHEMA) --keyspace $(package)_test --replication-factor 1 --reset +ifeq ($(INTEGRATION_FEDERATION_TESTS), 1) + $(EXE_SCHEMA) --keyspace $(package)_test2 --replication-factor 1 --reset +endif endif + ./dist/brig-index reset --elasticsearch-index directory_test --elasticsearch-server http://localhost:9200 > /dev/null + ./dist/brig-index reset --elasticsearch-index directory_test2 --elasticsearch-server http://localhost:9200 > /dev/null # Usage: # -# Migrate all keyspaces +# Migrate all keyspaces and reset the ES index # make db-migrate # # Migrate keyspace for only one service, say galley: # make db-migrate package=galley .PHONY: db-migrate db-migrate: c -ifeq ($(package), all) - ./dist/brig-schema --keyspace brig_test --replication-factor 1 - ./dist/galley-schema --keyspace galley_test --replication-factor 1 - ./dist/gundeck-schema --keyspace gundeck_test --replication-factor 1 - ./dist/spar-schema --keyspace spar_test --replication-factor 1 -# How this check works: https://stackoverflow.com/a/9802777 -else ifeq ($(package), $(filter $(package),brig galley gundeck spar)) - $(EXE_SCHEMA) --keyspace $(package)_test --replication-factor 1 -else - @echo No schema migrations for $(package) + ./dist/brig-schema --keyspace brig_test --replication-factor 1 > /dev/null + ./dist/galley-schema --keyspace galley_test --replication-factor 1 > /dev/null + ./dist/gundeck-schema --keyspace gundeck_test --replication-factor 1 > /dev/null + ./dist/spar-schema --keyspace spar_test --replication-factor 1 > /dev/null +ifeq ($(INTEGRATION_FEDERATION_TESTS), 1) + ./dist/brig-schema --keyspace brig_test2 --replication-factor 1 > /dev/null + ./dist/galley-schema --keyspace galley_test2 --replication-factor 1 > /dev/null + ./dist/gundeck-schema --keyspace gundeck_test2 --replication-factor 1 > /dev/null + ./dist/spar-schema --keyspace spar_test2 --replication-factor 1 > /dev/null endif - + ./dist/brig-index reset --elasticsearch-index-prefix directory --elasticsearch-server http://localhost:9200 > /dev/null + ./dist/brig-index reset --elasticsearch-index-prefix directory2 --elasticsearch-server http://localhost:9200 > /dev/null ################################# ## dependencies @@ -315,15 +336,15 @@ kube-integration: kube-integration-setup kube-integration-test .PHONY: kube-integration-setup kube-integration-setup: charts-integration - export NAMESPACE=$(NAMESPACE); ./hack/bin/integration-setup-federation.sh + export NAMESPACE=$(NAMESPACE); export HELM_PARALLELISM=$(HELM_PARALLELISM); ./hack/bin/integration-setup-federation.sh .PHONY: kube-integration-test kube-integration-test: - export NAMESPACE=$(NAMESPACE); ./hack/bin/integration-test.sh + export NAMESPACE=$(NAMESPACE); export HELM_PARALLELISM=$(HELM_PARALLELISM); ./hack/bin/integration-test.sh .PHONY: kube-integration-teardown kube-integration-teardown: - export NAMESPACE=$(NAMESPACE); ./hack/bin/integration-teardown-federation.sh + export NAMESPACE=$(NAMESPACE); export HELM_PARALLELISM=$(HELM_PARALLELISM); ./hack/bin/integration-teardown-federation.sh .PHONY: kube-integration-e2e-telepresence kube-integration-e2e-telepresence: diff --git a/cabal.project b/cabal.project index b1c4c70161..ddfd0b8efc 100644 --- a/cabal.project +++ b/cabal.project @@ -1,4 +1,4 @@ -with-compiler: ghc-8.10.7 +with-compiler: ghc-9.2.4 packages: libs/api-bot/ diff --git a/cassandra-schema.cql b/cassandra-schema.cql index e9e0b6c8b9..292d52425f 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -137,8 +137,13 @@ CREATE TABLE galley_test.team_features ( mls_allowed_ciphersuites set, mls_default_ciphersuite int, mls_default_protocol int, + mls_e2eid_lock_status int, + mls_e2eid_status int, + mls_e2eid_ver_exp timestamp, mls_protocol_toggle_users set, mls_status int, + outlook_cal_integration_lock_status int, + outlook_cal_integration_status int, search_visibility_inbound_status int, search_visibility_status int, self_deleting_messages_lock_status int, @@ -1083,7 +1088,7 @@ CREATE TABLE brig_test.provider ( CREATE TABLE brig_test.user_keys ( key text PRIMARY KEY, user uuid -) WITH bloom_filter_fp_chance = 0.01 +) WITH bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} @@ -1124,7 +1129,7 @@ CREATE TABLE brig_test.invitee_info ( invitee uuid PRIMARY KEY, conv uuid, inviter uuid -) WITH bloom_filter_fp_chance = 0.01 +) WITH bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} @@ -1290,7 +1295,7 @@ CREATE TABLE brig_test.user ( sso_id text, status int, team uuid -) WITH bloom_filter_fp_chance = 0.01 +) WITH bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} @@ -1398,7 +1403,7 @@ CREATE TABLE brig_test.password_reset ( retries int, timeout timestamp, user uuid -) WITH bloom_filter_fp_chance = 0.01 +) WITH bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} @@ -1513,7 +1518,7 @@ CREATE TABLE brig_test.connection ( status int, PRIMARY KEY (left, right) ) WITH CLUSTERING ORDER BY (right ASC) - AND bloom_filter_fp_chance = 0.01 + AND bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} @@ -1584,7 +1589,7 @@ CREATE TABLE brig_test.activation_keys ( key_type ascii, retries int, user uuid -) WITH bloom_filter_fp_chance = 0.01 +) WITH bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} AND comment = '' AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} diff --git a/charts/brig/templates/configmap.yaml b/charts/brig/templates/configmap.yaml index a62139035a..45f664e865 100644 --- a/charts/brig/templates/configmap.yaml +++ b/charts/brig/templates/configmap.yaml @@ -270,8 +270,11 @@ data: {{- if .setSftListAllServers }} setSftListAllServers: {{ .setSftListAllServers }} {{- end }} - {{- if .setWhitelist }} - setWhitelist: {{ toYaml .setWhitelist | nindent 8 }} + {{- if .setAllowlistEmailDomains }} + setAllowlistEmailDomains: {{ toYaml .setAllowlistEmailDomains | nindent 8 }} + {{- end }} + {{- if .setAllowlistPhonePrefixes }} + setAllowlistPhonePrefixes: {{ toYaml .setAllowlistPhonePrefixes | nindent 8 }} {{- end }} {{- if .setFeatureFlags }} setFeatureFlags: {{ toYaml .setFeatureFlags | nindent 8 }} diff --git a/charts/brig/templates/tests/brig-integration.yaml b/charts/brig/templates/tests/brig-integration.yaml index 17921894bf..c8cfa602f1 100644 --- a/charts/brig/templates/tests/brig-integration.yaml +++ b/charts/brig/templates/tests/brig-integration.yaml @@ -2,6 +2,9 @@ apiVersion: v1 kind: Service metadata: name: "brig-integration" + annotations: + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation labels: app: brig-integration chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} @@ -9,9 +12,7 @@ metadata: heritage: {{ .Release.Service }} spec: type: ClusterIP - ports: - - port: 9000 - targetPort: 9000 + clusterIP: None selector: app: brig-integration release: {{ .Release.Name }} @@ -21,7 +22,7 @@ kind: Pod metadata: name: "{{ .Release.Name }}-brig-integration" annotations: - "helm.sh/hook": test-success + "helm.sh/hook": test labels: app: brig-integration release: {{ .Release.Name }} diff --git a/charts/brig/templates/tests/configmap.yaml b/charts/brig/templates/tests/configmap.yaml index 45964c933c..56667e55ed 100644 --- a/charts/brig/templates/tests/configmap.yaml +++ b/charts/brig/templates/tests/configmap.yaml @@ -2,6 +2,9 @@ apiVersion: v1 kind: ConfigMap metadata: name: "brig-integration" + annotations: + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation data: integration.yaml: | brig: @@ -54,7 +57,6 @@ data: publicKey: /etc/wire/integration-secrets/provider-publickey.pem cert: /etc/wire/integration-secrets/provider-publiccert.pem botHost: https://brig-integration - botPort: 9000 backendTwo: brig: diff --git a/charts/brig/templates/tests/nginz-service.yaml b/charts/brig/templates/tests/nginz-service.yaml index c31128667c..6eda016c82 100644 --- a/charts/brig/templates/tests/nginz-service.yaml +++ b/charts/brig/templates/tests/nginz-service.yaml @@ -5,6 +5,9 @@ apiVersion: v1 kind: Service metadata: name: nginz-integration-http + annotations: + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation spec: type: ClusterIP ports: diff --git a/charts/brig/templates/tests/secret.yaml b/charts/brig/templates/tests/secret.yaml index bfe877caf7..69ce7e671e 100644 --- a/charts/brig/templates/tests/secret.yaml +++ b/charts/brig/templates/tests/secret.yaml @@ -2,6 +2,9 @@ apiVersion: v1 kind: ConfigMap metadata: name: brig-integration-secrets + annotations: + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation data: # These "secrets" are only used in tests and are therefore safe to be stored unencrypted provider-privatekey.pem: | diff --git a/charts/cargohold/templates/tests/cargohold-integration.yaml b/charts/cargohold/templates/tests/cargohold-integration.yaml index 6decd33e47..722d138637 100644 --- a/charts/cargohold/templates/tests/cargohold-integration.yaml +++ b/charts/cargohold/templates/tests/cargohold-integration.yaml @@ -3,7 +3,7 @@ kind: Pod metadata: name: "{{ .Release.Name }}-cargohold-integration" annotations: - "helm.sh/hook": test-success + "helm.sh/hook": test spec: volumes: - name: "cargohold-integration" diff --git a/charts/cargohold/templates/tests/configmap.yaml b/charts/cargohold/templates/tests/configmap.yaml index bb0ff67c8f..18a5b29b22 100644 --- a/charts/cargohold/templates/tests/configmap.yaml +++ b/charts/cargohold/templates/tests/configmap.yaml @@ -2,6 +2,9 @@ apiVersion: v1 kind: ConfigMap metadata: name: "cargohold-integration" + annotations: + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation data: integration.yaml: | cargohold: diff --git a/charts/cassandra-ephemeral/values.yaml b/charts/cassandra-ephemeral/values.yaml index e28bc2a529..611cf827a9 100644 --- a/charts/cassandra-ephemeral/values.yaml +++ b/charts/cassandra-ephemeral/values.yaml @@ -22,3 +22,14 @@ cassandra-ephemeral: seed_size: 1 max_heap_size: 2048M heap_new_size: 1024M + + livenessProbe: + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 15 + readinessProbe: + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 15 diff --git a/charts/coturn/Chart.yaml b/charts/coturn/Chart.yaml index e42ecb8149..5f21164779 100644 --- a/charts/coturn/Chart.yaml +++ b/charts/coturn/Chart.yaml @@ -11,4 +11,4 @@ version: 0.0.42 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. -appVersion: 4.5.2-wireapp.8 +appVersion: 4.6.0-wireapp.4 diff --git a/charts/coturn/README.md b/charts/coturn/README.md index 85f8765d78..db46c3bf45 100644 --- a/charts/coturn/README.md +++ b/charts/coturn/README.md @@ -1,4 +1,4 @@ -**Warning**: this chart is currently considered alpha. Use at your own risk! +**Warning**: this chart is currently considered beta. Use at your own risk! This chart deploys Wire's fork of [coturn](https://github.com/coturn/coturn), a STUN and TURN server, with some additional features developed by Wire (see diff --git a/charts/coturn/values.yaml b/charts/coturn/values.yaml index d56986c6e0..30bc42b470 100644 --- a/charts/coturn/values.yaml +++ b/charts/coturn/values.yaml @@ -36,7 +36,7 @@ tls: # for handling runtime certificate reloads. repository: quay.io/wire/config-reloader-sidecar pullPolicy: IfNotPresent - tag: 1aa6cbbf2ce3a5182ec47e3579bbcb8f47e22fdc + tag: 72c3c8434660bd157d42012b0bcd67f338fc5c7a metrics: serviceMonitor: diff --git a/charts/federator/templates/tests/configmap.yaml b/charts/federator/templates/tests/configmap.yaml index 910411fe5d..44146840bd 100644 --- a/charts/federator/templates/tests/configmap.yaml +++ b/charts/federator/templates/tests/configmap.yaml @@ -2,6 +2,9 @@ apiVersion: v1 kind: ConfigMap metadata: name: "federator-integration" + annotations: + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation data: integration.yaml: | federatorInternal: diff --git a/charts/federator/templates/tests/federator-integration.yaml b/charts/federator/templates/tests/federator-integration.yaml index 32e6eef09e..3341cf4173 100644 --- a/charts/federator/templates/tests/federator-integration.yaml +++ b/charts/federator/templates/tests/federator-integration.yaml @@ -3,7 +3,7 @@ kind: Pod metadata: name: "{{ .Release.Name }}-federator-integration" annotations: - "helm.sh/hook": test-success + "helm.sh/hook": test spec: volumes: - name: "federator-integration" diff --git a/charts/galley/templates/configmap.yaml b/charts/galley/templates/configmap.yaml index 9962452b0c..4cdca97b5c 100644 --- a/charts/galley/templates/configmap.yaml +++ b/charts/galley/templates/configmap.yaml @@ -115,5 +115,13 @@ data: mls: {{- toYaml .settings.featureFlags.mls | nindent 10 }} {{- end }} + {{- if .settings.featureFlags.outlookCalIntegration }} + outlookCalIntegration: + {{- toYaml .settings.featureFlags.outlookCalIntegration | nindent 10 }} + {{- end }} + {{- if .settings.featureFlags.mlsE2EId }} + mlsE2EId: + {{- toYaml .settings.featureFlags.mlsE2EId | nindent 10 }} + {{- end }} {{- end }} {{- end }} diff --git a/charts/galley/templates/tests/configmap.yaml b/charts/galley/templates/tests/configmap.yaml index 42a487f66f..89d7d589de 100644 --- a/charts/galley/templates/tests/configmap.yaml +++ b/charts/galley/templates/tests/configmap.yaml @@ -2,6 +2,9 @@ apiVersion: v1 kind: ConfigMap metadata: name: "galley-integration" + annotations: + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation data: integration.yaml: | galley: @@ -25,4 +28,3 @@ data: publicKey: /etc/wire/integration-secrets/provider-publickey.pem cert: /etc/wire/integration-secrets/provider-publiccert.pem botHost: https://galley-integration - botPort: 9000 diff --git a/charts/galley/templates/tests/galley-integration.yaml b/charts/galley/templates/tests/galley-integration.yaml index 883f57e2d9..c543eb2048 100644 --- a/charts/galley/templates/tests/galley-integration.yaml +++ b/charts/galley/templates/tests/galley-integration.yaml @@ -2,6 +2,9 @@ apiVersion: v1 kind: Service metadata: name: "galley-integration" + annotations: + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation labels: app: galley-integration chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} @@ -9,9 +12,7 @@ metadata: heritage: {{ .Release.Service }} spec: type: ClusterIP - ports: - - port: 9000 - targetPort: 9000 + clusterIP: None selector: app: galley-integration release: {{ .Release.Name }} @@ -21,7 +22,7 @@ kind: Pod metadata: name: "{{ .Release.Name }}-galley-integration" annotations: - "helm.sh/hook": test-success + "helm.sh/hook": test labels: app: galley-integration release: {{ .Release.Name }} diff --git a/charts/galley/templates/tests/secret.yaml b/charts/galley/templates/tests/secret.yaml index 74f118d1c2..d58a49c360 100644 --- a/charts/galley/templates/tests/secret.yaml +++ b/charts/galley/templates/tests/secret.yaml @@ -2,6 +2,9 @@ apiVersion: v1 kind: ConfigMap metadata: name: galley-integration-secrets + annotations: + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation data: # These "secrets" are only used in tests and are therefore safe to be stored unencrypted provider-privatekey.pem: | diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index 8f260a0abe..c54d90292b 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -84,6 +84,16 @@ config: validateSAMLemails: defaults: status: enabled + outlookCalIntegration: + defaults: + status: disabled + lockStatus: locked + mlsE2EId: + defaults: + status: disabled + config: + verificationExpiration: null + lockStatus: unlocked aws: region: "eu-west-1" diff --git a/charts/gundeck/templates/tests/configmap.yaml b/charts/gundeck/templates/tests/configmap.yaml index 5c398d39e1..c5df36067c 100644 --- a/charts/gundeck/templates/tests/configmap.yaml +++ b/charts/gundeck/templates/tests/configmap.yaml @@ -2,6 +2,9 @@ apiVersion: v1 kind: ConfigMap metadata: name: "gundeck-integration" + annotations: + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation data: integration.yaml: | gundeck: @@ -16,7 +19,7 @@ data: # some gundeck integration tests make use of two different # cannon instances to test the distributed case. when running # the integration tests locally, the two instances will be spun - # up separately (see `wire-server/services/integration.sh`). + # up separately (see `wire-server/services/run-services`). # # here, we spin up two replicas, provide the integration tests # with the same service coordinates, and rely on the k8s load diff --git a/charts/gundeck/templates/tests/gundeck-integration.yaml b/charts/gundeck/templates/tests/gundeck-integration.yaml index 8424fd3770..4f81fa2c22 100644 --- a/charts/gundeck/templates/tests/gundeck-integration.yaml +++ b/charts/gundeck/templates/tests/gundeck-integration.yaml @@ -3,7 +3,7 @@ kind: Pod metadata: name: "{{ .Release.Name }}-gundeck-integration" annotations: - "helm.sh/hook": test-success + "helm.sh/hook": test spec: volumes: - name: "gundeck-integration" diff --git a/charts/nginx-ingress-services/templates/ingress_federator.yaml b/charts/nginx-ingress-services/templates/ingress_federator.yaml index bd9a6aa0b1..8d52ff9a33 100644 --- a/charts/nginx-ingress-services/templates/ingress_federator.yaml +++ b/charts/nginx-ingress-services/templates/ingress_federator.yaml @@ -25,7 +25,11 @@ spec: - host: {{ .Values.config.dns.federator }} http: paths: - - backend: + - path: / + {{- if $ingressSupportsPathType }} + pathType: Prefix + {{- end }} + backend: {{- if $apiIsStable }} service: name: federator diff --git a/charts/nginz/static/swagger-ui/index.html b/charts/nginz/static/swagger-ui/index.html deleted file mode 100644 index 3524029f07..0000000000 --- a/charts/nginz/static/swagger-ui/index.html +++ /dev/null @@ -1,77 +0,0 @@ - - -
- - - -
- - - -
- -
- - - - - - - - - diff --git a/charts/nginz/static/swagger-ui/tab.html b/charts/nginz/static/swagger-ui/tab.html deleted file mode 100644 index 0798782592..0000000000 --- a/charts/nginz/static/swagger-ui/tab.html +++ /dev/null @@ -1,221 +0,0 @@ - - - - - - - Swagger UI - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -

- -
-
-
-
-
-
- -
- -
- -
- -
- -
- Show Access Token - Hide Access Token -
-
- -
 
-
- - - diff --git a/charts/nginz/templates/conf/_nginx.conf.tpl b/charts/nginz/templates/conf/_nginx.conf.tpl index 2cc30632be..052cf0b52f 100644 --- a/charts/nginz/templates/conf/_nginx.conf.tpl +++ b/charts/nginz/templates/conf/_nginx.conf.tpl @@ -223,12 +223,6 @@ http { {{- if hasKey $location "envs" -}} {{- range $env := $location.envs -}} {{- if or (eq $env $.Values.nginx_conf.env) (eq $env "all") -}} - - {{- if and (not (eq $.Values.nginx_conf.env "prod")) ($location.doc) -}} - - rewrite ^/api-docs{{ $location.path }} {{ $location.path }}/api-docs?base_url=https://{{ $.Values.nginx_conf.env }}-nginz-https.{{ $.Values.nginx_conf.external_env_domain }}/ break; - {{- end }} - {{- if $location.strip_version }} rewrite ^/v[0-9]+({{ $location.path }}) $1; @@ -326,39 +320,6 @@ http { {{- end -}} {{- end }} - {{ if not (eq $.Values.nginx_conf.env "prod") }} - # - # Swagger Resource Listing - # - location /api-docs { - zauth off; - default_type application/json; - root {{ $.Values.nginx_conf.swagger_root }}; - index resources.json; - if ($request_method = 'OPTIONS') { - add_header 'Access-Control-Allow-Methods' "GET, POST, PUT, DELETE, OPTIONS"; - add_header 'Access-Control-Allow-Headers' "$http_access_control_request_headers, DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type"; - add_header 'Content-Type' 'text/plain; charset=UTF-8'; - add_header 'Content-Length' 0; - return 204; - } - more_set_headers 'Access-Control-Allow-Origin: $http_origin'; - } - - # Swagger UI - location /swagger-ui { - zauth off; - gzip off; - alias /opt/zwagger-ui; - types { - application/javascript js; - text/css css; - text/html html; - image/png png; - } - } - {{ end }} - {{- if hasKey .Values.nginx_conf "deeplink" }} location ~* ^/deeplink.(json|html)$ { zauth off; diff --git a/charts/nginz/templates/deployment.yaml b/charts/nginz/templates/deployment.yaml index 2f51126402..d9aa8c19b5 100644 --- a/charts/nginz/templates/deployment.yaml +++ b/charts/nginz/templates/deployment.yaml @@ -25,8 +25,6 @@ spec: annotations: # An annotation of the configmap checksum ensures changes to the configmap cause a redeployment upon `helm upgrade` checksum/configmap: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }} - checksum/swagger-ui-configmap: {{ include (print .Template.BasePath "/swagger-ui-configmap.yaml") . | sha256sum }} - checksum/swagger-resources-configmap: {{ include (print .Template.BasePath "/swagger-resources-configmap.yaml") . | sha256sum }} checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} fluentbit.io/parser-nginz: nginz spec: @@ -53,11 +51,6 @@ spec: - name: upstreams mountPath: /etc/wire/nginz/upstreams readOnly: true - - name: swagger-ui - mountPath: /opt/zwagger-ui - readOnly: true - - name: swagger-resources - mountPath: {{ .Values.nginx_conf.swagger_root }}/api-docs ports: - name: http containerPort: {{ .Values.config.http.httpPort }} @@ -90,9 +83,3 @@ spec: secretName: nginz - name: upstreams emptyDir: {} - - name: swagger-ui - configMap: - name: nginz-swagger-ui - - name: swagger-resources - configMap: - name: nginz-swagger-resources diff --git a/charts/nginz/templates/swagger-resources-configmap.yaml b/charts/nginz/templates/swagger-resources-configmap.yaml deleted file mode 100644 index a6b53022f6..0000000000 --- a/charts/nginz/templates/swagger-resources-configmap.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Swagger-1.0 JSON to combine swagger-1.0 documents from different services -apiVersion: v1 -data: - resources.json: |2 - { - "apiVersion": "1.0", - "swaggerVersion": "1.2", - "apis": [ - { - "path": "/users", - "description": "Users, Connections and Onboarding" - }, - { - "path": "/push", - "description": "Push Notifications" - }, - { - "path": "/conversations", - "description": "Conversations and Messaging" - } - ] - } - -kind: ConfigMap -metadata: - name: nginz-swagger-resources - diff --git a/charts/nginz/templates/swagger-ui-configmap.yaml b/charts/nginz/templates/swagger-ui-configmap.yaml deleted file mode 100644 index 0ec5e5edc1..0000000000 --- a/charts/nginz/templates/swagger-ui-configmap.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: v1 -data: -{{ (.Files.Glob "static/swagger-ui/*").AsConfig | indent 2 }} -kind: ConfigMap -metadata: - name: nginz-swagger-ui diff --git a/charts/nginz/values.yaml b/charts/nginz/values.yaml index 3ff3fbe435..2427de7753 100644 --- a/charts/nginz/values.yaml +++ b/charts/nginz/values.yaml @@ -33,7 +33,6 @@ nginx_conf: worker_processes: auto worker_rlimit_nofile: 131072 worker_connections: 65536 - swagger_root: /var/www/swagger # deeplink: # endpoints: # backendURL: "https://prod-nginz-https.wire.com" @@ -140,10 +139,6 @@ nginx_conf: - all disable_zauth: true unlimited_requests_endpoint: true - - path: /users/api-docs$ - envs: - - staging - disable_zauth: true - path: /users envs: - all @@ -159,6 +154,10 @@ nginx_conf: disable_zauth: true envs: - staging + - path: /api-internal/swagger-ui + disable_zauth: true + envs: + - staging - path: /self$ # Matches exactly /self envs: - all @@ -382,10 +381,6 @@ nginx_conf: disable_zauth: true envs: - all - - path: /conversations/api-docs$ - envs: - - staging - disable_zauth: true - path: /conversations/([^/]*)/otr/messages envs: - all @@ -459,10 +454,6 @@ nginx_conf: disable_zauth: true basic_auth: true versioned: false - - path: /teams/api-docs - envs: - - all - disable_zauth: true - path: /teams/([^/]*)/features envs: - all @@ -484,10 +475,6 @@ nginx_conf: - path: /feature-configs(.*) envs: - all - - path: /galley-api/swagger-ui - disable_zauth: true - envs: - - all - path: /mls/welcome envs: - all @@ -501,10 +488,6 @@ nginx_conf: envs: - all gundeck: - - path: /push/api-docs$ - envs: - - staging - disable_zauth: true - path: /push envs: - all diff --git a/charts/postgresql/Chart.yaml b/charts/postgresql/Chart.yaml new file mode 100644 index 0000000000..bff81e0d6f --- /dev/null +++ b/charts/postgresql/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +description: Wrapper chart for bitnami/postgresql +name: postgresql +version: 0.0.42 diff --git a/charts/postgresql/README.md b/charts/postgresql/README.md new file mode 100644 index 0000000000..84f4ee02a5 --- /dev/null +++ b/charts/postgresql/README.md @@ -0,0 +1,4 @@ +This is the wrapper for PostgreSQL Bitnami chart. + +Configure the values.yaml file to create the database, username, password and other configuration. +List of parameters available - https://artifacthub.io/packages/helm/bitnami/postgresql#parameters diff --git a/charts/postgresql/requirements.yaml b/charts/postgresql/requirements.yaml new file mode 100644 index 0000000000..c1d2a1f639 --- /dev/null +++ b/charts/postgresql/requirements.yaml @@ -0,0 +1,4 @@ +dependencies: +- name: postgresql + version: 11.9.8 + repository: https://charts.bitnami.com/bitnami diff --git a/charts/postgresql/values.yaml b/charts/postgresql/values.yaml new file mode 100644 index 0000000000..fa2230183f --- /dev/null +++ b/charts/postgresql/values.yaml @@ -0,0 +1,5 @@ +# Configure the parent postgresql chart +postgresql: + fullnameOverride: postgresql + volumePermissions: + enabled: true diff --git a/charts/spar/templates/tests/configmap.yaml b/charts/spar/templates/tests/configmap.yaml index e446ee7bdd..2eb1966099 100644 --- a/charts/spar/templates/tests/configmap.yaml +++ b/charts/spar/templates/tests/configmap.yaml @@ -2,6 +2,9 @@ apiVersion: v1 kind: ConfigMap metadata: name: "spar-integration" + annotations: + "helm.sh/hook": post-install + "helm.sh/hook-delete-policy": before-hook-creation data: integration.yaml: | brig: diff --git a/charts/spar/templates/tests/spar-integration.yaml b/charts/spar/templates/tests/spar-integration.yaml index c4735ffd15..7063bbb745 100644 --- a/charts/spar/templates/tests/spar-integration.yaml +++ b/charts/spar/templates/tests/spar-integration.yaml @@ -3,7 +3,7 @@ kind: Pod metadata: name: "{{ .Release.Name }}-spar-integration" annotations: - "helm.sh/hook": test-success + "helm.sh/hook": test labels: app: spar-integration release: {{ .Release.Name }} diff --git a/deploy/dockerephemeral/coredns-config/db.example.com b/deploy/dockerephemeral/coredns-config/db.example.com index 941502a432..39beb50578 100644 --- a/deploy/dockerephemeral/coredns-config/db.example.com +++ b/deploy/dockerephemeral/coredns-config/db.example.com @@ -12,4 +12,6 @@ $ORIGIN example.com. www IN A 127.0.0.1 IN AAAA ::1 -_wire-server-federator._tcp IN SRV 0 0 443 federator.integration.example.com. +_wire-server-federator._tcp IN SRV 0 0 8443 localhost. +_wire-server-federator._tcp.b IN SRV 0 0 9443 localhost. + diff --git a/deploy/dockerephemeral/run.sh b/deploy/dockerephemeral/run.sh index 13a413743b..e982094a13 100755 --- a/deploy/dockerephemeral/run.sh +++ b/deploy/dockerephemeral/run.sh @@ -1,7 +1,10 @@ #!/usr/bin/env bash +set -x + # run.sh should work no matter what is the current directory SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" DOCKER_FILE="$SCRIPT_DIR/docker-compose.yaml" docker-compose --file "$DOCKER_FILE" up +docker-compose --file "$DOCKER_FILE" down diff --git a/docs/src/conf.py b/docs/src/conf.py index c91faf4b90..939c35ae68 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -10,26 +10,18 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) -import datetime +import os import requests +# unset SOURCE_DATE_EPOCH to aviod side effects caused by sphinx +if 'SOURCE_DATE_EPOCH' in os.environ: + del os.environ['SOURCE_DATE_EPOCH'] + # -- Project information ----------------------------------------------------- project = 'Wire' author = 'Wire Swiss GmbH' -# Since nix has an old timestamp it operates under for reproducability, get -# current date from the internet. -try: - r = requests.get("https://worldtimeapi.org/api/timezone/Europe/Berlin").json() - today_year = r['datetime'][:4] - # the first commit of wire-docs was in 2019. - copyright = f'2019 - {today_year}, ' + author -except: - print("Error in getting online date, fallback to potentially out-of-date year") - copyright = f'2019 - 2022, Wire Swiss GmbH' +copyright = f'2019 - 2023, Wire Swiss GmbH' version = '0.0.4' # the 'release' variable is used in latex-based PDF generation release = version diff --git a/docs/src/developer/developer/building.md b/docs/src/developer/developer/building.md index dac2b4daff..e913a41b24 100644 --- a/docs/src/developer/developer/building.md +++ b/docs/src/developer/developer/building.md @@ -18,16 +18,22 @@ Then the following Makefile targets can be used to compile and test wire-server # to compile all binaries to ./dist run make - + # to build and install all of galley's executables make c package=galley - + # also run galley's unit tests make c package=galley test=1 ## Troubleshooting +### If the PR doesn't pass the CI (read check marks on github) + +``` +make sanitize-pr +``` + ### Linker errors while compiling Linker errors can occur if the nix-provided build environment (see `nix/` directory) changes. Since cabal is not aware of the changed environment the cached build artifacts in `./dist-newstyle` and `~/.cabal/store/` from previous builds may be invalid causing the linker errors. @@ -73,10 +79,31 @@ deploy/dockerephemeral/run.sh After all containers are up you can use these Makefile targets to run the tests locally: -``` -# build and run galley's integration tests -make ci package=galley - -# run galley's integration tests that match a pattern -TASTY_PATTERN="/MLS/" make ci package=galley -``` +1. Build and run all integration tests + ```bash + make ci + ``` + +2. Build and run integration tests for a service (say galley) + ```bash + make ci package=galley + ``` + +3. Run integration tests written using `tasty` for a service (say galley) that match a pattern + ```bash + TASTY_PATTERN="/MLS/" make ci package=galley + ``` + For more details on pattern formats, see tasty docs: https://github.com/UnkindPartition/tasty#patterns + +4. Run integration tests written using `hspec` for a service (say spar) that match a pattern + ```bash + HSPEC_MATCH='Scim' make ci package=spar + ``` + For more details on match formats, see hspec docs: https://hspec.github.io/match.html + +5. Run integration tests without any parallelism + ```bash + TASTY_NUM_THREADS=1 make ci package=brig + ``` + + `TASTY_NUM_THREADS` can also be set to other values, it defaults to number of cores available. diff --git a/docs/src/developer/developer/features.md b/docs/src/developer/developer/features.md index d560b44162..d000403271 100644 --- a/docs/src/developer/developer/features.md +++ b/docs/src/developer/developer/features.md @@ -73,5 +73,6 @@ The Team features API preceedes the notion of the feature configuration API. The endpoints of the form `GET /teams/:tid/features/:feature-name` and `PUT /teams/:tid/features/:feature-name` can be used to get and set the feature configuration on a per-team level. Features that cannot be get and set on a -per-team level may be missing from the Team Features API. See the Swagger -documentation on what endpoints are available. +per-team level may be missing from the Team Features API. See the [Swagger +documentation](../../understand/api-client-perspective/swagger.md) on what +endpoints are available. diff --git a/docs/src/developer/developer/how-to.md b/docs/src/developer/developer/how-to.md index 68250bba21..ae65044547 100644 --- a/docs/src/developer/developer/how-to.md +++ b/docs/src/developer/developer/how-to.md @@ -19,11 +19,14 @@ Terminal 2: * Run services including nginz: `./services/start-services-only.sh`. Open your browser at: +[http://localhost:8080/api/swagger-ui](http://localhost:8080/api/swagger-ui) for +the Swagger 2.0 endpoints of the latest version. This endpoint is versioned; +i.e. the Swagger docs refer to the API version. Refer to the [Swagger API +documentation](../../understand/api-client-perspective/swagger.md) regarding +Swagger and API versioning. -- [http://localhost:8080/api/swagger-ui](http://localhost:8080/api/swagger-ui) for the swagger 2.0 endpoints (in development as of Feb 2021 - more endpoints will be added here as time goes on) -- [http://localhost:8080/swagger-ui](http://localhost:8080/swagger-ui) for the old swagger 1.2 API (old swagger, endpoints will disappear from here (and become available in the previous link) as time progresses). Run `make -C services/nginz integration-test/conf/nginz/zwagger-ui` once to get JS libraries needed (they are not included in the repo). +Swagger json is available under [http://localhost:8080/api/swagger.json](http://localhost:8080/api/swagger.json) -Swagger json (for swagger 2.0 endpoints) is available under [http://localhost:8080/api/swagger.json](http://localhost:8080/api/swagger.json) ## How to run federation tests across two backends diff --git a/docs/src/developer/developer/pr-guidelines.md b/docs/src/developer/developer/pr-guidelines.md index 1afeb2c56e..be0bf1f01b 100644 --- a/docs/src/developer/developer/pr-guidelines.md +++ b/docs/src/developer/developer/pr-guidelines.md @@ -18,7 +18,7 @@ See `docs/legacy/developer/changelog.md` for more information. ## Schema migrations -Don't delete columns that are still used by versions that are deployed. If you delete columns then the old version will fail in the deployment process. Rather than deleting keep the unused columns around and comment them as being discontinued in the schema migration code. +Don't delete columns that are still used by versions that are deployed. If you delete columns then the old version will fail in the deployment process. Rather than deleting keep the unused columns around and comment them as being discontinued in the schema migration code. If a cassandra schema migration has been added then add this to the checklist: @@ -84,7 +84,7 @@ If a PR adds new configuration options for say brig, the following files need to * [ ] The integration test config: `services/brig/brig.integration.yaml` * [ ] The charts: `charts/brig/templates/configmap.yaml` * [ ] The default values: `charts/brig/values.yaml` -* [ ] The values files for CI: `hack/helm_vars/wire-server/values.yaml` +* [ ] The values files for CI: `hack/helm_vars/wire-server/values.yaml.gotmpl` * [ ] The configuration docs: `docs/src/developer/reference/config-options.md` If any new configuration value is required and has no default, then: @@ -100,8 +100,9 @@ Remove them with the PR from wire-server `./charts` folder, as charts are linked ### Renaming configuration flags -Avoid doing this. If you must, see Removing/adding sections above. But please note that all people who have an installation of wire also may have overridden any of the configuration option you may wish to change the name of. As this is not type checked, it's very error prone and people may find themselves with default configuration values being used instead of their intended configuration settings. Guideline: only rename for good reasons, not for aesthetics; or be prepared to spend a significant -amount on documenting and communication about this change. +Avoid doing this, it's usually viable to introduce an at-least-equally-good name and remove the old one, that admins can first add the new options, then uprade the software, then remove the old ones. + +If you must, see Removing/adding sections above. But please note that all people who have an installation of wire also may have overridden any of the configuration option you may wish to change the name of. As this is not type checked, it's very error prone and people may find themselves with default configuration values being used instead of their intended configuration settings. Guideline: only rename for good reasons, not for aesthetics; or be prepared to spend a significant amount on documenting and communication about this change. ## Changes to developer workflow diff --git a/docs/src/developer/developer/upgrading.md b/docs/src/developer/developer/upgrading.md new file mode 100644 index 0000000000..3900a03403 --- /dev/null +++ b/docs/src/developer/developer/upgrading.md @@ -0,0 +1,32 @@ +# Upgrading + +Here are some workflow suggestions when you're trying to upgrade packages (or GHC) in wire-server. + +- Use and install [nix-output-monitor](https://github.com/maralorn/nix-output-monitor) when building nix derivations locally. It shows you the dependency tree building as well as the progress. This can give you a sense for much progress you've made. + +- The `.envrc` environment probably won't load anymore. To fix this adjust the for packages that get passed to `shellFor` in `wire-server.nix`. In the beginning you might want to start out with an empty list as an argument. + +- You can explore our nix derivations via `nix repl` and then `:l ./nix` to load the attrset in `./nix/default.nix`. For example to see which version of `aeson` will be used you can evaluate `wireServer.haskellPackages.aeson.version`. TAB-autocompletion also tells you if multiple versions of a package are included in the `nixpkgs` pin, e.g. `aeson_1_5_6_0`, `aeson_2_1_1_0`, which can be used in `manual-overrides.nix` to overwrite the default, e.g. `aeson`. + +- Your goal is to make all packages compile again with nix. Start small by trying to build single packages, e.g. `wire-api` or any external dependencies. +``` +nix-build ./nix -A wireServer.haskellPackagesUnoptimizedNoDocs.wire-api +``` + +- When a dependency doesn't build anymore because of unmet version constraints see if you can use a never version (`haskell-pins.nix`, or `manual-overrides.nix` if multiple versions of the package are included in the `nixpkgs` pin). Also check out the git repo or issue tracker of the dependency to find forks that work. You can also try removing versions constraints of packages: see `doJailbreak` in `manual-overrides.nix`. + +- If test-suites of a dependency don't compile, or cannot be run because they require IO interaction you can disable them: see `dontCheck` in `manual-overrides.nix` + +- To force a rebuild of the nix shell delete the `.env` directory and run `direnv reload` + +- If you need to fix code in a dependency consider forking it and creating a PR to the upstream after successful integration. Clone the repo and then symlink it inside `libs/` then run `generate-local-nix-packages.sh` to temporarily include its dependencies in the development shell. Make sure to include the package in `shellFor`. If you've got a working shell you can check the output of `ghc-pkg dump` to see the list of nix-provided ghc packages used by cabal. + +- Consider using `ghcid --command "cabal repl "` when fixing packages + +- Delete the `dist-newstyle` directory everytime you upgrade the nix environment, without any exception. That is because cabal does not pick up changes to the `ghc-pkg` set (defined the development shell). Cabal will complain about missing dependencies in these cases. + +- When trying to build any packages with cabal (e.g. for fixing code in a depencency or fixing local packages), make sure you've got the right package set in the `shellFor` argument and the right transitive dependencies in `cabal.project`. It takes a couple of tries to get both: a nix provided environment that works, and cabal not complaining about missing dependencies when building inside the environment. + +- It might happen that a package's test suite dependencies are not available in the nix environment. When you try to build with cabal it might try to build these external dependencies (which you want to avoid). What might work in these cases is to temporarily update the `default.nix` file (generated by `generate-local-nix-packages.sh`) to add the test suits dependencies to the library section. + +- If cabal is complaining about a missing `libsodium`, add `sodium-crypt-sign` to the `shellFor` environment. diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index 938ddba131..687aba4776 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -335,6 +335,21 @@ mls: This default configuration can be overriden on a per-team basis through the [feature config API](../developer/features.md) +### MLS End-to-End Identity + +The MLS end-to-end identity team feature adds an extra level of security and practicability. If turned on, automatic device authentication ensures that team members know they are communicating with people using authenticated devices. Team members get a certificate on all their devices. + +A timer can be set to configure until when team members need to get the verification certificate. When the timer goes off, they will be logged out and get the certificate automatically on their devices. The timer is set as a unix timestamp (number of seconds that have passed since 00:00:00 UTC on Thursday, 1 January 1970) after which the period for clients to verify their identity expires. + +```yaml +# galley.yaml +mlsE2EId: + defaults: + status: disabled + config: + verificationExpiration: 1676377048 + lockStatus: unlocked +``` ### Federation Domain @@ -463,6 +478,20 @@ federator: clientPrivateKey: client-key.pem ``` +## Outlook calalendar integration + +This feature setting only applies to the Outlook Calendar extension for Wire. As it is an external service, it should only be configured through this feature flag and otherwise ignored by the backend. + +Example default configuration: + +```yaml +# galley.yaml +outlookCalIntegration: + defaults: + status: disabled + lockStatus: locked +``` + ## Settings in brig Some features (as of the time of writing this: only diff --git a/docs/src/developer/reference/spar-braindump.md b/docs/src/developer/reference/spar-braindump.md index 05735e98c6..a5ed0a78b0 100644 --- a/docs/src/developer/reference/spar-braindump.md +++ b/docs/src/developer/reference/spar-braindump.md @@ -26,25 +26,6 @@ documentation answering your questions, look here! - if you want to work on our saml/scim implementation and do not have access to [https://github.com/zinfra/backend-issues/issues?q=is%3Aissue+is%3Aopen+label%3Aspar] and [https://github.com/wireapp/design-specs/tree/master/Single%20Sign%20On], please get in touch with us. -## design considerations - -### SCIM without SAML. - -Before https://github.com/wireapp/wire-server/pull/1200, scim tokens could only be added to teams that already had exactly one SAML IdP. Now, we also allow SAML-less teams to have SCIM provisioning. This is an alternative to onboarding via team-settings and produces user accounts that are authenticated with email and password. (Phone may or may not work, but is not officially supported.) - -The way this works is different from team-settings: we don't send invites, but we create active users immediately the moment the SCIM user post is processed. The new thing is that the created user has neither email nor phone nor a SAML identity, nor a password. - -How does this work? - -**email:** If no SAML IdP is present, SCIM user posts must contain an externalId that is an email address. This email address is not added to the newly created user, because it has not been validated. Instead, the flow for changing an email address is triggered in brig: an email is sent to the address containing a validation key, and once the user completes the flow, brig will add the email address to the user. We had to add very little code for this in this PR, it's all an old feature. - -When SCIM user gets are processed, in order to reconstruct the externalId from the user spar is retrieving from brig, we introduce a new json object for the `sso_id` field that looks like this: `{'scim_external_id': 'me@example.com'}`. - -In order to find users that have email addresses pending validation, we introduce a new table in spar's cassandra called `scim_external_ids`, in analogy to `user`. We have tried to use brig's internal `GET /i/user&email=...`, but that also finds pending email addresses, and there are corner cases when changing email addresses and waiting for the new address to be validated and the old to be removed... that made this approach seem infeasible. - -**password:** once the user has validated their email address, they need to trigger the "forgot password" flow -- also old code. - - ## operations ### enabling / disabling the sso feature for a team @@ -226,35 +207,6 @@ This entry gets removed automatically when the corresponding idp is deleted. You Clients can then ask for the default SSO code on `/sso/settings` and use it to initiate single sign-on. -### troubleshooting - -#### gathering information - -- find metadata for team in table `spar.idp_raw_metadata` via cqlsh - (since https://github.com/wireapp/wire-server/pull/872) - -- ask user for screenshots of the error message, or even better, for - the text. the error message contains lots of strings that you can - grep for in the spar sources. - - -#### making spar work with a new IdP - -often, new IdPs work out of the box, because there appears to be some -consensus about what minimum feature set everybody should support. - -if there are problems: collect the metadata xml and an authentication -response xml (either from the browser http logs via a more technically -savvy customer; FUTUREWORK: it would be nice to log all saml response -xml files that spar receives in prod and cannot process). - -https://github.com/wireapp/saml2-web-sso supports writing [unit vendor -compatibility -tests](https://github.com/wireapp/saml2-web-sso/blob/ff9b9f445475809d1fa31ef7f2932caa0ed31613/test/Test/SAML2/WebSSO/APISpec.hs#L266-L329) -against that response value. once that test passes, it should all -work fine. - - ### common misconceptions @@ -325,80 +277,3 @@ TODO (probably little difference between this and "user deletes herself"?) #### delete via scim TODO - - -## using the same IdP (same entityID, or Issuer) with different teams - -Some SAML IdP vendors do not allow to set up fresh entityIDs (issuers) -for fresh apps; instead, all apps controlled by the IdP are receiving -SAML credentials from the same issuer. - -In the past, wire has used the a tuple of IdP issuer and 'NameID' -(Haskell type 'UserRef') to uniquely identity users (tables -`spar.user_v2` and `spar.issuer_idp`). - -In order to allow one IdP to serve more than one team, this has been -changed: we now allow to identity an IdP by a combination of -entityID/issuer and wire `TeamId`. The necessary tweaks to the -protocol are listed here. - -For everybody using IdPs that do not have this limitation, we have -taken great care to not change the behavior. - - -### what you need to know when operating a team or an instance - -No instance-level configuration is required. - -If your IdP supports different entityID / issuer for different apps, -you don't need to change anything. We hope to deprecate the old -flavor of the SAML protocol eventually, but we will keep you posted in -the release notes, and give you time to react. - -If your IdP does not support different entityID / issuer for different -apps, keep reading. At the time of writing this section, there is no -support for multi-team IdP issuers in team-settings, so you have two -options: (1) use the rest API directly; or (2) contact our customer -support and send them the link to this section. - -If you feel up to calling the rest API, try the following: - -- Use the above end-point `GET /sso/metadata/:tid` with your `TeamId` - for pulling the SP metadata. -- When calling `POST /identity-provider`, make sure to add - `?api_version=v2`. (`?api_version=v1` or no omission of the query - param both invoke the old behavior.) - -NB: Neither version of the API allows you to provision a user with the -same Issuer and same NamdID. RATIONALE: this allows us to implement -'getSAMLUser' without adding 'TeamId' to 'UserRef', which in turn -would break the (admittedly leaky) abstarctions of saml2-web-sso. - - -### API changes in more detail - -- New query param `api_version=` for `POST - /identity-providers`. The version is stored in `spar.idp` together - with the rest of the IdP setup, and is used by `GET - /sso/initiate-login` (see below). -- `GET /sso/initiate-login` sends audience based on api_version stored - in `spar.idp`: for v1, the audience is `/sso/finalize-login`; for - v2, it's `/sso/finalize-login/:tid`. -- New end-point `POST /sso/finalize-login/:tid` that behaves - indistinguishable from `POST /sso/finalize-login`, except when more - than one IdP with the same issuer, but different teams are - registered. In that case, this end-point can process the - credentials by discriminating on the `TeamId`. -- `POST /sso/finalize-login/:tid` remains unchanged. -- New end-point `GET /sso/metadata/:tid` returns the same SP metadata as - `GET /sso/metadata`, with the exception that it lists - `"/sso/finalize-login/:tid"` as the path of the - `AssertionConsumerService` (rather than `"/sso/finalize-login"` as - before). -- `GET /sso/metadata` remains unchanged, and still returns the old SP - metadata, without the `TeamId` in the paths. - - -### database schema changes - -[V15](https://github.com/wireapp/wire-server/blob/b97439756cfe0721164934db1f80658b60de1e5e/services/spar/schema/src/V15.hs#L29-L43) diff --git a/docs/src/developer/reference/user/activation.md b/docs/src/developer/reference/user/activation.md index 723457ca7e..373b2c190d 100644 --- a/docs/src/developer/reference/user/activation.md +++ b/docs/src/developer/reference/user/activation.md @@ -10,7 +10,7 @@ A user is called _activated_ they have a verified identity -- e.g. a phone numbe A user that has been provisioned via single sign-on is always considered to be activated. -## Activated vs. non-activated users +## Activated vs. non-activated users (RefActivationBenefits)= Non-activated users can not [connect](connection.md) to others, nor can connection requests be made to anonymous accounts from verified accounts. As a result: @@ -19,10 +19,10 @@ Non-activated users can not [connect](connection.md) to others, nor can connecti The only flow where it makes sense for non-activated users to exist is the [wireless flow](RefRegistrationWireless) used for [guest rooms](https://wire.com/en/features/encrypted-guest-rooms/) -## API +## API (RefActivationApi)= -### Requesting an activation code +### Requesting an activation code (RefActivationRequest)= During the [standard registration flow](RefRegistrationStandard), the user submits an email address or phone number by making a request to `POST /activate/send`. A six-digit activation code will be sent to that email address / phone number. Sample request and response: @@ -44,7 +44,7 @@ The user can submit the activation code during registration to prove that they o The same `POST /activate/send` endpoint can be used to re-request an activation code. Please use this ability sparingly! To avoid unnecessary activation code requests, users should be warned that it might take up to a few minutes for an email or text message to arrive. -### Activating an existing account +### Activating an existing account (RefActivationSubmit)= If the account [has not been activated during verification](RefRegistrationNoPreverification), it can be activated afterwards by submitting an activation code to `POST /activate`. Sample request and response: @@ -80,7 +80,7 @@ If the email or phone has been verified already, `POST /activate` will return st There is a maximum of 3 activation attempts per activation code. On the third failed attempt the code is invalidated and a new one must be requested. -### Activation event +### Activation event (RefActivationEvent)= When the user becomes activated, they receive an event: @@ -92,7 +92,7 @@ When the user becomes activated, they receive an event: } ``` -### Detecting activation in the self profile +### Detecting activation in the self profile (RefActivationProfile)= In addition to the [activation event](RefActivationEvent), activation can be detected by polling the self profile: @@ -114,7 +114,7 @@ GET /self If the profile includes `"email"` or `"phone"`, the account is activated. -## Automating activation via email +## Automating activation via email (RefActivationEmailHeaders)= Our email verification messages contain headers that can be used to automate the activation process. @@ -134,20 +134,23 @@ X-Zeta-Key: ... X-Zeta-Code: 123456 ``` -## Phone/email whitelist -(RefActivationWhitelist)= +## Phone/email whitelist +(RefActivationAllowlist)= -The backend can be configured to only allow specific phone numbers or email addresses to register. The following options have to be set in `brig.yaml`: +The backend can be configured to only allow specific phone number prefixes and email address domains to register. The following options have to be set in `brig.yaml`: ```yaml optSettings: - setWhitelist: - whitelistUrl: ... # Checker URL - whitelistUser: ... # Basic auth username - whitelistPass: ... # Basic auth password + setAllowlistEmailDomains: + - wire.com + - example.com + - notagoodexample.com + setAllowlistPhonePrefixes: + - "+49" + - "+1555555" ``` -When those options are present, the backend will do a GET request at `?email=...` or `?mobile=...` for every activation request it receives. It will expect either status code 200 ("everything good") or 404 ("provided email/phone is not on the whitelist"). +When those options are present, the backend will match every activation request against these lists. If an email address or phone number are rejected by the whitelist, `POST /activate/send` or `POST /register` will return `403 Forbidden`: @@ -158,5 +161,3 @@ If an email address or phone number are rejected by the whitelist, `POST /activa "message": "Unauthorized e-mail address or phone number." } ``` - -Currently emails at `@wire.com` are always considered whitelisted, regardless of the whitelist service's response. diff --git a/docs/src/how-to/install/post-install.md b/docs/src/how-to/install/post-install.md index 6a513f0ece..f04d9157a8 100644 --- a/docs/src/how-to/install/post-install.md +++ b/docs/src/how-to/install/post-install.md @@ -1,3 +1,4 @@ +(checks)= # Verifying your installation After a successful installation of wire-server and its components, there are some useful checks to be run to ensure the proper functioning of the system. Here's a non-exhaustive list of checks to run on the hosts: diff --git a/docs/src/security-responses/2023-01-04_website_outage.md b/docs/src/security-responses/2023-01-04_website_outage.md new file mode 100644 index 0000000000..ce396c3291 --- /dev/null +++ b/docs/src/security-responses/2023-01-04_website_outage.md @@ -0,0 +1,21 @@ +# 2023-01-04 - Outage of wire.com caused by a DoS attack + +Last updated: 2023-01-19 + +## What happened? +On Tuesday, 2023-01-04, the Wire website wire.com was affected by an outage caused by a Denial-of-Service attack. This outage only concerns the wire.com website and none of the services provided by Wire. + +## What was the impact identified? +Several outages of short periods (7min, 2min, 3min, 4min) have been identified beginning from UTC 05:13. + +## Are Wire installations affected? +Wire/wire-server was not affected by the wire.com website outage. + +## Timeline + +*2023-01-04 05:13*: The website monitor triggered an alert on a server issue.\ +*2023-01-04 05:16*: The responsible team of the service provider responded and saw an outage for periods of 7 minutes beginning from UTC 05:13, 2 minutes (UTC 05:29), 3 minutes (UTC 05:36) and 4 minutes (UTC 05:43).\ +*2023-01-04/11:34*: Wire was informed about an attempted DoS attack on wire.com by the service provider which manually blocked the IP address in the process.\ +*2023-01-04 09:15*: Wire started an internal investigation to check whether other systems have been affected, which was not the case.\ +*2023-01-04 14:30*: Wire contacted the service provider for more details on the incident as well as the corresponding log files.\ +*2023-01-18 09:53*: Wire received the incident report from the service provider.\ diff --git a/docs/src/security-responses/2023-01-19_html_injection.md b/docs/src/security-responses/2023-01-19_html_injection.md new file mode 100644 index 0000000000..b0b24151d3 --- /dev/null +++ b/docs/src/security-responses/2023-01-19_html_injection.md @@ -0,0 +1,18 @@ +# 2023-01-19 - Security Advisory: HTML Injection in wire.com + +Last updated: 2023-01-31 + +## Introduction +On the 16st January, 2023, we were informed about a possible vulnerability in our website wire.com. A get-parameter on the page https://wire.com/en/pricing/ was vulnerable to HTML injection. As the website wire.com is not directly maintained by Wire, the service provider was directly informed about the disclosed issue. The patch that fixed that vulnerability was rolled out on the 18th of January. + +## Impact +An adversary would have been able to inject arbitrary HTML code through the parameter and send that link to someone else which would show them e.g., a defaced website or potentially inject JavaScript. + +## Are Wire installations affected? +Wire/wire-server is not affected by this vulnerability as our website wire.com is completely separated from the Wire backend. + +## Are Wire clients affected? +Wire clients are not affected by this vulnerability. + +## Credits +We thank [Umar Ahmed](https://linkedin.com/in/theumar9) for reporting this vulnerability. diff --git a/docs/src/understand/api-client-perspective/img/swagger.png b/docs/src/understand/api-client-perspective/img/swagger.png deleted file mode 100644 index a8b4b2fccb..0000000000 Binary files a/docs/src/understand/api-client-perspective/img/swagger.png and /dev/null differ diff --git a/docs/src/understand/api-client-perspective/swagger.md b/docs/src/understand/api-client-perspective/swagger.md index 5722c95aed..a7f1b0fb64 100644 --- a/docs/src/understand/api-client-perspective/swagger.md +++ b/docs/src/understand/api-client-perspective/swagger.md @@ -1,24 +1,106 @@ -# Swagger API documentation (all public endpoints) +# Swagger API documentation -Our staging system provides swagger documentation of our public rest +Our staging system provides [Swagger / +OpenAPI](https://swagger.io/resources/open-api/) documentation of our HTTP REST API. The swagger docs are correct by construction (compiled from the server -code), and the are complete up to bots/services and event notification +code), and they are complete up to bots/services and event notification payloads (as of 2023-01-16). -Please check the new docs first, and if you can't find what you're -looking for, double-check the old. +There are several ways to interpret this kind of documentation: -## New docs (swagger 2.0) +- Read it as a reference +- Generate client code from it +- Interactively explore the API by making requests -[new staging swagger page](https://staging-nginz-https.zinfra.io/api/swagger-ui/) +## Swagger docs (Swagger 2.0) -## Old docs (swagger 1.2) +The Swagger documentation for endpoints depends on the API version. -If you are an employee of Wire, you can log in here and try out requests in the browser; if not, you can make use of the "List Operations" button on both 1.2 and 2.0 pages to see the possible API requests. +The pages below show [Swagger / OpenAPI 2.0](https://swagger.io/specification/v2/) +docs. -Browse to our [old staging swagger page](https://staging-nginz-https.zinfra.io/swagger-ui/) to see rendered swagger documentation for the remaining endpoints. +### Public endpoints +- Version `v0`: + - [**public** + endpoints](https://staging-nginz-https.zinfra.io/v0/api/swagger-ui/) +- Version `v1`: + - [**public** + endpoints](https://staging-nginz-https.zinfra.io/v1/api/swagger-ui/) +- Version `v2`: + - [**public** + endpoints](https://staging-nginz-https.zinfra.io/v2/api/swagger-ui/) +- Version `v3`: + - [**public** + endpoints](https://staging-nginz-https.zinfra.io/v3/api/swagger-ui/) +- Unversioned: + - [**public** + endpoints](https://staging-nginz-https.zinfra.io/api/swagger-ui/) -```{image} img/swagger.png +The first part of the URL's path is the version. No specified version means +Swagger docs of the *latest* API version. This differs from other API endpoints +where no version means `v0`! New versions are added from time to time. If you +would like to look at the docs of another version (which did not exist at the +time of writing): Just update the first path element of an existing link. + +The URL pattern is `https:///v/api/swagger-ui/`. To figure +out which versions are supported by your backend, query +`https:////api-version`. + +The [API versioning](../../developer/developer/api-versioning.md) article +discusses the versioning topic in detail. + +#### Example + +To get the versions a backend (`staging-nginz-https.zinfra.io` in this case) +supports, execute: + +```sh +curl https://staging-nginz-https.zinfra.io/api-version +{"development":[3],"domain":"staging.zinfra.io","federation":false,"supported":[0,1,2]} ``` + +The URL to open in your browser for the development version `3` is +`https://staging-nginz-https.zinfra.io/v3/api/swagger-ui/`. + +### Internal endpoints + +Swagger docs for internal endpoints are served per service. I.e. there's one for +`brig`, one for `cannon`, etc.. This is because Swagger doesn't play well with +multiple actions having the same combination of HTTP method and URL path. + +- Version `v3`: + - [`brig` - **internal** (private) + endpoints](https://staging-nginz-https.zinfra.io/v3/api-internal/swagger-ui/brig) + - [`cannon` - **internal** (private) + endpoints](https://staging-nginz-https.zinfra.io/v3/api-internal/swagger-ui/cannon) + - [`cargohold` - **internal** (private) + endpoints](https://staging-nginz-https.zinfra.io/v3/api-internal/swagger-ui/cargohold) + - [`galley` - **internal** (private) + endpoints](https://staging-nginz-https.zinfra.io/v3/api-internal/swagger-ui/galley) + - [`legalhold` - **internal** (private) + endpoints](https://staging-nginz-https.zinfra.io/v3/api-internal/swagger-ui/legalhold) + - [`spar` - **internal** (private) + endpoints](https://staging-nginz-https.zinfra.io/v3/api-internal/swagger-ui/spar) +- Unversioned: + - [`brig` - **internal** (private) + endpoints](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig) + - [`cannon` - **internal** (private) + endpoints](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/cannon) + - [`cargohold` - **internal** (private) + endpoints](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/cargohold) + - [`galley` - **internal** (private) + endpoints](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/galley) + - [`legalhold` - **internal** (private) + endpoints](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/legalhold) + - [`spar` - **internal** (private) + endpoints](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/spar) + +The URL pattern is similar to that of public endpoints: +`https:///v/api-internal/swagger-ui/`. No specified version +means Swagger docs the *latest* version (as for public endpoints' Swagger docs.) + +Due to technical reasons (we started to export Swagger docs for internal +endpoints in version `v3`), there are no meaningful Swagger docs for internal +endpoints for versions `v0` to `v2`. diff --git a/docs/src/understand/federation/api.md b/docs/src/understand/federation/api.md index 7b576d9234..f993459102 100644 --- a/docs/src/understand/federation/api.md +++ b/docs/src/understand/federation/api.md @@ -200,6 +200,7 @@ to synchronize the state of the conversations of their members. remote user in a conversation (see end-to-end flows). - `on-mls-message-sent`: Receive a MLS message that originates in the calling backend - `on-new-remote-conversation`: Inform the called backend about a conversation that exists on the calling backend. This request is made before the first time the backend might learn about this conversation, e.g. when its first user is added to the conversation. +- `update-typing-indicator`: Used by the calling backend (that does not own the conversation ) to inform the backend about a change of the typing indicator status of one of its users - `on-typing-indicator-updated`: Used by the calling backend (that owns a conversation) to inform the called backend about a change of the typing indicator status of remote user - `on-user-deleted-conversations`: When a user on calling backend this request is made for all conversations on the called backend was part of - `query-group-info`: Query the MLS public group state diff --git a/docs/src/understand/mls.md b/docs/src/understand/mls.md index 591451a0ab..a762741eac 100644 --- a/docs/src/understand/mls.md +++ b/docs/src/understand/mls.md @@ -46,7 +46,7 @@ Next, MLS needs to be explictly enabled in brig. This can be configured at brig: config: optSettings: - enableMLS: true + setEnableMLS: true ``` Finally, the web applications need to be made aware of *MLS*. This is done by diff --git a/docs/src/understand/searchability.md b/docs/src/understand/searchability.md index 083faa030f..2f37fdcc09 100644 --- a/docs/src/understand/searchability.md +++ b/docs/src/understand/searchability.md @@ -5,7 +5,7 @@ You can configure how search is limited or not based on user membership in a giv There are two types of searches based on the direction of search: - **Inbound** searches mean that somebody is searching for you. Configuring the inbound search visibility means that you (or some admin) can configure whether others can find you or not. -- **Outbound** searches mean that you are searching for somebody. Configuring the outbound search visibility means that some admin can configure whether you can find other users or not. +- **Out-Bound** searches mean that you are searching for somebody. Configuring the out-bound search visibility means that some admin can configure whether you can find other users or not. There are different types of matches: @@ -14,9 +14,13 @@ There are different types of matches: ## Searching users on the same backend +```{note} +For configuring searching accross federated backends this section is irrelevant. +``` + Search visibility is controlled by three parameters on the backend: -- A team outbound configuration flag, `TeamSearchVisibility` with possible values `SearchVisibilityStandard`, `SearchVisibilityNoNameOutsideTeam` +- A team out-bound configuration flag, `TeamSearchVisibility` with possible values `SearchVisibilityStandard`, `SearchVisibilityNoNameOutsideTeam` - `SearchVisibilityStandard` means that the user can find other people outside of the team, if the searched-person inbound search allows it - `SearchVisibilityNoNameOutsideTeam` means that the user can not find any user outside the team by full text search (but exact handle search still works) @@ -28,7 +32,7 @@ Search visibility is controlled by three parameters on the backend: - A server configuration flag `searchSameTeamOnly` with possible values true, false. - - `Note`: For the same backend, this affects inbound and outbound searches (simply because all teams will be subject to this behavior) + - `Note`: For the same backend, this affects inbound and out-bound searches (simply because all teams will be subject to this behavior) - Setting this to `true` means that the all teams on that backend can only find users that belong to their team These flag are set on the backend and the clients do not need to be aware of them. @@ -45,13 +49,13 @@ The flags will influence the behavior of the search API endpoint; clients will o +------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ | Yes, `tA` | Yes, the same team `tA` | Irrelevant | Irrelevant | Irrelevant | Found | Found | +------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -| **Outbound search unrestricted** | +| **Out-Bound search unrestricted** | +------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ | Yes, `tA` | Yes, another team tB | false | `SearchVisibilityStandard` | `SearchableByAllTeams` | Found | Found | +------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ | Yes, `tA` | Yes, another team tB | false | `SearchVisibilityStandard` | `SearchableByOwnTeam` | Found | Not found | +------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ -| **Outbound search restricted** | +| **Out-Bound search restricted** | +------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ | Yes, `tA` | Yes, another team tB | true | Irrelevant | Irrelevant | Not found | Not found | +------------------------------------+---------------------------------+------------------------------------+------------------------------------------+-------------------------------------------+----------------------------------+--------------------------------------+ @@ -95,45 +99,38 @@ galley: This default value applies to all teams for which no explicit configuration of the `TeamSearchVisibility` has been set. -## Searching users on another (federated) backend - -For federated search the table above does not apply, see following table. - -```{note} -Incoming federated searches (i.e. searches from one backend to another) are considered always as being performed from a team user, even if they are performed from a personal user. - -This is because the incoming search request does not carry the information whether the user performing the search was in a team or not. +## Searching users on another federated backend -So we have to make one assumption, and we assume that they were in a team. -``` Allowing search is done at the backend configuration level by the sysadmin: -- Outbound search restrictions (`searchSameTeamOnly`, `TeamSearchVisibility`) do not apply to federated searches - - A configuration setting `FederatedUserSearchPolicy` per federating domain with these possible values: - `no_search` The federating backend is not allowed to search any users (either by exact handle or full-text). - `exact_handle_search` The federating backend may only search by exact handle - `full_search` The federating backend may search users by full text search on display name and handle. The search search results are additionally affected by `SearchVisibilityInbound` setting of each team on the backend. + The configuration value `FederatedUserSearchPolicy` is per federated domain, e.g. in the values of the wire-server chart: + + ```yaml + brig: + config: + optSettings: + setFederationDomainConfigs: + - domain: a.example.com + search_policy: no_search + - domain: a.example.com + search_policy: full_search + ``` + - The `SearchVisibilityInbound` setting applies. Since the default value for teams is `SearchableByOwnTeam` this means that for a team to be full-text searchable by users on a federating backend both - `FederatedUserSearchPolicy` needs to be set to to full_search for the federating backend - Any team that wants to be full-text searchable needs to be set to `SearchableByAllTeams` -The configuration value `FederatedUserSearchPolicy` is per federated domain, e.g. in the values of the wire-server chart: +- Out-Bound search restrictions (`searchSameTeamOnly`, `TeamSearchVisibility`) do not apply to federated searches + -```yaml -brig: - config: - optSettings: - setFederationDomainConfigs: - - domain: a.example.com - search_policy: no_search - - domain: a.example.com - search_policy: full_search -``` ### Table of possible outcomes @@ -152,95 +149,39 @@ It’s worth nothing that if two users are on two separate backend, they are als ## Changing the settings for a given team -If you need to change searchabilility for a specific team (rather than the entire backend, as above), you need to make specific calls to the API. - -### Team searchVisibility - -The team flag `searchVisibility` affects the outbound search of user searches. - -If it is set to `no-name-outside-team` for a team then all users of that team will no longer be able to find users that are not part of their team when searching. - -This also includes finding other users by by providing their exact handle. By default it is set to `standard`, which doesn't put any additional restrictions to outbound searches. - -The setting can be changed via endpoint (for more details on how to make the API calls with `curl`, read further): - -``` -GET /teams/{tid}/search-visibility - -- Shows the current TeamSearchVisibility value for the given team - -PUT /teams/{tid}/search-visibility - -- Set specific search visibility for the team - -pull-down-menu "body": - "standard" - "no-name-outside-team" -``` +### TeamFeature searchVisibilityInbound -The team feature flag `teamSearchVisibility` determines whether it is allowed to change the `searchVisibility` setting or not. +The team feature flag `searchVisibilityInbound` affects whether the team's users are searchable by users from other teams. -The default is `disabled-by-default`. +The default setting is `searchable-by-own-team` which hides users from search +results by users from other teams. If it is set to `searchable-by-all-teams` +then users of this team may be included in the results of search queries by +other users. -```{note} -Whenever this feature setting is disabled the `searchVisibility` will be reset to standard. -``` -The default setting that applies to all teams on the instance can be defined at configuration +The default setting that applies to all teams on the instance can be defined at configuration. ```yaml -settings: - featureFlags: - teamSearchVisibility: disabled-by-default # or enabled-by-default +galley: + config: + settings: + featureFlags: + searchVisibilityInbound: + defaults: + status: enabled # or "disabled" (default is "disabled") ``` -### TeamFeature searchVisibilityInbound - -The team feature flag `searchVisibilityInbound` affects if the team's users are searchable by users from other teams. - -The default setting is `searchable-by-own-team` which hides users from search results by users from other teams. - -If it is set to `searchable-by-all-teams` then users of this team may be included in the results of search queries by other users. - ```{note} -The configuration of this flag does not affect search results when the search query matches the handle exactly. - -If the handle is provdided then any user on the instance can find users. -``` - -This team feature flag can only by toggled by site-administrators with direct access to the galley instance (for more details on how to make the API calls with `curl`, read further): - -``` -PUT /i/teams/{tid}/features/search-visibility-inbound -``` - -With JSON body: - -```json -{"status": "enabled"} +Changing this setting in the instance configuration doesn't affect any users that have already been created. To affect these users please toggle the setting on a per-team basis (see below). Switching between "enabled" and "disabled" setting for the team causes a re-indexing of all the users of the team, thereby making the setting effective, e.g. changing to a "disabled" setting first, followed by changing to an "enabled" setting (or vice versa). ``` -or +#### Overriding the default setting -```json -{"status": "disabled"} -``` +Individual teams can overwrite the default setting with API calls: -Where `enabled` is equivalent to `searchable-by-all-teams` and `disabled` is equivalent to `searchable-by-own-team`. +To make API calls to set an explicit configuration for `SearchVisibilityInbound` per team, you first need to know the Team ID, which can be found in the team settings app. -The default setting that applies to all teams on the instance can be defined at configuration. - -```yaml -searchVisibilityInbound: - defaults: - status: enabled # OR disabled -``` - -Individual teams can overwrite the default setting with API calls as per above. - -### Making the API calls - -To make API calls to set an explicit configuration for\` TeamSearchVisibilityInbound\` per team, you first need to know the Team ID, which can be found in the team settings app. - -It is an `UUID` which has format like this `dcbedf9a-af2a-4f43-9fd5-525953a919e1`. +It is an [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) which has format like this `dcbedf9a-af2a-4f43-9fd5-525953a919e1`. In the following we will be using this Team ID as an example, please replace it with your own team id. @@ -267,9 +208,9 @@ Next, set up a port-forwarding from your local machine's port `9000` to the gall kubectl port-forward -n wire galley-5f4787fdc7-9l64n 9000:8080 ``` -Keep this command running until the end of these instuctions. +Keep this command running until the end of these instructions. -Please run the following commands in a seperate terminal while keeping the terminal which establishes the port-forwarding open. +Please run the following commands in a separate terminal while keeping the terminal which establishes the port-forwarding open. To see team's current setting run: @@ -281,15 +222,53 @@ curl -XGET http://localhost:9000/i/teams/dcbedf9a-af2a-4f43-9fd5-525953a919e1/fe Where `disabled` corresponds to `SearchableByOwnTeam` and enabled corresponds to `SearchableByAllTeams`. -To change the `TeamSearchVisibilityInbound` to `SearchableByAllTeams` for the team run: +To change the `SearchVisibilityInbound` to `SearchableByAllTeams` for the team run: ```sh curl -XPUT -H 'Content-Type: application/json' -d "{\"status\": \"enabled\"}" http://localhost:9000/i/teams/dcbedf9a-af2a-4f43-9fd5-525953a919e1/features/searchVisibilityInbound ``` -To change the TeamSearchVisibilityInbound to SearchableByOwnTeam for the team run: +To change the `SearchVisibilityInbound` to `SearchableByOwnTeam` for the team run: ```sh curl -XPUT -H 'Content-Type: application/json' -d "{\"status\": \"disabled\"}" http://localhost:9000/i/teams/dcbedf9a-af2a-4f43-9fd5-525953a919e1/features/searchVisibilityInbound ``` +### Team searchVisibility + +The team flag `searchVisibility` affects the out-bound search of user searches on the same backend. Federated searches are not affected by its setting. + +If it is set to `no-name-outside-team` for a team then all users of that team will no longer be able to find users that are not part of their team when searching. + +This also includes finding other users by providing their exact handle. By default it is set to `standard`, which doesn't put any additional restrictions to out-bound searches. + +The setting can be changed via endpoint (for more details on how to make the API calls with `curl`, read further): + +``` +GET /teams/{tid}/search-visibility + -- Shows the current TeamSearchVisibility value for the given team + +PUT /teams/{tid}/search-visibility + -- Set specific search visibility for the team + +pull-down-menu "body": + "standard" + "no-name-outside-team" +``` + +The team feature flag `teamSearchVisibility` determines whether it is allowed to change the `searchVisibility` setting or not. + +The default is `disabled-by-default`. + +```{note} +Whenever this feature setting is disabled the `searchVisibility` will be reset to standard. +``` + +The default setting that applies to all teams on the instance can be defined at configuration + +```yaml +settings: + featureFlags: + teamSearchVisibility: disabled-by-default # or enabled-by-default +``` + diff --git a/docs/src/understand/single-sign-on/index.md b/docs/src/understand/single-sign-on/index.md index 01317c99b5..915c8b8ee3 100644 --- a/docs/src/understand/single-sign-on/index.md +++ b/docs/src/understand/single-sign-on/index.md @@ -7,11 +7,12 @@ :glob: true :maxdepth: 1 -Single sign-on and user provisioning +Single sign-on and user provisioning: the user manual +Trouble shooting and FAQ Generic setup SSO integration with ADFS SSO integration with Azure SSO integration with Centrify SSO integration with Okta -* +Internals for the intensely curious ``` diff --git a/docs/src/understand/single-sign-on/trouble-shooting.md b/docs/src/understand/single-sign-on/trouble-shooting.md index cdc7e1204a..53b1e76fbe 100644 --- a/docs/src/understand/single-sign-on/trouble-shooting.md +++ b/docs/src/understand/single-sign-on/trouble-shooting.md @@ -1,5 +1,9 @@ (trouble-shooting-faq)= +```{contents} +:depth: 2 +``` + # Trouble shooting & FAQ ## Reporting a problem with user provisioning or SSO authentication @@ -7,75 +11,156 @@ In order for us to analyse and understand your problem, we need at least the following information up-front: - Have you followed the following instructions? - : - {ref}`FAQ ` (This document) + - {ref}`FAQ ` (This document) - [Howtos](https://docs.wire.com/how-to/single-sign-on/index.html) for supported vendors - [General documentation on the setup flow](https://support.wire.com/hc/en-us/articles/360001285718-Set-up-SSO-externally) -- Vendor information (octa, azure, centrica, other (which one)?) -- Team ID (looks like eg. `2e9a9c9c-6f83-11eb-a118-3342c6f16f4e`, can be found in team settings) +- Which vendor (or product) are you using (octa, azure, centrica, other (which one)?) +- Team ID (looks like eg. `2e9a9c9c-6f83-11eb-a118-3342c6f16f4e`, can be found in the team management app) +- User ID of the account that has the problem (alternatively: handle, email address) - What do you expect to happen? - : - eg.: "I enter login code, authenticate successfully against IdP, get redirected, and see the wire landing page." + - e.g.: "I enter login code, authenticate successfully against IdP, get redirected, and see the wire landing page." - What does happen instead? - : - Screenshots + - Screenshots - Copy the text into your report where applicable in addition to screenshots (for automatic processing). - - eg.: "instead of being logged into wire, I see the following error page: ..." + - e.g.: "instead of being logged into wire, I see the following error page: ..." - Screenshots of the Configuration (both SAML and SCIM, as applicable), including, but not limited to: - : - If you are using SAML: SAML IdP metadata file + - If you are using SAML: SAML IdP metadata file - If you are using SCIM for provisioning: Which attributes in the User schema are mapped? How? +- If you have successfully authenticated on your IdP and are + redirected into wire, and then see a white page with an error + message that contains a lot of machine-readable info: copy the full + message to the clipboard and insert it into your report. We do not + log this information for privacy reasons, but we can use it to + investigate your problem. (Hint, if you want to investigate + yourself: it's base64 encoded!) + +### notes for wire support / development + +Not officially supported IdP vendors may work out of the box, as we +are requiring a minimum amount of SAML features. + +If there are problems: collect the metadata xml and an authentication +response xml (either from the browser http logs via a more technically +savvy customer; or from the "white page with an error" mentioned +above. + +https://github.com/wireapp/saml2-web-sso supports writing [unit vendor +compatibility +tests](https://github.com/wireapp/saml2-web-sso/blob/ff9b9f445475809d1fa31ef7f2932caa0ed31613/test/Test/SAML2/WebSSO/APISpec.hs#L266-L329) +against that response value. Once that test passes, it should all +work fine. + + +## Can I use SCIM without SAML? + +Yes. Scim is a technology for onboarding alternative to the team management app, and can produce both user accounts authenticated via SAML or via email and password. (Phone may or may not work, but is not officially supported.) + +How does it work? Make sure your team has no SAML IdPs registered. Set up your SCIM peer to provision users with valid email addresses as `externalIds`. Newly provisioned users will be created in status `PendingInvitation`, and an invitation email will be sent. From here on out, the flow is exactly the same as if you had added the user to your team in the team management app. + +Upcoming features: +- support for the `emails` field in the scim user record (so you can choose non-email `externalId` values). +- flexible mapping between any number of SAML IdPs and any number of SCIM tokens in team management. + +## Can I use SAML without SCIM? + +Yes, but this is not recommended. User (de-)provisioning requires more manual work without SCIM, and some of the account information cannot be provisioned at all via SAML. + ## Can I use the same SSO login code for multiple teams? -No, but there is a good reason for it and a work-around. +Most SAML IdP products allow you to register arbitrary many apps for +arbitrary many teams, by using a different entity Id for each app/team. This is +currently supported out of the box. -Reason: we *could* implement this, but that would require that we -disable implicit user creation for those teams. Implicit user -creation means that a person who has never logged onto wire before can -use her credentials for the IdP to get access to wire, and create a -new user based on those credentials. In order for this to work, the -IdP must uniquely determine the team. +If you don't have this option, i.e. you need to serve two teams with an +IdP that has only one entity ID, please keep reading and/or contact +customer support. -Work-around: on your IdP dashboard, you can set up a separate app for -every wire team you own. Each IdP will get a different metadata file, -and can be registered with its target team only. This way, users from -different teams have different SSO logins, but the IdP operators can -still use the same user base for all teams. This has the extra -advantage that a user can be part of two teams with the same -credentials, which would be impossible even with the hypothetical fix. +### The long answer -## Can an existing user without IdP (or with a different IdP) be bound to a new IdP? +Some SAML IdP vendors do not allow to set up fresh entity IDs (issuers) +for fresh apps; instead, all apps controlled by the IdP are receiving +SAML credentials from the same issuer. -No. This is a feature we never fully implemented. Details / latest -updates: +In the past, wire has used a tuple of IdP issuer and 'NameID' +(Haskell type 'UserRef') to uniquely identity users (tables +`spar.user_v2` and `spar.issuer_idp`). -## Can the SSO feature be disabled for a team? +In order to allow one IdP to serve more than one team, this has been +changed: we now allow to identify an IdP by a combination of +entityID/issuer and wire `TeamId`. The necessary tweaks to the +protocol are listed here. + +**This extension is currently (as of 2023-02-03) not supported by the +team management app. If you need this, please contact customer +support.** + +#### what you need to know when operating a team or an instance -No, this is [not implemented](https://github.com/wireapp/wire-server/blob/7a97cb5a944ae593c729341b6f28dfa1dabc28e5/services/galley/src/Galley/API/Error.hs#L215). +No instance-level configuration is required. -## Can you remove a SAML connection? +If your IdP supports different entityID / issuer for different apps, +you don't need to change anything. -It is not possible to delete a SAML connection in the Team Settings app, however it can be overwritten with a new connection. -It is possible do delete a SAML connection directly via the API endpoint `DELETE /identity-providers/{id}`. However deleting a SAML connection also requires deleting all users that can log in with this SAML connection. To prevent accidental deletion of users this functionality is not available directly from Team Settings. +If your IdP does not support different entityID / issuer for different +apps, keep reading. At the time of writing this section, there is no +support for multi-team IdP issuers in the team management app, so you have two +options: (1) use the rest API directly; or (2) contact our customer +support and send them the link to this section. -## If you get an error when returning from your IdP +If you feel up to calling the rest API, try the following: -`Symptoms:` +- Use the above end-point `GET /sso/metadata/:tid` with your `TeamId` + for pulling the SP metadata. +- When calling `POST /identity-provider`, make sure to add + `?api_version=v2`. (`?api_version=v1` or no omission of the query + param both invoke the old behavior.) -You have successfully authenticated on your IdP and are -redirected into wire. Wire shows a white page with an error message -that contains a lot of machine-readable info. +NB: Neither version of the API allows you to provision a user with the +same Issuer and same NameID. The pair of Issuer and NameID must +always be globally unique. -`What we need from you:` +#### API changes in even more detail -- Your SSO metadata file -- The SSO login code (eg. `wire-3f61d2ce-525c-11ea-b8da-cf641a7b716a`; - you can find it in the team settings where you registered your IdP) -- The full browser page with the error message (copy it into your - clipboard and insert it into an email to us, or save the page as an - html file and send that to us) +- New query param `api_version=` for `POST + /identity-providers`. The version is stored in `spar.idp` together + with the rest of the IdP setup, and is used by `GET + /sso/initiate-login` (see below). +- `GET /sso/initiate-login` sends audience based on api_version stored + in `spar.idp`: for v1, the audience is `/sso/finalize-login`; for + v2, it's `/sso/finalize-login/:tid`. +- New end-point `POST /sso/finalize-login/:tid` that behaves + indistinguishable from `POST /sso/finalize-login`, except when more + than one IdP with the same issuer, but different teams are + registered. In that case, this end-point can process the + credentials by discriminating on the `TeamId`. +- `POST /sso/finalize-login/` remains unchanged. +- New end-point `GET /sso/metadata/:tid` returns the same SP metadata as + `GET /sso/metadata`, with the exception that it lists + `"/sso/finalize-login/:tid"` as the path of the + `AssertionConsumerService` (rather than `"/sso/finalize-login"` as + before). +- `GET /sso/metadata` remains unchanged, and still returns the old SP + metadata, without the `TeamId` in the paths. -With all this information, please get in touch with our customer -support. +#### database schema changes -## Do I need any firewall settings? +[V15](https://github.com/wireapp/wire-server/blob/b97439756cfe0721164934db1f80658b60de1e5e/services/spar/schema/src/V15.hs#L29-L43) + + +## Can an existing user without IdP (or with a different IdP) be bound to a new IdP? + +Yes, you can, by updating the user via SCIM. (If you use SAML without +SCIM, there is a way in theory, but there are no plans to implement +it.) + + +## Can the SSO feature be disabled for a team? + +No, this is [not implemented](https://github.com/wireapp/wire-server/blob/7a97cb5a944ae593c729341b6f28dfa1dabc28e5/services/galley/src/Galley/API/Error.hs#L215). But the team admin can remove all IdPs, which will effectively disable all SAML logins. + + +## Do I need to change any firewall settings in order to use SAML? No. @@ -83,6 +168,7 @@ There is nothing to be done here. There is no internet traffic between your SAML IdP and the wire service. All communication happens via the browser or app. + ## Why does the team owner have to keep using password? The user who creates the team cannot be authenticated via SSO. There @@ -92,7 +178,7 @@ that's the team owner with their password. (It is also unwise to bind that owner to SAML once it's installed. If there is ever any issue with SAML authentication that can only be -resolved by updating the IdP metadata in team settings, the owner must +resolved by updating the IdP metadata in the team management app, the owner must still have a way to authenticate in order to do that.) There is a good workaround, though: you can create a team with user A @@ -158,7 +244,7 @@ minimal example that still works, we'd be love to take a look. ## Why does the auth response not contain a reference to an auth request? (Also: can i use IdP-initiated login?) -tl;dr: Wire only supports SP-initiated login, where the user selects +**tl;dr:** Wire only supports SP-initiated login, where the user selects the auth method from inside the app's login screen. It does not support IdP-initiated login, where the user enters the app from a list of applications in the IdP UI. @@ -225,7 +311,7 @@ in your wire team: `unspecified`. 2. If email/password authentication is used, SCIM's `externalId` is mapped on wire's email address, and provisioning works like in - team settings with invitation emails. + the team management app with invitation emails. This means that if you use email/password authentication, you **must** map an email address to `externalId` on your side. With `userName` @@ -241,8 +327,8 @@ contact customer support if this causes any issues. Users may find it awkward to copy and paste the login code into the form. If they are using the webapp, an alternative is to give them -the following URL (fill in the login code that you can find in your -team settings): +the following URL (fill in the login code that you can find in the +team management app): ```bash https://wire-webapp-dev.zinfra.io/auth#sso/3c4f050a-f073-11eb-b4c9-931bceeed13e @@ -281,23 +367,3 @@ clash. Do not rely on case sensitivity of `IssuerID` or `NameID`, or on `NameID` qualifiers for distinguishing user identifiers. - -## How to report problems - -If you have a problem you cannot resolve by yourself, please get in touch. Add as much of the following details to your report as possible: - -- Are you on cloud or on-prem? (If on-prem: which instance?) -- XML IdP metadata -- SSL Login code or IdP Issuer EntityID -- NameID of the account that has the problem -- SP metadata - -Problem description, including, but not limited to: - -- what happened? -- what did you want to happen? -- what does your idp config in the wire team management app look like? -- what does your wire config in your IdP management app look like? -- Please include screenshots *and* copied text (for cut&paste when we investigate) *and* further description and comments where feasible. - -(If you can't produce some of this information of course please get in touch anyway! It'll merely be harder for us to resolve your issue quickly, and we may need to make a few extra rounds of data gathering together with you.) diff --git a/docs/src/understand/team-feature-settings.md b/docs/src/understand/team-feature-settings.md index 21733829f9..1d65c36d33 100644 --- a/docs/src/understand/team-feature-settings.md +++ b/docs/src/understand/team-feature-settings.md @@ -83,3 +83,26 @@ brig: # ... setNonceTtlSecs: 360 # 6 minutes ``` + +## MLS End-to-End Identity + +The MLS end-to-end identity team feature adds an extra level of security and practicability. If turned on, automatic device authentication ensures that team members know they are communicating with people using authenticated devices. Team members get a certificate on all their devices. + +A timer can be set to configure until when team members need to get the verification certificate. When the timer goes off, they will be logged out and get the certificate automatically on their devices. The timer is set as a unix timestamp (number of seconds that have passed since 00:00:00 UTC on Thursday, 1 January 1970) after which the period for clients to verify their identity expires. + +```yaml +galley: + # ... + config: + # ... + settings: + # ... + featureFlags: + # ... + mlsE2EId: + defaults: + status: disabled + config: + verificationExpiration: 1676377048 + lockStatus: unlocked +``` diff --git a/hack/bin/cabal-run-integration.sh b/hack/bin/cabal-run-integration.sh index 285744f0e1..57cf9a2874 100755 --- a/hack/bin/cabal-run-integration.sh +++ b/hack/bin/cabal-run-integration.sh @@ -46,7 +46,7 @@ run_integration_tests() { service_dir="$TOP_LEVEL/services/$package" cd "$service_dir" - "$TOP_LEVEL/services/integration.sh" \ + "$TOP_LEVEL/services/run-services" \ "$TOP_LEVEL/dist/$package-integration" \ -s "$service_dir/$package.integration.yaml" \ -i "$TOP_LEVEL/services/integration.yaml" \ diff --git a/hack/bin/generate-local-nix-packages.sh b/hack/bin/generate-local-nix-packages.sh index abfdfeb925..c993e71188 100755 --- a/hack/bin/generate-local-nix-packages.sh +++ b/hack/bin/generate-local-nix-packages.sh @@ -4,7 +4,7 @@ set -euo pipefail SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) ROOT_DIR=$(cd -- "$SCRIPT_DIR/../../" &> /dev/null && pwd) -cabalFiles=$(find "$ROOT_DIR" -name '*.cabal' \ +cabalFiles=$(find -L "$ROOT_DIR" -name '*.cabal' \ | grep -v dist-newstyle | sort) warningFile=$(mktemp) @@ -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; nixpkgs-fmt default.nix &> /dev/null' + | xargs -I {} bash -c 'cd $(dirname {}); cabal2nix . --no-hpack --extra-arguments gitignoreSource | sed "s/src = \.\/\./src = gitignoreSource .\/./g" >> default.nix; nixpkgs-fmt default.nix &> /dev/null' overridesFile="$ROOT_DIR/nix/local-haskell-packages.nix" diff --git a/hack/bin/integration-logs-relevant-bits.sh b/hack/bin/integration-logs-relevant-bits.sh new file mode 100755 index 0000000000..96b836ca90 --- /dev/null +++ b/hack/bin/integration-logs-relevant-bits.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -euo pipefail + +USAGE=" +Filter out garbage from logs (but keep colors and highlight problems). + +(Adapted from logfilter.sh by Stefan Matting) + +To use it pipe log output from integration-test.sh into this tool. + +Usage: logs | $0 [options] +" + +red_color="\x1b\[;1m\x1b\[31m" +problem_markers="Failures:|FAILED|ExitFailure""\ +|\^+""\ +|FAIL""\ +|tests failed""\ +|Test suite failure""\ +|""$red_color""error:""\ +|""$red_color""\ +|Test suite .+ failed" + +exit_usage() { + echo "$USAGE" + exit 1 +} + +# remove debug/info logs +# often this is just noise like connection to cassandra. +excludeLogEntries() { + grep -v '^{".*Info"' | + grep -v '^{".*Debug"' | + grep -v '^20.*, D, .*socket: [0-9]\+>$' +} + +cleanup() { + # replace backspaces with newlines + # remove "Progress" lines + # Remove blank lines + # add newline between interleaved test name and log output lines + sed 's/\x08\+/\n/g' | + sed '/^Progress [0-9]\+/d' | + sed '/^\s\+$/d' | + sed 's/:\s\+{/:\n{/g' +} + +grepper() { + # print 10 lines before/after for context + rg "$problem_markers" --color=always -A 10 -B 10 + echo -e "\033[0m" +} + +cleanup | excludeLogEntries | grepper diff --git a/hack/bin/integration-setup-federation.sh b/hack/bin/integration-setup-federation.sh index 7795226bab..f569928239 100755 --- a/hack/bin/integration-setup-federation.sh +++ b/hack/bin/integration-setup-federation.sh @@ -7,6 +7,7 @@ TOP_LEVEL="$DIR/../.." export NAMESPACE=${NAMESPACE:-test-integration} HELMFILE_ENV=${HELMFILE_ENV:-default} CHARTS_DIR="${TOP_LEVEL}/.local/charts" +HELM_PARALLELISM=${HELM_PARALLELISM:-1} . "$DIR/helm_overrides.sh" ${DIR}/integration-cleanup.sh @@ -20,9 +21,8 @@ ${DIR}/integration-cleanup.sh # (e.g. cassandra from underneath databases-ephemeral) echo "updating recursive dependencies ..." charts=(fake-aws databases-ephemeral redis-cluster wire-server nginx-ingress-controller nginx-ingress-services) -for chart in "${charts[@]}"; do - "$DIR/update.sh" "$CHARTS_DIR/$chart" -done +mkdir -p ~/.parallel && touch ~/.parallel/will-cite +printf '%s\n' "${charts[@]}" | parallel -P "${HELM_PARALLELISM}" "$DIR/update.sh" "$CHARTS_DIR/{}" # FUTUREWORK: use helm functions instead, see https://wearezeta.atlassian.net/browse/SQPIT-723 echo "Generating self-signed certificates..." diff --git a/hack/bin/integration-setup.sh b/hack/bin/integration-setup.sh index 634cc3a49f..59cf0e4f84 100755 --- a/hack/bin/integration-setup.sh +++ b/hack/bin/integration-setup.sh @@ -7,6 +7,7 @@ TOP_LEVEL="$DIR/../.." export NAMESPACE=${NAMESPACE:-test-integration} HELMFILE_ENV=${HELMFILE_ENV:-default} CHARTS_DIR="${TOP_LEVEL}/.local/charts" +HELM_PARALLELISM=${HELM_PARALLELISM:-1} . "$DIR/helm_overrides.sh" @@ -14,9 +15,8 @@ CHARTS_DIR="${TOP_LEVEL}/.local/charts" echo "updating recursive dependencies ..." charts=(fake-aws databases-ephemeral redis-cluster wire-server nginx-ingress-controller nginx-ingress-services) -for chart in "${charts[@]}"; do - "$DIR/update.sh" "$CHARTS_DIR/$chart" -done +mkdir -p ~/.parallel && touch ~/.parallel/will-cite +printf '%s\n' "${charts[@]}" | parallel -P "${HELM_PARALLELISM}" "$DIR/update.sh" "$CHARTS_DIR/{}" echo "Generating self-signed certificates..." export FEDERATION_DOMAIN_BASE="$NAMESPACE.svc.cluster.local" diff --git a/hack/bin/integration-test.sh b/hack/bin/integration-test.sh index 8ffb21d8c7..47c5db9c3d 100755 --- a/hack/bin/integration-test.sh +++ b/hack/bin/integration-test.sh @@ -1,9 +1,93 @@ #!/usr/bin/env bash set -euo pipefail +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" NAMESPACE=${NAMESPACE:-test-integration} +# set to 1 to disable running helm tests in parallel +HELM_PARALLELISM=${HELM_PARALLELISM:-1} +CLEANUP_LOCAL_FILES=${CLEANUP_LOCAL_FILES:-1} # set to 0 to keep files -echo "Running integration tests on wire-server" +echo "Running integration tests on wire-server with parallelism=${HELM_PARALLELISM} ..." CHART=wire-server -helm test --logs -n "${NAMESPACE}" "${NAMESPACE}-${CHART}" --timeout 900s +tests=(galley cargohold gundeck federator spar brig) + +cleanup() { + if (( CLEANUP_LOCAL_FILES > 0 )); then + for t in "${tests[@]}"; do + rm -f "stat-$t" + rm -f "logs-$t" + done + fi +} + +summary() { + echo "===============" + echo "=== summary ===" + echo "===============" + printf '%s\n' "${tests[@]}" | parallel echo "=== tail {}: ===" ';' tail -2 logs-{} + + for t in "${tests[@]}"; do + x=$(cat "stat-$t") + if ((x > 0)); then + echo "$t-integration FAILED ❌. pfff..." + else + echo "$t-integration passed ✅." + fi + done +} + +# Run tests in parallel using GNU parallel (see https://www.gnu.org/software/parallel/) +# The below commands are a little convoluted, but we wish to: +# - run integration tests. If they fail, keep track of this, but still go and get logs, so we see what failed +# - run all tests. Perhaps multiple flaky tests in multiple services exist, if so, we wish to see all problems +mkdir -p ~/.parallel && touch ~/.parallel/will-cite +printf '%s\n' "${tests[@]}" | parallel echo "Running helm tests for {}..." +printf '%s\n' "${tests[@]}" | parallel -P "${HELM_PARALLELISM}" \ + helm test -n "${NAMESPACE}" "${NAMESPACE}-${CHART}" --timeout 900s --filter name="${NAMESPACE}-${CHART}-{}-integration" '> logs-{};' \ + echo '$? > stat-{};' \ + echo "==== Done testing {}. ====" '};' \ + kubectl -n "${NAMESPACE}" logs "${NAMESPACE}-${CHART}-{}-integration" '>> logs-{};' + +summary + +# in case any integration test suite failed, exit this script with an error. +exit_code=0 +for t in "${tests[@]}"; do + x=$(cat "stat-$t") + if ((x > 0)); then + exit_code=1 + fi +done + +if ((exit_code > 0)); then + echo "=======================" + echo "=== failed job logs ===" + echo "=======================" + # in case a integration test suite failed, print relevant logs + for t in "${tests[@]}"; do + x=$(cat "stat-$t") + if ((x > 0)); then + echo "=== logs for failed $t-integration ===" + cat "logs-$t" + fi + done + summary + for t in "${tests[@]}"; do + x=$(cat "stat-$t") + if ((x > 0)); then + echo "=== (relevant) logs for failed $t-integration ===" + "$DIR/integration-logs-relevant-bits.sh" < "logs-$t" + fi + done + summary +fi + +cleanup + +if ((exit_code > 0)); then + echo "Tests failed." + exit 1 +else + echo "All integration tests passed ✅." +fi diff --git a/hack/bin/register_idp.sh b/hack/bin/register_idp.sh index 8b08e8d95e..d2e0930ca3 100755 --- a/hack/bin/register_idp.sh +++ b/hack/bin/register_idp.sh @@ -53,9 +53,9 @@ else fi payload="{\"email\":\"$login\",\"password\":\"$password\"}" -test -n "$trace" && echo "$curl_exe -is --show-error -XPOST https://$backend/login -H'Content-type: application/json' -d\"$payload\"" -access_token=$($curl_exe -s --show-error -XPOST https://$backend/login -H'Content-type: application/json' -d"$payload" | $jq_exe -r .access_token) +test -n "$trace" && echo "$curl_exe -is --show-error -XPOST \"https://$backend/login\" -H'Content-type: application/json' -d\"$payload\"" +access_token=$($curl_exe -s --show-error -XPOST "https://$backend/login" -H'Content-type: application/json' -d"$payload" | $jq_exe -r .access_token) # register idp -test -n "$trace" && echo "$curl_exe -is --show-error -XPOST https://$backend/identity-providers -H\"Authorization: Bearer $access_token\" -H'Content-type: application/xml' -d@\"$metadata_file\"" -$curl_exe -is --show-error -XPOST https://$backend/identity-providers -H"Authorization: Bearer $access_token" -H'Content-type: application/xml' -d@"$metadata_file" +test -n "$trace" && echo "$curl_exe -is --show-error -XPOST \"https://$backend/identity-providers\" -H\"Authorization: Bearer $access_token\" -H'Content-type: application/xml' -d@\"$metadata_file\"" +$curl_exe -is --show-error -XPOST "https://$backend/identity-providers" -H"Authorization: Bearer $access_token" -H'Content-type: application/xml' -d@"$metadata_file" diff --git a/hack/bin/split-member-constraints.py b/hack/bin/split-member-constraints.py old mode 100644 new mode 100755 index 28a54e688c..17fa49542b --- a/hack/bin/split-member-constraints.py +++ b/hack/bin/split-member-constraints.py @@ -25,8 +25,11 @@ def make_constraint(e): def f(m): effects = re.split(r'\s*,\s*', m.group(1)) - constraints = ', '.join(make_constraint(e) for e in effects) - return f'({constraints})' + constraints = [make_constraint(e) for e in effects] + s = ',\n '.join(constraints) + if len(constraints) > 1: + s = f'({s})' + return s code = open(sys.argv[1]).read() print(re.sub(r"Members\s+'\[\s*([^\]]*)\s*\]\s+r", f, code, flags=re.MULTILINE), diff --git a/hack/bin/upload-images.sh b/hack/bin/upload-images.sh index 79c0798f2c..dc94d15a63 100755 --- a/hack/bin/upload-images.sh +++ b/hack/bin/upload-images.sh @@ -28,10 +28,7 @@ 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 - printf '*** Uploading image %s\n' "$image_name" - "$SCRIPT_DIR/upload-image.sh" "wireServer.$IMAGES_ATTR.$image_name" -done <"$image_list_file" +xargs -I {} -P 10 "$SCRIPT_DIR/upload-image.sh" "wireServer.$IMAGES_ATTR.{}" < "$image_list_file" for image_name in nginz nginz-disco; do printf '*** Uploading image %s\n' "$image_name" diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 789cdf16b9..503fee55e4 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -34,7 +34,6 @@ brig: requests: {} limits: memory: 512Mi - cpu: 500m config: externalUrls: nginz: https://kube-staging-nginz-https.zinfra.io @@ -53,8 +52,8 @@ brig: providerTokenTimeout: 60 enableFederator: true # keep in sync with galley.config.enableFederator, cargohold.config.enableFederator and tags.federator! optSettings: - setActivationTimeout: 5 - setVerificationTimeout: 5 + setActivationTimeout: 10 + setVerificationTimeout: 10 # keep this in sync with brigSettingsTeamInvitationTimeout in spar/templates/tests/configmap.yaml setTeamInvitationTimeout: 10 setExpiredUserCleanupTimeout: 1 @@ -130,7 +129,6 @@ cannon: requests: {} limits: memory: 512Mi - cpu: 500m drainTimeout: 0 cargohold: replicaCount: 1 @@ -139,7 +137,6 @@ cargohold: requests: {} limits: memory: 512Mi - cpu: 500m config: aws: s3Bucket: dummy-bucket @@ -192,7 +189,6 @@ gundeck: requests: {} limits: memory: 1024Mi - cpu: 1000m config: cassandra: host: cassandra-ephemeral @@ -249,7 +245,6 @@ spar: requests: {} limits: memory: 1024Mi - cpu: 1000m config: tlsDisableCertValidation: true cassandra: diff --git a/hack/helmfile.yaml b/hack/helmfile.yaml index 9dd863334c..6392d64a43 100644 --- a/hack/helmfile.yaml +++ b/hack/helmfile.yaml @@ -19,7 +19,7 @@ environments: - namespaceFed2: {{ requiredEnv "NAMESPACE_2" }} - federationDomainFed2: {{ requiredEnv "FEDERATION_DOMAIN_2" }} - imagePullPolicy: Always - - redisStorageClass: csi-hostpath-sc + - redisStorageClass: hcloud-volumes kind: values: - namespace: {{ requiredEnv "NAMESPACE_1" }} @@ -129,6 +129,8 @@ releases: value: {{ .Values.federationDomain }} - name: brig.config.optSettings.setFederationDomainConfigs[0].domain value: {{ .Values.federationDomainFed2 }} + needs: + - '{{ .Values.namespace }}-databases-ephemeral' - name: '{{ .Values.namespace }}-wire-server-2' namespace: '{{ .Values.namespaceFed2 }}' @@ -145,3 +147,5 @@ releases: value: {{ .Values.federationDomainFed2 }} - name: brig.config.optSettings.setFederationDomainConfigs[0].domain value: {{ .Values.federationDomain }} + needs: + - '{{ .Values.namespace }}-databases-ephemeral-2' diff --git a/libs/api-bot/api-bot.cabal b/libs/api-bot/api-bot.cabal index 6f544d716d..75b936e1d4 100644 --- a/libs/api-bot/api-bot.cabal +++ b/libs/api-bot/api-bot.cabal @@ -71,6 +71,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: aeson >=2.0.1.0 diff --git a/libs/api-client/api-client.cabal b/libs/api-client/api-client.cabal index 864d9cb7f5..ab3277d7ae 100644 --- a/libs/api-client/api-client.cabal +++ b/libs/api-client/api-client.cabal @@ -70,6 +70,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: aeson >=2.0.1.0 diff --git a/libs/api-client/src/Network/Wire/Client/API/Conversation.hs b/libs/api-client/src/Network/Wire/Client/API/Conversation.hs index da0324b4f7..3dc4ace781 100644 --- a/libs/api-client/src/Network/Wire/Client/API/Conversation.hs +++ b/libs/api-client/src/Network/Wire/Client/API/Conversation.hs @@ -141,6 +141,6 @@ createConv users name = sessionRequest req rsc readBody method POST . path "conversations" . acceptJson - . json (NewConv users [] (name >>= checked) mempty Nothing Nothing Nothing Nothing roleNameWireAdmin M.ProtocolProteusTag Nothing) + . json (NewConv users [] (name >>= checked) mempty Nothing Nothing Nothing Nothing roleNameWireAdmin M.ProtocolProteusTag) $ empty rsc = status201 :| [] diff --git a/libs/api-client/src/Network/Wire/Client/API/Push.hs b/libs/api-client/src/Network/Wire/Client/API/Push.hs index 30f91155e3..be4aca51f1 100644 --- a/libs/api-client/src/Network/Wire/Client/API/Push.hs +++ b/libs/api-client/src/Network/Wire/Client/API/Push.hs @@ -93,7 +93,7 @@ data Notification = Notification } awaitNotifications :: - (MonadSession m, Functor m) => + MonadSession m => -- TODO: Maybe ClientId (Notification -> IO ()) -> m (Async ()) diff --git a/libs/api-client/src/Network/Wire/Client/API/Search.hs b/libs/api-client/src/Network/Wire/Client/API/Search.hs index 0d96eb72c5..1bf421a3db 100644 --- a/libs/api-client/src/Network/Wire/Client/API/Search.hs +++ b/libs/api-client/src/Network/Wire/Client/API/Search.hs @@ -27,7 +27,6 @@ module Network.Wire.Client.API.Search where import Bilge -import Control.Monad.Catch (MonadMask) import qualified Data.ByteString.Char8 as C import Data.Default.Class import Data.List.NonEmpty @@ -51,7 +50,7 @@ instance Default SearchParams where def = SearchParams "" 2 10 True -- | Search for already connected and/or potential contacts. -search :: (MonadSession m, MonadUnliftIO m, MonadMask m) => SearchParams -> m (SearchResult Contact) +search :: MonadSession m => SearchParams -> m (SearchResult Contact) search SearchParams {..} = sessionRequest req rsc readBody where req = diff --git a/libs/api-client/src/Network/Wire/Client/API/User.hs b/libs/api-client/src/Network/Wire/Client/API/User.hs index 7013d48c70..3bec4bbe0f 100644 --- a/libs/api-client/src/Network/Wire/Client/API/User.hs +++ b/libs/api-client/src/Network/Wire/Client/API/User.hs @@ -77,7 +77,7 @@ activateKey (ActivationKey key) (ActivationCode code) = do ------------------------------------------------------------------------------- -- Authenticated -getSelfProfile :: (MonadSession m, MonadUnliftIO m, MonadMask m) => m User +getSelfProfile :: MonadSession m => m User getSelfProfile = sessionRequest req rsc readBody where req = @@ -87,7 +87,7 @@ getSelfProfile = sessionRequest req rsc readBody $ empty rsc = status200 :| [] -getProfile :: (MonadSession m, MonadUnliftIO m, MonadMask m) => UserId -> m UserProfile +getProfile :: MonadSession m => UserId -> m UserProfile getProfile uid = sessionRequest req rsc readBody where req = @@ -97,7 +97,7 @@ getProfile uid = sessionRequest req rsc readBody $ empty rsc = status200 :| [] -connectTo :: (MonadSession m, MonadUnliftIO m, MonadMask m) => ConnectionRequest -> m UserConnection +connectTo :: MonadSession m => ConnectionRequest -> m UserConnection connectTo cr = sessionRequest req rsc readBody where req = @@ -108,7 +108,7 @@ connectTo cr = sessionRequest req rsc readBody $ empty rsc = status201 :| [status200] -updateConnection :: (MonadSession m, MonadUnliftIO m, MonadMask m) => UserId -> ConnectionUpdate -> m UserConnection +updateConnection :: MonadSession m => UserId -> ConnectionUpdate -> m UserConnection updateConnection u cu = sessionRequest req rsc readBody where req = @@ -119,7 +119,7 @@ updateConnection u cu = sessionRequest req rsc readBody $ empty rsc = status200 :| [] -getConnection :: (MonadSession m, MonadUnliftIO m, MonadMask m) => UserId -> m (Maybe UserConnection) +getConnection :: (MonadSession m, MonadMask m) => UserId -> m (Maybe UserConnection) getConnection u = do rs <- sessionRequest req rsc consumeBody case statusCode rs of diff --git a/libs/api-client/src/Network/Wire/Client/HTTP.hs b/libs/api-client/src/Network/Wire/Client/HTTP.hs index 148b444f15..fb25fcc18c 100644 --- a/libs/api-client/src/Network/Wire/Client/HTTP.hs +++ b/libs/api-client/src/Network/Wire/Client/HTTP.hs @@ -62,7 +62,7 @@ instance FromJSON Error where clientRequest :: forall m a. - (Log.MonadLogger m, MonadClient m, MonadUnliftIO m, MonadMask m) => + (MonadClient m, MonadUnliftIO m, MonadMask m) => -- | The request to send. Request -> -- | Expected response codes. diff --git a/libs/bilge/bilge.cabal b/libs/bilge/bilge.cabal index 1e24fdc361..931b4f55fc 100644 --- a/libs/bilge/bilge.cabal +++ b/libs/bilge/bilge.cabal @@ -71,6 +71,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: aeson >=2.0.1.0 diff --git a/libs/bilge/src/Bilge/Assert.hs b/libs/bilge/src/Bilge/Assert.hs index 2a584e5b6d..62662a740e 100644 --- a/libs/bilge/src/Bilge/Assert.hs +++ b/libs/bilge/src/Bilge/Assert.hs @@ -1,4 +1,6 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- @@ -77,7 +79,7 @@ newtype Assertions a = Assertions -- assertion that failed). It will also return the response, -- so it can be used for further inspection. ( + (HasCallStack, MonadIO m, MonadCatch m) => m (Response (Maybe Lazy.ByteString)) -> Assertions () -> m (Response (Maybe Lazy.ByteString)) @@ -95,12 +97,12 @@ io String msg (i, Just m) = printf "%2d: " i ++ err m msg _ = "" - printErr :: MonadIO m => SomeException -> m a + printErr :: SomeException -> m a printErr e = error $ title "Error executing request: " ++ err (show e) -- | Like ' + (HasCallStack, MonadIO m, MonadCatch m) => m (Response (Maybe Lazy.ByteString)) -> Assertions () -> m () diff --git a/libs/bilge/src/Bilge/IO.hs b/libs/bilge/src/Bilge/IO.hs index 18ed2b5b65..5765d9877e 100644 --- a/libs/bilge/src/Bilge/IO.hs +++ b/libs/bilge/src/Bilge/IO.hs @@ -117,6 +117,7 @@ handleRequest :: MonadHttp m => Request -> m (Response (Maybe LByteString)) handleRequest req = handleRequestWithCont req consumeBody instance MonadIO m => MonadHttp (HttpT m) where + handleRequestWithCont :: Request -> (Response BodyReader -> IO a) -> HttpT m a handleRequestWithCont req h = do m <- ask liftIO $ withResponse req m h @@ -210,7 +211,7 @@ instance MonadUnliftIO m => MonadUnliftIO (HttpT m) where withRunInIO $ \run -> inner (run . runHttpT r) -runHttpT :: Monad m => Manager -> HttpT m a -> m a +runHttpT :: Manager -> HttpT m a -> m a runHttpT m h = runReaderT (unwrap h) m -- | Given a 'Request' builder function, perform an actual HTTP request using the @@ -224,7 +225,7 @@ get, options, trace, patch :: - (MonadIO m, MonadHttp m) => + MonadHttp m => (Request -> Request) -> m (Response (Maybe LByteString)) get f = httpLbs empty (method GET . f) @@ -244,7 +245,7 @@ get', options', trace', patch' :: - (MonadIO m, MonadHttp m) => + MonadHttp m => Request -> (Request -> Request) -> m (Response (Maybe LByteString)) @@ -258,14 +259,14 @@ trace' r f = httpLbs r (method TRACE . f) patch' r f = httpLbs r (method PATCH . f) httpLbs :: - (MonadIO m, MonadHttp m) => + MonadHttp m => Request -> (Request -> Request) -> m (Response (Maybe LByteString)) httpLbs r f = http r f consumeBody http :: - (MonadIO m, MonadHttp m) => + MonadHttp m => Request -> (Request -> Request) -> (Response BodyReader -> IO a) -> diff --git a/libs/bilge/src/Bilge/RPC.hs b/libs/bilge/src/Bilge/RPC.hs index 34bd8d059b..e1db9d0b2c 100644 --- a/libs/bilge/src/Bilge/RPC.hs +++ b/libs/bilge/src/Bilge/RPC.hs @@ -105,7 +105,7 @@ rpcExceptionMsg (RPCException sys req ex) = hdr (k, v) x = x ~~ original k .= v statusCheck :: - (MonadError e m, MonadIO m) => + (MonadError e m) => Int -> (LText -> e) -> Response (Maybe LByteString) -> @@ -116,7 +116,7 @@ statusCheck c f r = f ("unexpected status code: " <> pack (show $ statusCode r)) parseResponse :: - (Exception e, MonadThrow m, Monad m, FromJSON a) => + (Exception e, MonadThrow m, FromJSON a) => (LText -> e) -> Response (Maybe LByteString) -> m a diff --git a/libs/bilge/src/Bilge/Response.hs b/libs/bilge/src/Bilge/Response.hs index f8019c32f3..fc6f16ea14 100644 --- a/libs/bilge/src/Bilge/Response.hs +++ b/libs/bilge/src/Bilge/Response.hs @@ -1,4 +1,6 @@ {-# LANGUAGE OverloadedStrings #-} +-- Disabling due to HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- diff --git a/libs/brig-types/brig-types.cabal b/libs/brig-types/brig-types.cabal index 357a127075..90839e99cb 100644 --- a/libs/brig-types/brig-types.cabal +++ b/libs/brig-types/brig-types.cabal @@ -70,7 +70,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -funbox-strict-fields + -funbox-strict-fields -Wredundant-constraints build-depends: aeson >=2.0.1.0 @@ -151,7 +151,7 @@ test-suite brig-types-tests ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -with-rtsopts=-N + -threaded -with-rtsopts=-N -Wredundant-constraints build-depends: aeson >=2.0.1.0 diff --git a/libs/cargohold-types/cargohold-types.cabal b/libs/cargohold-types/cargohold-types.cabal index b490be25c4..02f6dc2473 100644 --- a/libs/cargohold-types/cargohold-types.cabal +++ b/libs/cargohold-types/cargohold-types.cabal @@ -60,6 +60,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: base >=4 && <5 diff --git a/libs/cassandra-util/cassandra-util.cabal b/libs/cassandra-util/cassandra-util.cabal index d7dfaeca72..a7ca1e9234 100644 --- a/libs/cassandra-util/cassandra-util.cabal +++ b/libs/cassandra-util/cassandra-util.cabal @@ -64,6 +64,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: aeson >=2.0.1.0 diff --git a/libs/cassandra-util/src/Cassandra/Exec.hs b/libs/cassandra-util/src/Cassandra/Exec.hs index ae7589deb3..df74bac370 100644 --- a/libs/cassandra-util/src/Cassandra/Exec.hs +++ b/libs/cassandra-util/src/Cassandra/Exec.hs @@ -45,7 +45,7 @@ import Database.CQL.Protocol (Error, QueryParams (QueryParams), Tuple, pagingSta import qualified Database.CQL.Protocol as Protocol import Imports hiding (init) -params :: Tuple a => Consistency -> a -> QueryParams a +params :: Consistency -> a -> QueryParams a params c p = QueryParams c False p Nothing Nothing Nothing Nothing {-# INLINE params #-} @@ -80,7 +80,7 @@ data CassandraError | Other !SomeException deriving (Show) -syncCassandra :: (Functor m, MonadIO m, MonadCatch m) => m a -> m (Either CassandraError a) +syncCassandra :: (MonadIO m, MonadCatch m) => m a -> m (Either CassandraError a) syncCassandra m = catches (Right <$> m) diff --git a/libs/deriving-swagger2/deriving-swagger2.cabal b/libs/deriving-swagger2/deriving-swagger2.cabal index 9f1e8dc6bd..c05b0dcd5c 100644 --- a/libs/deriving-swagger2/deriving-swagger2.cabal +++ b/libs/deriving-swagger2/deriving-swagger2.cabal @@ -57,6 +57,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: base >=4 && <5 diff --git a/libs/dns-util/dns-util.cabal b/libs/dns-util/dns-util.cabal index 9b03100539..e4c2629464 100644 --- a/libs/dns-util/dns-util.cabal +++ b/libs/dns-util/dns-util.cabal @@ -62,6 +62,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: base >=4.6 && <5.0 @@ -124,7 +125,7 @@ test-suite spec ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -rtsopts -with-rtsopts=-N + -threaded -rtsopts -with-rtsopts=-N -Wredundant-constraints build-tool-depends: hspec-discover:hspec-discover build-depends: diff --git a/libs/extended/extended.cabal b/libs/extended/extended.cabal index b58883b2c1..da2004dc1f 100644 --- a/libs/extended/extended.cabal +++ b/libs/extended/extended.cabal @@ -68,6 +68,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: aeson @@ -143,7 +144,7 @@ test-suite extended-tests ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -with-rtsopts=-N + -threaded -with-rtsopts=-N -Wredundant-constraints build-tool-depends: hspec-discover:hspec-discover build-depends: diff --git a/libs/extended/src/Servant/API/Extended.hs b/libs/extended/src/Servant/API/Extended.hs index f87fc5d214..2d6914c7f1 100644 --- a/libs/extended/src/Servant/API/Extended.hs +++ b/libs/extended/src/Servant/API/Extended.hs @@ -21,6 +21,7 @@ module Servant.API.Extended where import qualified Data.ByteString.Lazy as BL import Data.EitherR (fmapL) +import Data.Kind import Data.Metrics.Servant import Data.String.Conversions (cs) import Data.Typeable @@ -54,12 +55,12 @@ import Prelude () -- that'll be). -- -- See also: https://github.com/haskell-servant/servant/issues/353 -data ReqBodyCustomError' (mods :: [*]) (list :: [ct]) (tag :: Symbol) (a :: *) +data ReqBodyCustomError' (mods :: [Type]) (list :: [ct]) (tag :: Symbol) (a :: Type) type ReqBodyCustomError = ReqBodyCustomError' '[Required, Strict] -- | Custom parse error for bad request bodies. -class MakeCustomError (tag :: Symbol) (a :: *) where +class MakeCustomError (tag :: Symbol) (a :: Type) where makeCustomError :: String -> ServerError -- | Variant of the 'ReqBody'' instance that takes a 'ServerError' as argument instead of a diff --git a/libs/galley-types/galley-types.cabal b/libs/galley-types/galley-types.cabal index 7ca1661580..005902f1d9 100644 --- a/libs/galley-types/galley-types.cabal +++ b/libs/galley-types/galley-types.cabal @@ -15,12 +15,10 @@ library Galley.Types Galley.Types.Bot Galley.Types.Bot.Service - Galley.Types.Conversations.Intra Galley.Types.Conversations.Members Galley.Types.Conversations.One2One Galley.Types.Conversations.Roles Galley.Types.Teams - Galley.Types.Teams.Intra other-modules: Paths_galley_types hs-source-dirs: src @@ -67,6 +65,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: aeson >=2.0.1.0 @@ -146,7 +145,7 @@ test-suite galley-types-tests ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -with-rtsopts=-N + -threaded -with-rtsopts=-N -Wredundant-constraints build-depends: aeson diff --git a/libs/galley-types/src/Galley/Types/Teams.hs b/libs/galley-types/src/Galley/Types/Teams.hs index a06c1b0ea0..96d410f5ab 100644 --- a/libs/galley-types/src/Galley/Types/Teams.hs +++ b/libs/galley-types/src/Galley/Types/Teams.hs @@ -38,7 +38,9 @@ module Galley.Types.Teams flagsTeamFeatureValidateSAMLEmailsStatus, flagTeamFeatureSndFactorPasswordChallengeStatus, flagTeamFeatureSearchVisibilityInbound, + flagOutlookCalIntegration, flagMLS, + flagMlsE2EId, Defaults (..), ImplicitLockStatus (..), unImplicitLockStatus, @@ -150,7 +152,9 @@ data FeatureFlags = FeatureFlags _flagsTeamFeatureValidateSAMLEmailsStatus :: !(Defaults (ImplicitLockStatus ValidateSAMLEmailsConfig)), _flagTeamFeatureSndFactorPasswordChallengeStatus :: !(Defaults (WithStatus SndFactorPasswordChallengeConfig)), _flagTeamFeatureSearchVisibilityInbound :: !(Defaults (ImplicitLockStatus SearchVisibilityInboundConfig)), - _flagMLS :: !(Defaults (ImplicitLockStatus MLSConfig)) + _flagMLS :: !(Defaults (ImplicitLockStatus MLSConfig)), + _flagOutlookCalIntegration :: !(Defaults (WithStatus OutlookCalIntegrationConfig)), + _flagMlsE2EId :: !(Defaults (WithStatus MlsE2EIdConfig)) } deriving (Eq, Show, Generic) @@ -200,6 +204,8 @@ instance FromJSON FeatureFlags where <*> (fromMaybe (Defaults (defFeatureStatus @SndFactorPasswordChallengeConfig)) <$> (obj .:? "sndFactorPasswordChallenge")) <*> withImplicitLockStatusOrDefault obj "searchVisibilityInbound" <*> withImplicitLockStatusOrDefault obj "mls" + <*> (fromMaybe (Defaults (defFeatureStatus @OutlookCalIntegrationConfig)) <$> (obj .:? "outlookCalIntegration")) + <*> (fromMaybe (Defaults (defFeatureStatus @MlsE2EIdConfig)) <$> (obj .:? "mlsE2EId")) where withImplicitLockStatusOrDefault :: forall cfg. (IsFeatureConfig cfg, Schema.ToSchema cfg) => Object -> Key -> A.Parser (Defaults (ImplicitLockStatus cfg)) withImplicitLockStatusOrDefault obj fieldName = fromMaybe (Defaults (ImplicitLockStatus (defFeatureStatus @cfg))) <$> obj .:? fieldName @@ -220,6 +226,8 @@ instance ToJSON FeatureFlags where sndFactorPasswordChallenge searchVisibilityInbound mls + outlookCalIntegration + mlsE2EId ) = object [ "sso" .= sso, @@ -234,7 +242,9 @@ instance ToJSON FeatureFlags where "validateSAMLEmails" .= validateSAMLEmails, "sndFactorPasswordChallenge" .= sndFactorPasswordChallenge, "searchVisibilityInbound" .= searchVisibilityInbound, - "mls" .= mls + "mls" .= mls, + "outlookCalIntegration" .= outlookCalIntegration, + "mlsE2EId" .= mlsE2EId ] instance FromJSON FeatureSSO where diff --git a/libs/galley-types/test/unit/Test/Galley/Types.hs b/libs/galley-types/test/unit/Test/Galley/Types.hs index 3e92614977..756ccebc2a 100644 --- a/libs/galley-types/test/unit/Test/Galley/Types.hs +++ b/libs/galley-types/test/unit/Test/Galley/Types.hs @@ -24,7 +24,6 @@ import Control.Lens import Data.Set hiding (drop) import qualified Data.Set as Set import Galley.Types.Teams -import Galley.Types.Teams.Intra (GuardLegalholdPolicyConflicts) import Imports import Test.Galley.Roundtrip (testRoundTrip) import qualified Test.QuickCheck as QC @@ -49,7 +48,6 @@ tests = assertBool "owner.self" ((rolePermissions r2 ^. self) `isSubsetOf` (rolePermissions r1 ^. self)) assertBool "owner.copy" ((rolePermissions r2 ^. copy) `isSubsetOf` (rolePermissions r1 ^. copy)), testRoundTrip @FeatureFlags, - testRoundTrip @GuardLegalholdPolicyConflicts, testGroup "permissionsRole, rolePermissions" [ testCase "'Role' maps to expected permissions" $ do @@ -98,6 +96,8 @@ instance Arbitrary FeatureFlags where <*> arbitrary <*> fmap (fmap unlocked) arbitrary <*> fmap (fmap unlocked) arbitrary + <*> arbitrary + <*> arbitrary where unlocked :: ImplicitLockStatus a -> ImplicitLockStatus a unlocked = ImplicitLockStatus . Public.setLockStatus Public.LockStatusUnlocked . _unImplicitLockStatus diff --git a/libs/gundeck-types/gundeck-types.cabal b/libs/gundeck-types/gundeck-types.cabal index 31918ad98c..65c12a9a7b 100644 --- a/libs/gundeck-types/gundeck-types.cabal +++ b/libs/gundeck-types/gundeck-types.cabal @@ -64,6 +64,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: aeson >=2.0.1.0 diff --git a/libs/gundeck-types/src/Gundeck/Types/Common.hs b/libs/gundeck-types/src/Gundeck/Types/Common.hs index 37208a1339..e9bc0c4690 100644 --- a/libs/gundeck-types/src/Gundeck/Types/Common.hs +++ b/libs/gundeck-types/src/Gundeck/Types/Common.hs @@ -57,5 +57,5 @@ instance ToByteString URI where instance FromByteString URI where parser = takeByteString >>= parse . Bytes.unpack -parse :: (Monad m, MonadFail m) => String -> m URI +parse :: MonadFail m => String -> m URI parse = maybe (fail "Invalid URI") (pure . URI) . Net.parseURI diff --git a/libs/hscim/default.nix b/libs/hscim/default.nix index ff9dda2955..7bd207e273 100644 --- a/libs/hscim/default.nix +++ b/libs/hscim/default.nix @@ -174,7 +174,7 @@ mkDerivation { warp ]; testToolDepends = [ hspec-discover ]; - homepage = "httpsgitignoreSource ./.github.cogitignoreSource ./.ireapgitignoreSource ./.ire-servegitignoreSource ./.ibgitignoreSource ./.scigitignoreSource ./.EADME.md"; + homepage = "https://github.com/wireapp/wire-server/libs/hscim/README.md"; description = "hscim json schema and server implementation"; license = lib.licenses.agpl3Only; mainProgram = "hscim-server"; diff --git a/libs/hscim/hscim.cabal b/libs/hscim/hscim.cabal index 6ae8cbdae8..7dab82296c 100644 --- a/libs/hscim/hscim.cabal +++ b/libs/hscim/hscim.cabal @@ -81,46 +81,46 @@ library TypeOperators TypeSynonymInstances - ghc-options: -Wall -Werror + ghc-options: -Wall -Werror -Wredundant-constraints build-depends: - aeson >=2 - , aeson-qq >=0.8.2 && <0.9 - , attoparsec >=0.13.2 && <0.15 - , base >=4.12 && <4.15 - , bytestring >=0.10.8 && <0.11 - , case-insensitive >=1.2.1.0 && <1.3 - , email-validate >=2.3.2 && <2.4 - , hashable >=1.2.7 && <1.5 - , hedgehog >=1.0.1 && <1.1 - , hspec >=2.7.1 && <2.9 - , hspec-expectations >=0.8.2 && <0.9 - , hspec-wai >=0.9.2 && <0.10 - , http-api-data >=0.4.1 && <0.5 - , http-media >=0.8.0 && <0.9 - , http-types >=0.12.3 && <0.13 - , hw-hspec-hedgehog >=0.1.0 && <0.2 - , list-t >=1.0.4 && <1.1 - , microlens >=0.4.10 && <0.5 - , mmorph >=1.1.3 && <1.2 - , mtl >=2.2.2 && <2.3 - , network-uri >=2.6.2 && <2.7 - , retry >=0.8.1.0 && <0.10 - , scientific >=0.3.6 && <0.4 - , servant >=0.16.2 && <0.20 - , servant-client >=0.16.2 && <0.20 - , servant-client-core >=0.16.2 && <0.20 - , servant-server >=0.16.2 && <0.20 - , stm >=2.5.0 && <2.6 - , stm-containers >=1.1.0 && <1.3 - , string-conversions >=0.4.0 && <0.5 - , template-haskell >=2.14.0 && <2.17 - , text >=1.2.3 && <1.3 - , time >=1.8.0 && <1.10 - , unordered-containers >=0.2.10 && <0.3 - , uuid >=1.3.13 && <1.4 - , wai >=3.2.2 && <3.3 - , wai-extra >=3.0.28 && <3.2 - , warp >=3.2.28 && <3.4 + 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 default-language: Haskell2010 @@ -146,47 +146,50 @@ executable hscim-server TypeOperators TypeSynonymInstances - ghc-options: -Wall -Werror -threaded -rtsopts -with-rtsopts=-N + ghc-options: + -Wall -Werror -threaded -rtsopts -with-rtsopts=-N + -Wredundant-constraints + build-depends: - aeson >=2 - , aeson-qq >=0.8.2 && <0.9 - , attoparsec >=0.13.2 && <0.15 - , base >=4.12 && <4.15 - , bytestring >=0.10.8 && <0.11 - , case-insensitive >=1.2.1.0 && <1.3 - , email-validate >=2.3.2 && <2.4 - , hashable >=1.2.7 && <1.5 - , hedgehog >=1.0.1 && <1.1 + aeson + , aeson-qq + , attoparsec + , base + , bytestring + , case-insensitive + , email-validate + , hashable + , hedgehog , hscim - , hspec >=2.7.1 && <2.9 - , hspec-expectations >=0.8.2 && <0.9 - , hspec-wai >=0.9.2 && <0.10 - , http-api-data >=0.4.1 && <0.5 - , http-media >=0.8.0 && <0.9 - , http-types >=0.12.3 && <0.13 - , hw-hspec-hedgehog >=0.1.0 && <0.2 - , list-t >=1.0.4 && <1.1 - , microlens >=0.4.10 && <0.5 - , mmorph >=1.1.3 && <1.2 - , mtl >=2.2.2 && <2.3 - , network-uri >=2.6.2 && <2.7 - , retry >=0.8.1.0 && <0.10 - , scientific >=0.3.6 && <0.4 - , servant >=0.16.2 && <0.20 - , servant-client >=0.16.2 && <0.20 - , servant-client-core >=0.16.2 && <0.20 - , servant-server >=0.16.2 && <0.20 - , stm >=2.5.0 && <2.6 - , stm-containers >=1.1.0 && <1.3 - , string-conversions >=0.4.0 && <0.5 - , template-haskell >=2.14.0 && <2.17 - , text >=1.2.3 && <1.3 - , time >=1.8.0 && <1.10 - , unordered-containers >=0.2.10 && <0.3 - , uuid >=1.3.13 && <1.4 - , wai >=3.2.2 && <3.3 - , wai-extra >=3.0.28 && <3.2 - , warp >=3.2.28 && <3.4 + , 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 default-language: Haskell2010 @@ -229,48 +232,51 @@ test-suite spec TypeOperators TypeSynonymInstances - ghc-options: -Wall -Werror -threaded -rtsopts -with-rtsopts=-N + ghc-options: + -Wall -Werror -threaded -rtsopts -with-rtsopts=-N + -Wredundant-constraints + build-tool-depends: hspec-discover:hspec-discover build-depends: - aeson >=2 - , aeson-qq >=0.8.2 && <0.9 - , attoparsec >=0.13.2 && <0.15 - , base >=4.12 && <4.15 - , bytestring >=0.10.8 && <0.11 - , case-insensitive >=1.2.1.0 && <1.3 - , email-validate >=2.3.2 && <2.4 - , hashable >=1.2.7 && <1.5 - , hedgehog >=1.0.1 && <1.1 + aeson + , aeson-qq + , attoparsec + , base + , bytestring + , case-insensitive + , email-validate + , hashable + , hedgehog , hscim - , hspec >=2.7.1 && <2.9 - , hspec-expectations >=0.8.2 && <0.9 - , hspec-wai >=0.9.2 && <0.10 - , http-api-data >=0.4.1 && <0.5 - , http-media >=0.8.0 && <0.9 - , http-types >=0.12.3 && <0.13 - , hw-hspec-hedgehog >=0.1.0 && <0.2 + , hspec + , hspec-expectations + , hspec-wai + , http-api-data + , http-media + , http-types + , hw-hspec-hedgehog , indexed-traversable - , list-t >=1.0.4 && <1.1 - , microlens >=0.4.10 && <0.5 - , mmorph >=1.1.3 && <1.2 - , mtl >=2.2.2 && <2.3 - , network-uri >=2.6.2 && <2.7 - , retry >=0.8.1.0 && <0.10 - , scientific >=0.3.6 && <0.4 - , servant >=0.16.2 && <0.20 - , servant-client >=0.16.2 && <0.20 - , servant-client-core >=0.16.2 && <0.20 - , servant-server >=0.16.2 && <0.20 - , stm >=2.5.0 && <2.6 - , stm-containers >=1.1.0 && <1.3 - , string-conversions >=0.4.0 && <0.5 - , template-haskell >=2.14.0 && <2.17 - , text >=1.2.3 && <1.3 - , time >=1.8.0 && <1.10 - , unordered-containers >=0.2.10 && <0.3 - , uuid >=1.3.13 && <1.4 - , wai >=3.2.2 && <3.3 - , wai-extra >=3.0.28 && <3.2 - , warp >=3.2.28 && <3.4 + , 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 default-language: Haskell2010 diff --git a/libs/hscim/src/Web/Scim/Class/Group.hs b/libs/hscim/src/Web/Scim/Class/Group.hs index 6643d27383..d5af1f65e7 100644 --- a/libs/hscim/src/Web/Scim/Class/Group.hs +++ b/libs/hscim/src/Web/Scim/Class/Group.hs @@ -171,7 +171,7 @@ class (Monad m, GroupTypes tag, AuthDB tag m) => GroupDB tag m where groupServer :: forall tag m. - (Show (GroupId tag), GroupDB tag m) => + GroupDB tag m => Maybe (AuthData tag) -> GroupSite tag (AsServerT (ScimHandler m)) groupServer authData = diff --git a/libs/hscim/src/Web/Scim/Client.hs b/libs/hscim/src/Web/Scim/Client.hs index 477e90b427..fee613ac87 100644 --- a/libs/hscim/src/Web/Scim/Client.hs +++ b/libs/hscim/src/Web/Scim/Client.hs @@ -164,16 +164,12 @@ deleteUser env tok = case users @tag (scimClients env) tok of ((_ :<|> (_ :<|> _ -- groups getGroups :: - forall tag. - HasScimClient tag => ClientEnv -> Maybe (AuthData tag) -> IO (ListResponse (StoredGroup tag)) getGroups = error "groups are not authenticated at the moment; implement that first!" getGroup :: - forall tag. - HasScimClient tag => ClientEnv -> Maybe (AuthData tag) -> GroupId tag -> @@ -181,8 +177,6 @@ getGroup :: getGroup = error "groups are not authenticated at the moment; implement that first!" postGroup :: - forall tag. - HasScimClient tag => ClientEnv -> Maybe (AuthData tag) -> Group -> @@ -190,8 +184,6 @@ postGroup :: postGroup = error "groups are not authenticated at the moment; implement that first!" putGroup :: - forall tag. - HasScimClient tag => ClientEnv -> Maybe (AuthData tag) -> GroupId tag -> @@ -199,8 +191,6 @@ putGroup :: putGroup = error "groups are not authenticated at the moment; implement that first!" patchGroup :: - forall tag. - HasScimClient tag => ClientEnv -> Maybe (AuthData tag) -> GroupId tag -> @@ -208,8 +198,6 @@ patchGroup :: patchGroup = error "groups are not authenticated at the moment; implement that first!" deleteGroup :: - forall tag. - HasScimClient tag => ClientEnv -> Maybe (AuthData tag) -> GroupId tag -> diff --git a/libs/hscim/src/Web/Scim/Server.hs b/libs/hscim/src/Web/Scim/Server.hs index 2559a942d9..db8176ae12 100644 --- a/libs/hscim/src/Web/Scim/Server.hs +++ b/libs/hscim/src/Web/Scim/Server.hs @@ -85,7 +85,7 @@ data Site tag route = Site siteServer :: forall tag m. - (DB tag m, Show (GroupId tag)) => + DB tag m => Configuration -> Site tag (AsServerT (ScimHandler m)) siteServer conf = diff --git a/libs/hscim/src/Web/Scim/Test/Util.hs b/libs/hscim/src/Web/Scim/Test/Util.hs index e05dfad157..601cdaab59 100644 --- a/libs/hscim/src/Web/Scim/Test/Util.hs +++ b/libs/hscim/src/Web/Scim/Test/Util.hs @@ -1,6 +1,7 @@ {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TupleSections #-} +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- @@ -82,17 +83,17 @@ import Web.Scim.Schema.User (UserTypes (..)) -- FUTUREWORK: make this a PR upstream. (while we're at it, we can also patch 'WaiSession' -- and 'request' to keep track of the 'SRequest', and add that to the error message here with -- the response.) -shouldRespondWith :: HasCallStack => WaiSession SResponse -> ResponseMatcher -> WaiExpectation +shouldRespondWith :: HasCallStack => WaiSession st SResponse -> ResponseMatcher -> WaiExpectation st shouldRespondWith action matcher = either (liftIO . expectationFailure) pure =<< doesRespondWith action matcher -doesRespondWith :: HasCallStack => WaiSession SResponse -> ResponseMatcher -> WaiSession (Either String ()) +doesRespondWith :: HasCallStack => WaiSession st SResponse -> ResponseMatcher -> WaiSession st (Either String ()) doesRespondWith action matcher = do r <- action let extmsg = " details: " <> show r <> "\n" pure $ maybe (Right ()) (Left . (<> extmsg)) (match r matcher) -shouldEventuallyRespondWith :: HasCallStack => WaiSession SResponse -> ResponseMatcher -> WaiExpectation +shouldEventuallyRespondWith :: HasCallStack => WaiSession st SResponse -> ResponseMatcher -> WaiExpectation st shouldEventuallyRespondWith action matcher = either (liftIO . expectationFailure) pure =<< Retry.retrying @@ -150,31 +151,31 @@ defAcceptanceQueryConfig = AcceptanceQueryConfig {..} a' = maybe a (\(t, l) -> if l == '/' then t else a) $ BS8.unsnoc a b' = maybe b (\(h, t) -> if h == '/' then t else b) $ BS8.uncons b -post :: ByteString -> L.ByteString -> WaiSession SResponse +post :: ByteString -> L.ByteString -> WaiSession st SResponse post path = request methodPost path [(hContentType, "application/scim+json")] -put :: ByteString -> L.ByteString -> WaiSession SResponse +put :: ByteString -> L.ByteString -> WaiSession st SResponse put path = request methodPut path [(hContentType, "application/scim+json")] -patch :: ByteString -> L.ByteString -> WaiSession SResponse +patch :: ByteString -> L.ByteString -> WaiSession st SResponse patch path = request methodPatch path [(hContentType, "application/scim+json")] -request' :: Method -> AcceptanceQueryConfig tag -> ByteString -> L.ByteString -> WaiSession SResponse +request' :: Method -> AcceptanceQueryConfig tag -> ByteString -> L.ByteString -> WaiSession st SResponse request' method (AcceptanceQueryConfig prefix token) path = request method (prefix path) [(hAuthorization, token), (hContentType, "application/scim+json")] -get' :: AcceptanceQueryConfig tag -> ByteString -> WaiSession SResponse +get' :: AcceptanceQueryConfig tag -> ByteString -> WaiSession st SResponse get' cfg path = request' methodGet cfg path "" -post' :: AcceptanceQueryConfig tag -> ByteString -> L.ByteString -> WaiSession SResponse +post' :: AcceptanceQueryConfig tag -> ByteString -> L.ByteString -> WaiSession st SResponse post' = request' methodPost -put' :: AcceptanceQueryConfig tag -> ByteString -> L.ByteString -> WaiSession SResponse +put' :: AcceptanceQueryConfig tag -> ByteString -> L.ByteString -> WaiSession st SResponse put' = request' methodPut -patch' :: AcceptanceQueryConfig tag -> ByteString -> L.ByteString -> WaiSession SResponse +patch' :: AcceptanceQueryConfig tag -> ByteString -> L.ByteString -> WaiSession st SResponse patch' = request' methodPatch -delete' :: AcceptanceQueryConfig tag -> ByteString -> L.ByteString -> WaiSession SResponse +delete' :: AcceptanceQueryConfig tag -> ByteString -> L.ByteString -> WaiSession st SResponse delete' = request' methodDelete ---------------------------------------------------------------------------- diff --git a/libs/hscim/test/Test/Capabilities/MetaSchemaSpec.hs b/libs/hscim/test/Test/Capabilities/MetaSchemaSpec.hs index bff1aa4068..ec456f4d8e 100644 --- a/libs/hscim/test/Test/Capabilities/MetaSchemaSpec.hs +++ b/libs/hscim/test/Test/Capabilities/MetaSchemaSpec.hs @@ -28,7 +28,6 @@ import qualified Data.List as List import Data.Text (Text) import Network.Wai.Test (SResponse (..)) import Servant -import Servant.API.Generic import Test.Hspec hiding (shouldSatisfy) import qualified Test.Hspec.Expectations as Expect import Test.Hspec.Wai hiding (patch, post, put, shouldRespondWith) @@ -44,9 +43,9 @@ app = do shouldSatisfy :: (Show a, FromJSON a) => - WaiSession SResponse -> + WaiSession st SResponse -> (a -> Bool) -> - WaiExpectation + WaiExpectation st shouldSatisfy resp predicate = do maybeDecoded <- eitherDecode . simpleBody <$> resp case maybeDecoded of @@ -69,7 +68,7 @@ coreSchemas = ] spec :: Spec -spec = beforeAll app $ do +spec = with app $ do describe "GET /Schemas" $ do it "lists schemas" $ do get "/Schemas" `shouldRespondWith` 200 diff --git a/libs/hscim/test/Test/Class/AuthSpec.hs b/libs/hscim/test/Test/Class/AuthSpec.hs index a5130476d5..9f5b063271 100644 --- a/libs/hscim/test/Test/Class/AuthSpec.hs +++ b/libs/hscim/test/Test/Class/AuthSpec.hs @@ -35,7 +35,7 @@ testStorage :: IO TestStorage testStorage = TestStorage <$> STMMap.newIO <*> STMMap.newIO spec :: Spec -spec = beforeAll ((\s -> app @Mock empty (nt s)) <$> testStorage) $ do +spec = with ((\s -> app @Mock empty (nt s)) <$> testStorage) $ do describe "/ServiceProviderConfig" $ do it "is accessible without authentication" $ do get "/ServiceProviderConfig" `shouldRespondWith` 200 diff --git a/libs/hscim/test/Test/Class/GroupSpec.hs b/libs/hscim/test/Test/Class/GroupSpec.hs index 7d3a472854..a48f92aa3c 100644 --- a/libs/hscim/test/Test/Class/GroupSpec.hs +++ b/libs/hscim/test/Test/Class/GroupSpec.hs @@ -43,7 +43,7 @@ app = do (nt storage) spec :: Spec -spec = beforeAll app $ do +spec = with app $ do describe "GET & POST /Groups" $ do it "responds with [] in empty environment" $ do get "/" `shouldRespondWith` emptyList @@ -54,17 +54,21 @@ spec = beforeAll app $ do it "responds with 404 for unknown group" $ do get "/9999" `shouldRespondWith` 404 it "retrieves stored group" $ do + post "/" adminGroup `shouldRespondWith` 201 -- the test implementation stores groups with uid [0,1..n-1] get "/0" `shouldRespondWith` admins describe "PUT /Groups/:id" $ do it "adds member to existing group" $ do + post "/" adminGroup `shouldRespondWith` 201 put "/0" adminUpdate0 `shouldRespondWith` updatedAdmins0 it "does not create new group" $ do put "/9999" adminGroup `shouldRespondWith` 404 describe "DELETE /Groups/:id" $ do it "responds with 404 for unknown group" $ do + post "/" adminGroup `shouldRespondWith` 201 delete "/Users/unknown" `shouldRespondWith` 404 it "deletes a stored group" $ do + post "/" adminGroup `shouldRespondWith` 201 delete "/0" `shouldRespondWith` 204 -- group should be gone get "/0" `shouldRespondWith` 404 diff --git a/libs/hscim/test/Test/Class/UserSpec.hs b/libs/hscim/test/Test/Class/UserSpec.hs index deebc04840..3d3d16d0e1 100644 --- a/libs/hscim/test/Test/Class/UserSpec.hs +++ b/libs/hscim/test/Test/Class/UserSpec.hs @@ -39,7 +39,7 @@ app = do pure $ mkapp @Mock (Proxy @(UserAPI Mock)) (toServant (userServer auth)) (nt storage) spec :: Spec -spec = beforeAll app $ do +spec = with app $ do describe "GET & POST /Users" $ do it "responds with [] in empty environment" $ do get "/" `shouldRespondWith` emptyList @@ -49,17 +49,23 @@ spec = beforeAll app $ do get "/" `shouldRespondWith` allUsers describe "filtering" $ do it "can filter by username" $ do + post "/" newBarbara `shouldRespondWith` 201 get "/?filter=userName eq \"bjensen\"" `shouldRespondWith` onlyBarbara it "is case-insensitive regarding syntax" $ do + post "/" newBarbara `shouldRespondWith` 201 get "/?filter=USERName EQ \"bjensen\"" `shouldRespondWith` onlyBarbara it "is case-insensitive regarding usernames" $ do + post "/" newBarbara `shouldRespondWith` 201 get "/?filter=userName eq \"BJensen\"" `shouldRespondWith` onlyBarbara it "handles malformed filter syntax" $ do + post "/" newBarbara `shouldRespondWith` 201 get "/?filter=userName eqq \"bjensen\"" `shouldRespondWith` 400 -- TODO: would be nice to check the error message as well it "handles type errors in comparisons" $ do + post "/" newBarbara `shouldRespondWith` 201 get "/?filter=userName eq true" `shouldRespondWith` 400 + describe "GET /Users/:id" $ do it "responds with 404 for unknown user" $ do get "/9999" `shouldRespondWith` 404 @@ -68,12 +74,15 @@ spec = beforeAll app $ do xit "responds with 401 for unparseable user ID" $ do get "/unparseable" `shouldRespondWith` 401 it "retrieves stored user" $ do + post "/" newBarbara `shouldRespondWith` 201 -- the test implementation stores users with uid [0,1..n-1] get "/0" `shouldRespondWith` barbara describe "PUT /Users/:id" $ do it "overwrites the user" $ do + post "/" newBarbara `shouldRespondWith` 201 put "/0" barbUpdate0 `shouldRespondWith` updatedBarb0 it "does not create new users" $ do + post "/" newBarbara `shouldRespondWith` 201 put "/9999" newBarbara `shouldRespondWith` 404 -- TODO(arianvp): Perhaps we want to make this an acceptance spec. describe "PATCH /Users/:id" $ do @@ -82,6 +91,7 @@ spec = beforeAll app $ do -- TODO(arianvp): We need to merge multi-value fields, but not supported yet -- TODO(arianvp): Add and Replace tests currently identical, because of lack of multi-value it "adds all fields if no target" $ do + post "/" newBarbara `shouldRespondWith` 201 _ <- put "/0" smallUser -- reset patch "/0" @@ -116,6 +126,7 @@ spec = beforeAll app $ do { matchStatus = 200 } it "adds fields if they didn't exist yet" $ do + post "/" newBarbara `shouldRespondWith` 201 _ <- put "/0" smallUser -- reset patch "/0" @@ -147,6 +158,7 @@ spec = beforeAll app $ do { matchStatus = 200 } it "replaces individual simple fields" $ do + post "/" newBarbara `shouldRespondWith` 201 _ <- put "/0" smallUser -- reset patch "/0" @@ -183,6 +195,7 @@ spec = beforeAll app $ do -- not limit by type what fields it lenses in to. It is a very untyped -- thingy currently. it "PatchOp is atomic. Either fully applies or not at all" $ do + post "/" newBarbara `shouldRespondWith` 201 _ <- put "/0" smallUser -- reset patch "/0" @@ -202,6 +215,7 @@ spec = beforeAll app $ do describe "Replace" $ do -- TODO(arianvp): Implement and test multi-value fields properly it "adds all fields if no target" $ do + post "/" newBarbara `shouldRespondWith` 201 _ <- put "/0" smallUser -- reset patch "/0" @@ -236,6 +250,7 @@ spec = beforeAll app $ do { matchStatus = 200 } it "adds fields if they didn't exist yet" $ do + post "/" newBarbara `shouldRespondWith` 201 _ <- put "/0" smallUser -- reset patch "/0" @@ -267,6 +282,7 @@ spec = beforeAll app $ do { matchStatus = 200 } it "replaces individual simple fields" $ do + post "/" newBarbara `shouldRespondWith` 201 _ <- put "/0" smallUser -- reset patch "/0" @@ -298,6 +314,7 @@ spec = beforeAll app $ do { matchStatus = 200 } it "PatchOp is atomic. Either fully applies or not at all" $ do + post "/" newBarbara `shouldRespondWith` 201 _ <- put "/0" smallUser -- reset patch "/0" @@ -316,6 +333,7 @@ spec = beforeAll app $ do get "/0" `shouldRespondWith` smallUserGet {matchStatus = 200} describe "Remove" $ do it "fails if no target" $ do + post "/" newBarbara `shouldRespondWith` 201 _ <- put "/0" barbUpdate0 -- reset patch "/0" @@ -329,6 +347,7 @@ spec = beforeAll app $ do { matchStatus = 400 } it "fails if removing immutable" $ do + post "/" newBarbara `shouldRespondWith` 201 _ <- put "/0" barbUpdate0 -- reset patch "/0" @@ -342,6 +361,7 @@ spec = beforeAll app $ do { matchStatus = 400 } it "deletes the specified attribute" $ do + post "/" newBarbara `shouldRespondWith` 201 _ <- put "/0" smallUser -- reset patch "/0" @@ -369,6 +389,7 @@ spec = beforeAll app $ do it "responds with 404 for unknown user" $ do delete "/9999" `shouldRespondWith` 404 it "deletes a stored user" $ do + post "/" newBarbara `shouldRespondWith` 201 delete "/0" `shouldRespondWith` 204 -- user should be gone get "/0" `shouldRespondWith` 404 diff --git a/libs/hscim/test/Test/Schema/UserSpec.hs b/libs/hscim/test/Test/Schema/UserSpec.hs index 6f7ae9180a..deff894b70 100644 --- a/libs/hscim/test/Test/Schema/UserSpec.hs +++ b/libs/hscim/test/Test/Schema/UserSpec.hs @@ -1,6 +1,7 @@ {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE ViewPatterns #-} +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} -- This file is part of the Wire Server implementation. -- diff --git a/libs/imports/imports.cabal b/libs/imports/imports.cabal index 238f033ec6..27af805297 100644 --- a/libs/imports/imports.cabal +++ b/libs/imports/imports.cabal @@ -64,6 +64,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: base diff --git a/libs/imports/src/Imports.hs b/libs/imports/src/Imports.hs index 81e583b880..4f5747f410 100644 --- a/libs/imports/src/Imports.hs +++ b/libs/imports/src/Imports.hs @@ -153,7 +153,7 @@ import Data.HashMap.Strict (HashMap) import Data.HashSet (HashSet) import Data.Int -- 'insert' and 'delete' are common in database modules -import Data.List hiding (delete, insert) +import Data.List hiding (delete, insert, singleton) -- Lazy and strict versions are the same import Data.Map (Map) import Data.Maybe @@ -161,7 +161,7 @@ import Data.Maybe import Data.Monoid hiding (First (..), Last (..)) import Data.Ord -- conflicts with Options.Applicative.Option (should we care?) -import Data.Semigroup hiding (Option, diff, option) +import Data.Semigroup hiding (diff) import Data.Set (Set) import Data.String import Data.Text (Text) diff --git a/libs/jwt-tools/jwt-tools.cabal b/libs/jwt-tools/jwt-tools.cabal index e4eb1a6b9a..74156013d3 100644 --- a/libs/jwt-tools/jwt-tools.cabal +++ b/libs/jwt-tools/jwt-tools.cabal @@ -58,6 +58,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: base @@ -102,6 +103,7 @@ test-suite jwt-tools-tests ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints default-extensions: NoImplicitPrelude diff --git a/libs/jwt-tools/src/Data/Jwt/Tools.hs b/libs/jwt-tools/src/Data/Jwt/Tools.hs index 11edd177bf..23360373fc 100644 --- a/libs/jwt-tools/src/Data/Jwt/Tools.hs +++ b/libs/jwt-tools/src/Data/Jwt/Tools.hs @@ -47,13 +47,13 @@ import Foreign.Storable (peek) import Imports import Network.HTTP.Types (StdMethod (..)) -data JwtResponse +data HsResult type ProofCStr = CString type UserIdCStr = CString -type ClientIdWord16 = Word16 +type ClientIdWord64 = Word64 type DomainCStr = CString @@ -75,7 +75,7 @@ foreign import ccall unsafe "generate_dpop_access_token" generate_dpop_access_token :: ProofCStr -> UserIdCStr -> - ClientIdWord16 -> + ClientIdWord64 -> DomainCStr -> NonceCStr -> UrlCStr -> @@ -84,18 +84,18 @@ foreign import ccall unsafe "generate_dpop_access_token" ExpiryEpochWord64 -> EpochWord64 -> BackendBundleCStr -> - IO (Ptr JwtResponse) + IO (Ptr HsResult) -foreign import ccall unsafe "free_dpop_access_token" free_dpop_access_token :: Ptr JwtResponse -> IO () +foreign import ccall unsafe "free_dpop_access_token" free_dpop_access_token :: Ptr HsResult -> IO () -foreign import ccall unsafe "get_error" get_error :: Ptr JwtResponse -> Ptr CUChar +foreign import ccall unsafe "get_error" get_error :: Ptr HsResult -> Ptr CUChar -foreign import ccall unsafe "get_token" get_token :: Ptr JwtResponse -> CString +foreign import ccall unsafe "get_token" get_token :: Ptr HsResult -> CString generateDpopAccessTokenFfi :: ProofCStr -> UserIdCStr -> - ClientIdWord16 -> + ClientIdWord64 -> DomainCStr -> NonceCStr -> UrlCStr -> @@ -104,21 +104,21 @@ generateDpopAccessTokenFfi :: ExpiryEpochWord64 -> EpochWord64 -> BackendBundleCStr -> - IO (Maybe (Ptr JwtResponse)) + IO (Maybe (Ptr HsResult)) generateDpopAccessTokenFfi dpopProof user client domain nonce uri method maxSkewSecs expiration now backendKeys = do ptr <- generate_dpop_access_token dpopProof user client domain nonce uri method maxSkewSecs expiration now backendKeys if ptr /= nullPtr then pure $ Just ptr else pure Nothing -getErrorFfi :: Ptr JwtResponse -> IO (Maybe Word8) +getErrorFfi :: Ptr HsResult -> IO (Maybe Word8) getErrorFfi ptr = do let errorPtr = get_error ptr if errorPtr /= nullPtr then Just . fromIntegral <$> peek errorPtr else pure Nothing -getTokenFfi :: Ptr JwtResponse -> IO (Maybe String) +getTokenFfi :: Ptr HsResult -> IO (Maybe String) getTokenFfi ptr = do let tokenPtr = get_token ptr if tokenPtr /= nullPtr @@ -190,7 +190,7 @@ generateDpopToken dpopProof uid cid domain nonce uri method maxSkewSecs maxExpir PATCH -> "PATCH" toResult :: Maybe Word8 -> Maybe String -> Either DPoPTokenGenerationError ByteString --- the only valid case is when the error=0 (meaning no error) and the token is not null +-- the only valid cases are when the error=0 (meaning no error) or nothing and the token is not null toResult (Just 0) (Just token) = Right $ cs token toResult Nothing (Just token) = Right $ cs token -- errors @@ -212,7 +212,7 @@ newtype UserId = UserId {_unUserId :: ByteString} deriving (Eq, Show) deriving newtype (ToByteString) -newtype ClientId = ClientId {_unClientId :: Word16} +newtype ClientId = ClientId {_unClientId :: Word64} deriving (Eq, Show) deriving newtype (ToByteString) diff --git a/libs/jwt-tools/test/Spec.hs b/libs/jwt-tools/test/Spec.hs index 77b702f649..0c54be31ca 100644 --- a/libs/jwt-tools/test/Spec.hs +++ b/libs/jwt-tools/test/Spec.hs @@ -23,11 +23,14 @@ import Test.Hspec main :: IO () main = hspec $ do - describe "generateDpopToken FFI" $ do - it "should return a value" $ do - actual <- callFFIWithConstValues - let expected = Right $ cs token - actual `shouldBe` expected + describe "generateDpopToken FFI when passing valid inputs" $ do + it "should return an access token" $ do + actual <- callFFIWithValidValues + isRight actual `shouldBe` True + describe "generateDpopToken FFI when passing nonsense values" $ do + it "should return an error" $ do + actual <- callFFIWithNonsenseValues + isRight actual `shouldBe` False describe "toResult" $ do it "should convert to correct error" $ do toResult Nothing (Just token) `shouldBe` Right (cs token) @@ -68,28 +71,52 @@ main = hspec $ do toResult (Just 18) Nothing `shouldBe` Left ExpError toResult (Just 18) (Just token) `shouldBe` Left ExpError toResult Nothing Nothing `shouldBe` Left UnknownError + where + token :: String + token = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" -token :: String -token = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" +callFFIWithNonsenseValues :: IO (Either DPoPTokenGenerationError ByteString) +callFFIWithNonsenseValues = + runExceptT $ generateDpopToken proof uid cid domain nonce uri method maxSkewSecs expires now pem + where + proof = Proof "xxxx.yyyy.zzzz" + uid = UserId "8a6e8a6e-8a6e-8a6e-8a6e-8a6e8a6e8a6e" + cid = ClientId 8899 + domain = Domain "example.com" + nonce = Nonce "123" + uri = Uri "/foo" + method = POST + maxSkewSecs = MaxSkewSecs 1 + now = NowEpoch 5435234232 + expires = ExpiryEpoch $ 5435234232 + 360 + pem = + PemBundle $ + "-----BEGIN PRIVATE KEY-----\n\ + \MC4CAQAwBQYDK2VwBCIEIFANnxZLNE4p+GDzWzR3wm/v8x/0bxZYkCyke1aTRucX\n\ + \-----END PRIVATE KEY-----\n\ + \-----BEGIN PUBLIC KEY-----\n\ + \MCowBQYDK2VwAyEACPvhIdimF20tOPjbb+fXJrwS2RKDp7686T90AZ0+Th8=\n\ + \-----END PUBLIC KEY-----\n" -callFFIWithConstValues :: IO (Either DPoPTokenGenerationError ByteString) -callFFIWithConstValues = do - let proof = Proof "xxxx.yyyy.zzzz" - let uid = UserId "8a6e8a6e-8a6e-8a6e-8a6e-8a6e8a6e8a6e" - let cid = ClientId 8899 - let domain = Domain "example.com" - let nonce = Nonce "123" - let uri = Uri "/foo" - let method = POST - let maxSkewSecs = MaxSkewSecs 1 - let now = NowEpoch 5435234232 - let expires = ExpiryEpoch $ 5435234232 + 360 - let pem = - PemBundle $ - "-----BEGIN PRIVATE KEY-----\n\ - \MC4CAQAwBQYDK2VwBCIEIFANnxZLNE4p+GDzWzR3wm/v8x/0bxZYkCyke1aTRucX\n\ - \-----END PRIVATE KEY-----\n\ - \-----BEGIN PUBLIC KEY-----\n\ - \MCowBQYDK2VwAyEACPvhIdimF20tOPjbb+fXJrwS2RKDp7686T90AZ0+Th8=\n\ - \-----END PUBLIC KEY-----\n" +callFFIWithValidValues :: IO (Either DPoPTokenGenerationError ByteString) +callFFIWithValidValues = runExceptT $ generateDpopToken proof uid cid domain nonce uri method maxSkewSecs expires now pem + where + proof = Proof "eyJhbGciOiJFZERTQSIsInR5cCI6ImRwb3Arand0IiwiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiZzQwakI3V3pmb2ZCdkxCNVlybmlZM2ZPZU1WVGtfNlpfVnNZM0tBbnpOUSJ9fQ.eyJpYXQiOjE2Nzc2NzAwODEsImV4cCI6MTY3Nzc1NjQ4MSwibmJmIjoxNjc3NjcwMDgxLCJzdWIiOiJpbXBwOndpcmVhcHA9WldKa01qY3labUk0TW1aa05ETXlZamczTm1NM1lXSmtZVFUwWkdSaU56VS8xODllNDhjNmNhODZiNWQ0QGV4YW1wbGUub3JnIiwianRpIjoiZDE5ZWExYmItNWI0Ny00ZGJiLWE1MTktNjU0ZWRmMjU0MTQ0Iiwibm9uY2UiOiJZMkZVTjJaTlExUnZSV0l6Ympsa2RGRjFjWGhHZDJKbWFXUlRiamhXZVdRIiwiaHRtIjoiUE9TVCIsImh0dSI6Imh0dHA6Ly9sb2NhbGhvc3Q6NjQwNTQvIiwiY2hhbCI6IkJpMkpkUGk1eWVTTVdhZjA5TnJEZTVUQXFjZ0FnQmE3In0._PrwHUTS7EoAflXyNDlPNqGMbjKu-JuSXwkNPyryBQdg2gDIb20amsH05Ocih78Josz9h7lAB6FvAWsXKQB1Dw" + uid = UserId "ebd272fb-82fd-432b-876c-7abda54ddb75" + cid = ClientId 1773935321869104596 + domain = Domain "example.org" + nonce = Nonce "Y2FUN2ZNQ1RvRWIzbjlkdFF1cXhGd2JmaWRTbjhWeWQ" + uri = Uri "http://localhost:64054/" + method = POST + maxSkewSecs = MaxSkewSecs 2 + now = NowEpoch 5435234232 + expires = ExpiryEpoch $ 2082008461 + pem = + PemBundle $ + "-----BEGIN PRIVATE KEY-----\n\ + \MC4CAQAwBQYDK2VwBCIEIKW3jzXCsRVgnclmiTu53Pu1/r6AUmnKDoghOOVMjozQ\n\ + \-----END PRIVATE KEY-----\n\ + \-----BEGIN PUBLIC KEY-----\n\ + \MCowBQYDK2VwAyEA7t9veqi02mPhllm44JXWga8m/l4JxUeQm3qPyMlerxY=\n\ + \-----END PUBLIC KEY-----\n" diff --git a/libs/metrics-core/metrics-core.cabal b/libs/metrics-core/metrics-core.cabal index e3fc16acea..876aeab315 100644 --- a/libs/metrics-core/metrics-core.cabal +++ b/libs/metrics-core/metrics-core.cabal @@ -61,6 +61,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: base >=4.9 diff --git a/libs/metrics-core/src/Data/Metrics.hs b/libs/metrics-core/src/Data/Metrics.hs index f7a661c331..53a88db5f0 100644 --- a/libs/metrics-core/src/Data/Metrics.hs +++ b/libs/metrics-core/src/Data/Metrics.hs @@ -147,7 +147,7 @@ toInfo (Path p) = -- | Checks whether a given key exists in a mutable hashmap (i.e. one inside an IORef) -- If it exists it is returned, if it does not then one is initialized using the provided -- initializer, then stored, then returned. -getOrCreate :: (MonadIO m, Eq k, Hashable k) => IORef (HashMap k v) -> k -> IO v -> m v +getOrCreate :: (MonadIO m, Hashable k) => IORef (HashMap k v) -> k -> IO v -> m v getOrCreate mapRef key initializer = liftIO $ do hMap <- readIORef mapRef maybe initialize pure (HM.lookup key hMap) diff --git a/libs/metrics-wai/metrics-wai.cabal b/libs/metrics-wai/metrics-wai.cabal index f5e340b24a..63854462ee 100644 --- a/libs/metrics-wai/metrics-wai.cabal +++ b/libs/metrics-wai/metrics-wai.cabal @@ -64,6 +64,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints ghc-prof-options: -fprof-auto build-depends: @@ -135,7 +136,7 @@ test-suite unit ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -with-rtsopts=-N + -threaded -with-rtsopts=-N -Wredundant-constraints build-tool-depends: hspec-discover:hspec-discover build-depends: diff --git a/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal b/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal index 56bec80f2f..934d5b0931 100644 --- a/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal +++ b/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal @@ -75,6 +75,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: base >=4.6 && <5.0 @@ -148,7 +149,7 @@ test-suite spec -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -j -Wno-redundant-constraints -Werror -threaded -rtsopts - -with-rtsopts=-N + -with-rtsopts=-N -Wredundant-constraints build-tool-depends: hspec-discover:hspec-discover build-depends: diff --git a/libs/polysemy-wire-zoo/src/Wire/Sem/Now/Input.hs b/libs/polysemy-wire-zoo/src/Wire/Sem/Now/Input.hs index a9ec505a97..1a7a84854a 100644 --- a/libs/polysemy-wire-zoo/src/Wire/Sem/Now/Input.hs +++ b/libs/polysemy-wire-zoo/src/Wire/Sem/Now/Input.hs @@ -1,3 +1,6 @@ +-- Disabling to stop warnings on FromUTC +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -24,7 +27,7 @@ import Data.Time (UTCTime) import Imports import Polysemy import Polysemy.Input -import Wire.Sem.FromUTC +import Wire.Sem.FromUTC (FromUTC (..)) import Wire.Sem.Now nowToInput :: diff --git a/libs/polysemy-wire-zoo/src/Wire/Sem/Paging.hs b/libs/polysemy-wire-zoo/src/Wire/Sem/Paging.hs index 2636eae7a9..9648b957df 100644 --- a/libs/polysemy-wire-zoo/src/Wire/Sem/Paging.hs +++ b/libs/polysemy-wire-zoo/src/Wire/Sem/Paging.hs @@ -30,13 +30,14 @@ module Wire.Sem.Paging ) where +import Data.Kind import Imports -type family Page p a :: (page :: *) | page -> p a +type family Page p a :: (page :: Type) | page -> p a -type family PagingState p a = (ps :: *) +type family PagingState p a = (ps :: Type) -type family PagingBounds p a :: * +type family PagingBounds p a :: Type class Paging p where pageItems :: Page p a -> [a] diff --git a/libs/ropes/ropes.cabal b/libs/ropes/ropes.cabal index 644f2434e7..cf23d61256 100644 --- a/libs/ropes/ropes.cabal +++ b/libs/ropes/ropes.cabal @@ -60,6 +60,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: aeson >=2.0.1.0 diff --git a/libs/schema-profunctor/schema-profunctor.cabal b/libs/schema-profunctor/schema-profunctor.cabal index 2c73cb77f2..9ad3b7f616 100644 --- a/libs/schema-profunctor/schema-profunctor.cabal +++ b/libs/schema-profunctor/schema-profunctor.cabal @@ -57,6 +57,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: aeson >=2.0.1.0 && <2.2 @@ -125,6 +126,7 @@ test-suite schemas-tests ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: aeson diff --git a/libs/schema-profunctor/src/Data/Schema.hs b/libs/schema-profunctor/src/Data/Schema.hs index 103e81429e..af1b1a574f 100644 --- a/libs/schema-profunctor/src/Data/Schema.hs +++ b/libs/schema-profunctor/src/Data/Schema.hs @@ -102,6 +102,7 @@ import qualified Data.Swagger as S import qualified Data.Swagger.Declare as S import qualified Data.Swagger.Internal as S import qualified Data.Text as T +import qualified Data.Text.Lazy as TL import qualified Data.Vector as V import Imports hiding (Product) import Numeric.Natural @@ -307,7 +308,7 @@ optField = fieldF -- | A schema for a JSON object with a single optional field. fieldF :: - (HasOpt doc, HasField doc' doc, FieldFunctor doc f) => + (HasField doc' doc, FieldFunctor doc f) => Text -> SchemaP doc' A.Value A.Value a b -> SchemaP doc A.Object [A.Pair] a (f b) @@ -375,7 +376,7 @@ optFieldWithDocModifier name modify sch = optField name (over doc modify sch) -- | Like 'fieldF', but apply an arbitrary function to the -- documentation of the field. fieldWithDocModifierF :: - (HasOpt doc, HasField doc' doc, FieldFunctor doc f) => + (HasField doc' doc, FieldFunctor doc f) => Text -> (doc' -> doc') -> SchemaP doc' A.Value A.Value a b -> @@ -544,11 +545,11 @@ enum name sch = SchemaP (SchemaDoc d) (SchemaIn i) (SchemaOut o) -- -- This is most commonly used for optional fields, and it will cause the field -- to be omitted from the output of the serialiser. -maybe_ :: HasOpt d => Monoid w => SchemaP d v w a b -> SchemaP d v w (Maybe a) b +maybe_ :: Monoid w => SchemaP d v w a b -> SchemaP d v w (Maybe a) b maybe_ = maybeWithDefault mempty -- | A schema for 'Maybe', producing the given default value on serialisation. -maybeWithDefault :: HasOpt d => w -> SchemaP d v w a b -> SchemaP d v w (Maybe a) b +maybeWithDefault :: w -> SchemaP d v w a b -> SchemaP d v w (Maybe a) b maybeWithDefault w0 (SchemaP (SchemaDoc d) (SchemaIn i) (SchemaOut o)) = SchemaP (SchemaDoc d) (SchemaIn i) (SchemaOut (maybe (pure w0) o)) @@ -632,7 +633,7 @@ null_ = mkSchema mempty i o -- -- The serialiser behaves similarly, but in the other direction. nullable :: - (Monoid d, HasOpt d) => + Monoid d => ValueSchema d a -> ValueSchema d (Maybe a) nullable s = @@ -837,6 +838,8 @@ instance ToSchema a => A.FromJSON (Schema a) where instance ToSchema Text where schema = genericToSchema +instance ToSchema TL.Text where schema = genericToSchema + instance ToSchema Int where schema = genericToSchema instance ToSchema Int32 where schema = genericToSchema diff --git a/libs/sodium-crypto-sign/sodium-crypto-sign.cabal b/libs/sodium-crypto-sign/sodium-crypto-sign.cabal index d4b44e350c..22b69932b5 100644 --- a/libs/sodium-crypto-sign/sodium-crypto-sign.cabal +++ b/libs/sodium-crypto-sign/sodium-crypto-sign.cabal @@ -60,6 +60,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints pkgconfig-depends: libsodium >=0.4.5 build-depends: diff --git a/libs/ssl-util/ssl-util.cabal b/libs/ssl-util/ssl-util.cabal index 3e14908b5f..518d7031b3 100644 --- a/libs/ssl-util/ssl-util.cabal +++ b/libs/ssl-util/ssl-util.cabal @@ -58,6 +58,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: base >=4.7 && <5 diff --git a/libs/tasty-cannon/src/Test/Tasty/Cannon.hs b/libs/tasty-cannon/src/Test/Tasty/Cannon.hs index 8b0b437277..dd1e81c244 100644 --- a/libs/tasty-cannon/src/Test/Tasty/Cannon.hs +++ b/libs/tasty-cannon/src/Test/Tasty/Cannon.hs @@ -2,6 +2,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ViewPatterns #-} +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- @@ -369,7 +370,7 @@ assertMatchN_ t wss f = void $ assertMatchN t wss f assertSuccess :: (HasCallStack, MonadIO m, MonadThrow m) => Either MatchTimeout a -> m a assertSuccess = either throwM pure -assertNoEvent :: (HasCallStack, MonadIO m, MonadCatch m) => Timeout -> [WebSocket] -> m () +assertNoEvent :: (HasCallStack, MonadIO m) => Timeout -> [WebSocket] -> m () assertNoEvent t ww = do results <- awaitMatchN' t (zip [(0 :: Int) ..] ww) pure for_ results $ \(ix, result) -> diff --git a/libs/tasty-cannon/tasty-cannon.cabal b/libs/tasty-cannon/tasty-cannon.cabal index 3f4cde5b6b..d17891ee05 100644 --- a/libs/tasty-cannon/tasty-cannon.cabal +++ b/libs/tasty-cannon/tasty-cannon.cabal @@ -57,6 +57,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: aeson diff --git a/libs/types-common-aws/default.nix b/libs/types-common-aws/default.nix index a296d9c9d8..0eb974f80e 100644 --- a/libs/types-common-aws/default.nix +++ b/libs/types-common-aws/default.nix @@ -21,6 +21,7 @@ , tasty-hunit , text , time +, unliftio }: mkDerivation { pname = "types-common-aws"; @@ -43,6 +44,7 @@ mkDerivation { tasty-hunit text time + unliftio ]; description = "Shared AWS type definitions"; license = lib.licenses.agpl3Only; diff --git a/libs/types-common-aws/src/Util/Test/SQS.hs b/libs/types-common-aws/src/Util/Test/SQS.hs index e68e684484..9b93f9c759 100644 --- a/libs/types-common-aws/src/Util/Test/SQS.hs +++ b/libs/types-common-aws/src/Util/Test/SQS.hs @@ -1,9 +1,12 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} +-- Disabling for HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- @@ -27,52 +30,73 @@ module Util.Test.SQS where import qualified Amazonka as AWS import qualified Amazonka.SQS as SQS import qualified Amazonka.SQS.Lens as SQS -import Control.Exception (asyncExceptionFromException) import Control.Lens hiding ((.=)) -import Control.Monad.Catch hiding (bracket) import qualified Data.ByteString.Base64 as B64 +import Data.List (delete) import Data.ProtoLens import qualified Data.Text.Encoding as Text import Imports import Safe (headDef) -import Test.Tasty.HUnit +import UnliftIO (Async, async) import UnliftIO.Resource (MonadResource, ResourceT) +import UnliftIO.Timeout (timeout) ------------------------------------------------------------------------------ --- Assertions -assertQueue :: Message a => Text -> String -> AWS.Env -> (String -> Maybe a -> IO ()) -> IO () -assertQueue url label env check = execute env $ fetchMessage url label check - -assertNoMessages :: Text -> AWS.Env -> IO () -assertNoMessages url env = do - msgs <- execute env $ readAndDeleteAllUntilEmpty url - assertEqual "ensureNoMessages: length" 0 (length msgs) - ------------------------------------------------------------------------------ --- Queue operations -purgeQueue :: (Monad m, MonadReader AWS.Env m, MonadResource m) => Text -> m () -purgeQueue = void . readAndDeleteAllUntilEmpty +data SQSWatcher a = SQSWatcher + { watcherProcess :: Async (), + events :: IORef [a] + } --- Note that Amazon's purge queue is a bit incovenient for testing purposes because --- it may be delayed in ~60 seconds which causes messages that are published later --- to be (unintentionally) deleted which is why we have our own for testing purposes -readAndDeleteAllUntilEmpty :: (Monad m, MonadReader AWS.Env m, MonadResource m) => Text -> m [SQS.Message] -readAndDeleteAllUntilEmpty url = do - firstBatch <- fromMaybe [] . view SQS.receiveMessageResponse_messages <$> sendEnv (receive 1 url) - readUntilEmpty firstBatch firstBatch +-- | Starts an async loop which continuously retrieves messages from a given SQS +-- queue, parses them and stores them in an `IORef [a]`. +-- +-- This function drops everything in the queue before starting the async loop. +-- This helps test run faster and makes sure that initial tests don't timeout if +-- the queue has too many things in it before the tests start. +-- Note that the purgeQueue command is not guaranteed to be instant (can take up to 60 seconds) +-- Hopefully, the fake-aws implementation used during tests is fast enough. +watchSQSQueue :: Message a => AWS.Env -> Text -> IO (SQSWatcher a) +watchSQSQueue env queueUrl = do + eventsRef <- newIORef [] + ensureEmpty + process <- async $ recieveLoop eventsRef + pure $ SQSWatcher process eventsRef where - readUntilEmpty acc [] = pure acc - readUntilEmpty acc msgs = do - forM_ msgs $ deleteMessage url - newMsgs <- fromMaybe [] . view SQS.receiveMessageResponse_messages <$> sendEnv (receive 1 url) - forM_ newMsgs $ deleteMessage url - readUntilEmpty (acc ++ newMsgs) newMsgs - -deleteMessage :: (Monad m, MonadReader AWS.Env m, MonadResource m) => Text -> SQS.Message -> m () -deleteMessage url m = do - for_ - (m ^. SQS.message_receiptHandle) - (void . sendEnv . SQS.newDeleteMessage url) + recieveLoop ref = do + let rcvReq = + SQS.newReceiveMessage queueUrl + & set SQS.receiveMessage_waitTimeSeconds (Just 100) + . set SQS.receiveMessage_maxNumberOfMessages (Just 1) + . set SQS.receiveMessage_visibilityTimeout (Just 1) + rcvRes <- execute env $ sendEnv rcvReq + let msgs = fromMaybe [] $ view SQS.receiveMessageResponse_messages rcvRes + parsedMsgs <- fmap catMaybes . execute env $ mapM (parseDeleteMessage queueUrl) msgs + case parsedMsgs of + [] -> pure () + _ -> atomicModifyIORef ref $ \xs -> + (parsedMsgs <> xs, ()) + recieveLoop ref + + ensureEmpty :: IO () + ensureEmpty = void $ execute env $ sendEnv (SQS.newPurgeQueue queueUrl) + +-- | Waits for a message matching a predicate for a given number of seconds. +waitForMessage :: (MonadUnliftIO m, Eq a) => SQSWatcher a -> Int -> (a -> Bool) -> m (Maybe a) +waitForMessage watcher seconds predicate = timeout (seconds * 1_000_000) poll + where + poll = do + matched <- atomicModifyIORef (events watcher) $ \events -> + case filter predicate events of + [] -> (events, Nothing) + (x : _) -> (delete x events, Just x) + maybe (threadDelay 10000 >> poll) pure matched + +-- | First waits for a message matching a given predicate for 3 seconds (this +-- number could be chosen more scientifically) and then uses a callback to make +-- an assertion on such a message. +assertMessage :: (MonadUnliftIO m, Eq a, HasCallStack) => SQSWatcher a -> String -> (a -> Bool) -> (String -> Maybe a -> m ()) -> m () +assertMessage watcher label predicate callback = do + matched <- waitForMessage watcher 3 predicate + callback label matched ----------------------------------------------------------------------------- -- Generic AWS execution helpers @@ -92,13 +116,19 @@ receive n url = . set SQS.receiveMessage_maxNumberOfMessages (Just n) . set SQS.receiveMessage_visibilityTimeout (Just 1) -fetchMessage :: (MonadIO m, Message a, MonadReader AWS.Env m, MonadResource m) => Text -> String -> (String -> Maybe a -> IO ()) -> m () +fetchMessage :: (Message a, MonadReader AWS.Env m, MonadResource m) => Text -> String -> (String -> Maybe a -> IO ()) -> m () fetchMessage url label callback = do msgs <- fromMaybe [] . view SQS.receiveMessageResponse_messages <$> sendEnv (receive 1 url) events <- mapM (parseDeleteMessage url) msgs liftIO $ callback label (headDef Nothing events) -parseDeleteMessage :: (Monad m, Message a, MonadIO m, MonadReader AWS.Env m, MonadResource m) => Text -> SQS.Message -> m (Maybe a) +deleteMessage :: (MonadReader AWS.Env m, MonadResource m) => Text -> SQS.Message -> m () +deleteMessage url m = do + for_ + (m ^. SQS.message_receiptHandle) + (void . sendEnv . SQS.newDeleteMessage url) + +parseDeleteMessage :: (Message a, MonadReader AWS.Env m, MonadResource m) => Text -> SQS.Message -> m (Maybe a) parseDeleteMessage url m = do let decodedMessage = decodeMessage <=< (B64.decode . Text.encodeUtf8) evt <- case decodedMessage <$> (m ^. SQS.message_body) of @@ -109,44 +139,5 @@ parseDeleteMessage url m = do deleteMessage url m pure evt -queueMessage :: (MonadReader AWS.Env m, Message a, MonadResource m) => Text -> a -> m () -queueMessage url e = do - void $ sendEnv req - where - event = Text.decodeLatin1 $ B64.encode $ encodeMessage e - req = SQS.newSendMessage url event - -newtype MatchFailure a = MatchFailure {mFailure :: (a, SomeException)} - --- Try to match some assertions (callback) during the given timeout; if there's no --- match during the timeout, it asserts with the given label --- Matched matches are consumed while unmatched ones are republished to the queue -tryMatch :: - (Show a, Message a, MonadReader AWS.Env m, MonadResource m, MonadThrow m, MonadCatch m) => - String -> - Int -> - Text -> - (String -> Maybe a -> IO ()) -> - m () -tryMatch label tries url callback = go tries - where - go 0 = liftIO (assertFailure $ label <> ": No matching event found") - go n = do - msgs <- readAndDeleteAllUntilEmpty url - (bad, ok) <- partitionEithers <$> mapM (check <=< parseDeleteMessage url) msgs - -- Requeue all failed checks - forM_ bad $ \x -> for_ (fst . mFailure $ x) (queueMessage url) - -- If no success, continue! - when (null ok) $ do - liftIO $ threadDelay (10 ^ (6 :: Int)) - go (n - 1) - check e = - do - liftIO $ callback label e - pure (Right $ show e) - `catchAll` \ex -> case asyncExceptionFromException ex of - Just x -> throwM (x :: SomeAsyncException) - Nothing -> pure . Left $ MatchFailure (e, ex) - sendEnv :: (MonadReader AWS.Env m, MonadResource m, AWS.AWSRequest a) => a -> m (AWS.AWSResponse a) sendEnv x = flip AWS.send x =<< ask diff --git a/libs/types-common-aws/types-common-aws.cabal b/libs/types-common-aws/types-common-aws.cabal index 120d78603f..2926bd1da7 100644 --- a/libs/types-common-aws/types-common-aws.cabal +++ b/libs/types-common-aws/types-common-aws.cabal @@ -71,6 +71,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints ghc-prof-options: -fprof-auto-exported build-depends: @@ -90,9 +91,7 @@ library , tasty-hunit , text >=0.11 , time - - if impl(ghc >=8) - ghc-options: -fno-warn-redundant-constraints + , unliftio if flag(protobuf) cpp-options: -DWITH_PROTOBUF diff --git a/libs/types-common-journal/types-common-journal.cabal b/libs/types-common-journal/types-common-journal.cabal index bda3351280..476676023b 100644 --- a/libs/types-common-journal/types-common-journal.cabal +++ b/libs/types-common-journal/types-common-journal.cabal @@ -75,7 +75,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -fno-warn-redundant-constraints + -Wredundant-constraints ghc-prof-options: -fprof-auto-exported build-tool-depends: proto-lens-protoc:proto-lens-protoc diff --git a/libs/types-common/default.nix b/libs/types-common/default.nix index b1f22221ba..6d974ab32b 100644 --- a/libs/types-common/default.nix +++ b/libs/types-common/default.nix @@ -41,9 +41,7 @@ , schema-profunctor , scientific , servant-server -, singletons , string-conversions -, swagger , swagger2 , tagged , tasty @@ -100,9 +98,7 @@ mkDerivation { schema-profunctor scientific servant-server - singletons string-conversions - swagger swagger2 tagged tasty diff --git a/libs/types-common/src/Data/Id.hs b/libs/types-common/src/Data/Id.hs index 5663626362..526d9c850a 100644 --- a/libs/types-common/src/Data/Id.hs +++ b/libs/types-common/src/Data/Id.hs @@ -70,6 +70,7 @@ import qualified Data.Char as Char import Data.Default (Default (..)) import Data.Hashable (Hashable) import Data.ProtocolBuffers.Internal +import Data.Proxy import Data.Schema import Data.String.Conversions (cs) import qualified Data.Swagger as S @@ -218,7 +219,7 @@ instance A.ToJSONKey (Id a) where instance A.FromJSONKey (Id a) where fromJSONKey = A.FromJSONKeyTextParser idFromText -randomId :: (Functor m, MonadIO m) => m (Id a) +randomId :: MonadIO m => m (Id a) randomId = Id <$> liftIO nextRandom idFromText :: Text -> A.Parser (Id a) @@ -273,12 +274,13 @@ newtype ConnId = ConnId NFData, Generic ) + deriving (FromJSON, ToJSON, S.ToSchema) via Schema ConnId -instance ToJSON ConnId where - toJSON (ConnId c) = A.String (decodeUtf8 c) +instance ToSchema ConnId where + schema = (decodeUtf8 . fromConnId) .= fmap (ConnId . encodeUtf8) (text "ConnId") -instance FromJSON ConnId where - parseJSON x = ConnId . encodeUtf8 <$> A.withText "ConnId" pure x +instance S.ToParamSchema ConnId where + toParamSchema _ = S.toParamSchema (Proxy @Text) instance FromHttpApiData ConnId where parseUrlPiece = Right . ConnId . encodeUtf8 diff --git a/libs/types-common/src/Data/Json/Util.hs b/libs/types-common/src/Data/Json/Util.hs index daeed51852..92ba6bb8e4 100644 --- a/libs/types-common/src/Data/Json/Util.hs +++ b/libs/types-common/src/Data/Json/Util.hs @@ -3,6 +3,7 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE NumDecimals #-} {-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} -- This file is part of the Wire Server implementation. -- @@ -74,7 +75,6 @@ import Data.Time.Locale.Compat (defaultTimeLocale) import Imports import Servant import Test.QuickCheck (Arbitrary (arbitrary)) --- for UTCTime import Test.QuickCheck.Instances () append :: A.Pair -> [A.Pair] -> [A.Pair] @@ -115,7 +115,7 @@ utcTimeSchema :: ValueSchema NamedSwaggerDoc UTCTime utcTimeSchema = showUTCTime .= utcTimeTextSchema {-# INLINE toUTCTimeMillis #-} -toUTCTimeMillis :: HasCallStack => UTCTime -> UTCTimeMillis +toUTCTimeMillis :: UTCTime -> UTCTimeMillis toUTCTimeMillis = UTCTimeMillis . (TL.seconds . coerced @Pico @_ @Integer %~ (* 1e9) . (`div` 1e9)) {-# INLINE showUTCTimeMillis #-} @@ -157,6 +157,18 @@ class ToJSONObject a where instance ToJSONObject A.Object where toJSONObject = id +----------------------------------------------------------------------------- +-- Aeson Object + +instance S.ToParamSchema A.Object where + toParamSchema _ = + mempty & S.type_ ?~ S.SwaggerString + +instance ToSchema A.Object where + schema = + named "Object" $ + id .= jsonObject + ----------------------------------------------------------------------------- -- toJSONFieldName diff --git a/libs/types-common/src/Data/LegalHold.hs b/libs/types-common/src/Data/LegalHold.hs index dfc60764b5..5ec047730d 100644 --- a/libs/types-common/src/Data/LegalHold.hs +++ b/libs/types-common/src/Data/LegalHold.hs @@ -22,7 +22,6 @@ import Control.Lens ((?~)) import Data.Aeson hiding (constructorTagModifier) import Data.Schema import qualified Data.Swagger as S -import qualified Data.Swagger.Build.Api as Doc import Imports import Test.QuickCheck @@ -50,16 +49,6 @@ instance ToSchema UserLegalHoldStatus where defUserLegalHoldStatus :: UserLegalHoldStatus defUserLegalHoldStatus = UserLegalHoldNoConsent -typeUserLegalHoldStatus :: Doc.DataType -typeUserLegalHoldStatus = - Doc.string $ - Doc.enum - [ "enabled", - "pending", - "disabled", - "no_consent" - ] - instance Cql UserLegalHoldStatus where ctype = Tagged IntColumn diff --git a/libs/types-common/src/Data/List1.hs b/libs/types-common/src/Data/List1.hs index a729912395..ef0b0b208b 100644 --- a/libs/types-common/src/Data/List1.hs +++ b/libs/types-common/src/Data/List1.hs @@ -25,7 +25,8 @@ import Cassandra import Data.Aeson import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as N -import qualified Data.Vector as V +import Data.Schema as S +import qualified Data.Swagger as Swagger import Imports import Test.QuickCheck (Arbitrary) import Test.QuickCheck.Instances () @@ -35,6 +36,7 @@ newtype List1 a = List1 } deriving stock (Eq, Ord, Read, Show, Functor, Foldable, Traversable) deriving newtype (Applicative, Monad, Semigroup, Arbitrary) + deriving (FromJSON, ToJSON, Swagger.ToSchema) via S.Schema (List1 a) infixr 5 <| @@ -62,15 +64,17 @@ head :: List1 a -> a head = N.head . toNonEmpty {-# INLINE head #-} -instance ToJSON a => ToJSON (List1 a) where - toJSON = toJSON . toList - toEncoding = toEncoding . toList - -instance FromJSON a => FromJSON (List1 a) where - parseJSON a@(Array v) - | V.length v >= 1 = List1 . N.fromList <$> parseJSON a - | otherwise = fail "At least 1 element in list required." - parseJSON _ = mzero +instance ToSchema a => ToSchema (List1 a) where + schema = + named "List1" $ + toNonEmpty S..= fmap List1 (nonEmptyArray S.schema) + +instance Swagger.ToParamSchema (List1 a) where + toParamSchema _ = + mempty + { Swagger._paramSchemaType = Just Swagger.SwaggerArray, + Swagger._paramSchemaMinLength = Just 1 + } instance (Cql a) => Cql (List1 a) where ctype = Tagged (ListColumn (untag (ctype :: Tagged a ColumnType))) diff --git a/libs/types-common/src/Data/Misc.hs b/libs/types-common/src/Data/Misc.hs index 30d3196596..5488c1239e 100644 --- a/libs/types-common/src/Data/Misc.hs +++ b/libs/types-common/src/Data/Misc.hs @@ -52,9 +52,6 @@ module Data.Misc -- * PlainTextPassword PlainTextPassword (..), - -- * Swagger - modelLocation, - -- * Typesafe FUTUREWORKS FutureWork (..), ) @@ -76,7 +73,6 @@ 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 import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Imports @@ -184,14 +180,6 @@ location :: Latitude -> Longitude -> Location location (Latitude lat) (Longitude lon) = Location {_latitude = lat, _longitude = lon} -modelLocation :: Doc.Model -modelLocation = Doc.defineModel "Location" $ do - Doc.description "Geographical location" - Doc.property "lat" Doc.double' $ - Doc.description "Latitude" - Doc.property "lon" Doc.double' $ - Doc.description "Longitude" - instance Arbitrary Location where arbitrary = Location <$> arbitrary <*> arbitrary diff --git a/libs/types-common/src/Data/Nonce.hs b/libs/types-common/src/Data/Nonce.hs index c627000e6a..c68c26cf0d 100644 --- a/libs/types-common/src/Data/Nonce.hs +++ b/libs/types-common/src/Data/Nonce.hs @@ -74,7 +74,7 @@ instance ToHttpApiData Nonce where instance FromHttpApiData Nonce where parseQueryParam = maybe (Left "Invalid Nonce") Right . fromByteString' . cs -randomNonce :: (Functor m, MonadIO m) => m Nonce +randomNonce :: MonadIO m => m Nonce randomNonce = Nonce <$> liftIO nextRandom isValidBase64UrlEncodedUUID :: ByteString -> Bool diff --git a/libs/types-common/src/Data/Range.hs b/libs/types-common/src/Data/Range.hs index ffceb0fddf..7b61b9cf91 100644 --- a/libs/types-common/src/Data/Range.hs +++ b/libs/types-common/src/Data/Range.hs @@ -3,6 +3,8 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} +-- Disabling due to the use of LTE and other type level checks +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- @@ -24,7 +26,6 @@ module Data.Range ( Range, toRange, - LTE, Within, Bounds (..), checked, @@ -58,8 +59,6 @@ where import Cassandra (ColumnType, Cql (..), Tagged, retag) import Control.Lens ((%~), (?~)) import Data.Aeson (FromJSON (parseJSON), ToJSON (toJSON)) -import Data.Aeson.Types as Aeson (Parser) -import qualified Data.Attoparsec.ByteString as Atto import qualified Data.Bifunctor as Bifunctor import qualified Data.ByteString as B import Data.ByteString.Conversion @@ -75,22 +74,20 @@ import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as N import Data.List1 (List1, toNonEmpty) import qualified Data.Map as Map +import Data.Proxy import Data.Schema import Data.Sequence (Seq) import qualified Data.Sequence as Seq import qualified Data.Set as Set -import Data.Singletons -import Data.Singletons.Prelude.Num -import Data.Singletons.Prelude.Ord -import Data.Singletons.TypeLits import Data.Swagger (ParamSchema, ToParamSchema (..)) import qualified Data.Swagger as S import qualified Data.Text as T import Data.Text.Ascii (AsciiChar, AsciiChars, AsciiText, fromAsciiChars) import qualified Data.Text.Ascii as Ascii import qualified Data.Text.Lazy as TL +import Data.Type.Ord +import GHC.TypeNats import Imports -import Numeric.Natural (Natural) import Servant (FromHttpApiData (..)) import System.Random (Random) import Test.QuickCheck (Arbitrary (arbitrary, shrink), Gen) @@ -103,7 +100,7 @@ newtype Range (n :: Nat) (m :: Nat) a = Range } deriving (Eq, Ord, Show) -toRange :: (LTE n x, LTE x m, KnownNat x, Num a) => Proxy x -> Range n m a +toRange :: (n <= x, x <= m, KnownNat x, Num a) => Proxy x -> Range n m a toRange = Range . fromIntegral . natVal instance (Show a, Num a, Within a n m, KnownNat n, KnownNat m) => Bounded (Range n m a) where @@ -115,19 +112,18 @@ instance NFData (Range n m a) where rnf (Range a) = seq a () instance ToJSON a => ToJSON (Range n m a) where toJSON = toJSON . fromRange -instance (Within a n m, FromJSON a) => FromJSON (Range n m a) where - parseJSON v = parseJSON v >>= maybe (msg sing sing) pure . checked +instance forall a n m. (KnownNat n, KnownNat m, Within a n m, FromJSON a) => FromJSON (Range n m a) where + parseJSON v = parseJSON v >>= maybe msg pure . checked where - msg :: Bounds a => SNat n -> SNat m -> Aeson.Parser (Range n m a) - msg sn sm = fail (errorMsg (fromSing sn) (fromSing sm) "") + msg = fail (errorMsg (natVal (Proxy @n)) (natVal (Proxy @m)) "") rangedSchema :: forall n m d v w a b. - (Within a n m, HasRangedSchemaDocModifier d b) => + (KnownNat n, KnownNat m, Within a n m, HasRangedSchemaDocModifier d b) => SchemaP d v w a b -> SchemaP d v w a (Range n m b) rangedSchema sch = - Range <$> untypedRangedSchema (toInteger (demote @n)) (toInteger (demote @m)) sch + Range <$> untypedRangedSchema (toInteger (natVal (Proxy @n))) (toInteger (natVal (Proxy @m))) sch untypedRangedSchema :: forall d v w a b. @@ -178,16 +174,15 @@ instance S.HasSchema d S.Schema => HasRangedSchemaDocModifier d Word32 where ran instance S.HasSchema d S.Schema => HasRangedSchemaDocModifier d Word64 where rangedSchemaDocModifier _ = numRangedSchemaDocModifier -instance (Within a n m, ToSchema a, HasRangedSchemaDocModifier NamedSwaggerDoc a) => ToSchema (Range n m a) where +instance (KnownNat n, KnownNat m, Within a n m, ToSchema a, HasRangedSchemaDocModifier NamedSwaggerDoc a) => ToSchema (Range n m a) where schema = fromRange .= rangedSchema schema -instance (Within a n m, Cql a) => Cql (Range n m a) where +instance forall a n m. (KnownNat n, KnownNat m, Within a n m, Cql a) => Cql (Range n m a) where ctype = retag (ctype :: Tagged a ColumnType) toCql = toCql . fromRange - fromCql c = fromCql c >>= maybe (msg sing sing) pure . checked + fromCql c = fromCql c >>= maybe msg pure . checked where - msg :: Bounds a => SNat n -> SNat m -> Either String (Range n m a) - msg sn sm = Left (errorMsg (fromSing sn) (fromSing sm) "") + msg = Left (errorMsg (natVal (Proxy @n)) (natVal (Proxy @m)) "") instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m Integer) where toParamSchema = rangedNumToParamSchema @@ -241,25 +236,21 @@ instance S.ToSchema a => S.ToSchema (Range n m a) where declareNamedSchema _ = S.declareNamedSchema (Proxy @a) -instance (Within a n m, FromHttpApiData a) => FromHttpApiData (Range n m a) where +instance (KnownNat n, KnownNat m, Within a n m, FromHttpApiData a) => FromHttpApiData (Range n m a) where parseUrlPiece t = do unchecked <- parseUrlPiece t Bifunctor.first T.pack $ checkedEither @_ @n @m unchecked -type LTE (n :: Nat) (m :: Nat) = (SingI n, SingI m, (n <= m) ~ 'True) +type Within a (n :: Nat) (m :: Nat) = (Bounds a, n <= m) -type Within a (n :: Nat) (m :: Nat) = (Bounds a, LTE n m) +mk :: Bounds a => a -> Nat -> Nat -> Maybe (Range n m a) +mk a n m = + if within a (toInteger n) (toInteger m) + then Just (Range a) + else Nothing -mk :: Bounds a => a -> SNat n -> SNat m -> Maybe (Range n m a) -mk a sn sm = - let n = fromSing sn - m = fromSing sm - in if within a (toInteger n) (toInteger m) - then Just (Range a) - else Nothing - -checked :: Within a n m => a -> Maybe (Range n m a) -checked x = mk x sing sing +checked :: forall n m a. (KnownNat n, KnownNat m, Within a n m) => a -> Maybe (Range n m a) +checked x = mk x (natVal (Proxy @n)) (natVal (Proxy @m)) errorMsg :: (Show a, Show b) => a -> b -> ShowS errorMsg n m = @@ -269,20 +260,20 @@ errorMsg n m = . shows m . showString "]" -checkedEitherMsg :: forall a n m. Within a n m => String -> a -> Either String (Range n m a) +checkedEitherMsg :: forall a n m. (KnownNat n, KnownNat m) => Within a n m => String -> a -> Either String (Range n m a) checkedEitherMsg msg x = do - let sn = sing :: SNat n - sm = sing :: SNat m + let sn = natVal (Proxy @n) + sm = natVal (Proxy @m) case mk x sn sm of - Nothing -> Left $ showString msg . showString ": " . errorMsg (fromSing sn) (fromSing sm) $ "" + Nothing -> Left $ showString msg . showString ": " . errorMsg sn sm $ "" Just r -> Right r -checkedEither :: forall a n m. Within a n m => a -> Either String (Range n m a) +checkedEither :: forall a n m. (KnownNat n, KnownNat m) => Within a n m => a -> Either String (Range n m a) checkedEither x = do - let sn = sing :: SNat n - sm = sing :: SNat m + let sn = natVal (Proxy @n) + sm = natVal (Proxy @m) case mk x sn sm of - Nothing -> Left (errorMsg (fromSing sn) (fromSing sm) "") + Nothing -> Left (errorMsg sn sm "") Just r -> Right r rangedChunks :: forall a n. (Within [a] 1 n, KnownNat n) => [a] -> [Range 1 n [a]] @@ -293,34 +284,33 @@ rangedChunks xs = [] -> [] _ -> Range headPart : rangedChunks tailPart -unsafeRange :: (Show a, Within a n m) => a -> Range n m a -unsafeRange x = fromMaybe (msg sing sing) (checked x) +unsafeRange :: forall a n m. (Show a, KnownNat n, KnownNat m, Within a n m) => a -> Range n m a +unsafeRange x = fromMaybe msg (checked x) where - msg :: SNat n -> SNat m -> Range n m a - msg sn sm = + msg = error . shows x . showString " " - . errorMsg (fromSing sn) (fromSing sm) + . errorMsg (natVal (Proxy @n)) (natVal (Proxy @m)) $ "" -rcast :: (LTE n m, (m <= m') ~ 'True, (n >= n') ~ 'True) => Range n m a -> Range n' m' a +rcast :: (n <= m, m <= m', n >= n') => Range n m a -> Range n' m' a rcast (Range a) = Range a rnil :: Monoid a => Range 0 0 a rnil = Range mempty -rcons, (<|) :: LTE n m => a -> Range n m [a] -> Range n (m + 1) [a] +rcons, (<|) :: n <= m => a -> Range n m [a] -> Range n (m + 1) [a] rcons a (Range aa) = Range (a : aa) infixr 5 <| (<|) = rcons -rinc :: (Integral a, LTE n m) => Range n m a -> Range n (m + 1) a +rinc :: (Integral a, n <= m) => Range n m a -> Range n (m + 1) a rinc (Range a) = Range (a + 1) -rappend :: (LTE n m, LTE n' m', Monoid a) => Range n m a -> Range n' m' a -> Range n (m + m') a +rappend :: (n <= m, n' <= m', Monoid a) => Range n m a -> Range n' m' a -> Range n (m + m') a rappend (Range a) (Range b) = Range (a <> b) rsingleton :: a -> Range 1 1 [a] @@ -413,20 +403,19 @@ instance Bounds (AsciiText r) where ----------------------------------------------------------------------------- -instance (Within a n m, Read a) => Read (Range n m a) where +instance (KnownNat n, KnownNat m, Within a n m, Read a) => Read (Range n m a) where readsPrec p s = fromMaybe [] $ foldr f (Just []) (readsPrec p s) where - f :: (Within a n m, Read a) => (a, String) -> Maybe [(Range n m a, String)] -> Maybe [(Range n m a, String)] + f :: (a, String) -> Maybe [(Range n m a, String)] -> Maybe [(Range n m a, String)] f _ Nothing = Nothing f (a, t) (Just acc) = (\a' -> (a', t) : acc) <$> checked a ----------------------------------------------------------------------------- -instance (Within a n m, FromByteString a) => FromByteString (Range n m a) where - parser = parser >>= maybe (msg sing sing) pure . checked +instance (KnownNat n, KnownNat m, Within a n m, FromByteString a) => FromByteString (Range n m a) where + parser = parser >>= maybe msg pure . checked where - msg :: Bounds a => SNat n -> SNat m -> Atto.Parser (Range n m a) - msg sn sm = fail (errorMsg (fromSing sn) (fromSing sm) "") + msg = fail (errorMsg (natVal (Proxy @n)) (natVal (Proxy @m)) "") instance ToByteString a => ToByteString (Range n m a) where builder = builder . fromRange @@ -442,20 +431,20 @@ instance Arbitrary (Range m n a) => Arbitrary (Ranged m n a) where arbitrary = Ranged . fromRange <$> arbitrary @(Range m n a) instance - (KnownNat n, KnownNat m, LTE n m, Arbitrary a, Show a) => + (KnownNat n, KnownNat m, n <= m, Arbitrary a, Show a) => Arbitrary (Range n m [a]) where arbitrary = genRangeList @n @m @a arbitrary genRangeList :: - forall (n :: Nat) (m :: Nat) (a :: *). - (Show a, KnownNat n, KnownNat m, LTE n m) => + forall (n :: Nat) (m :: Nat) (a :: Type). + (Show a, KnownNat n, KnownNat m, n <= m) => Gen a -> Gen (Range n m [a]) genRangeList = genRange id instance - (KnownNat n, KnownNat m, LTE n m, Arbitrary a, Show a, Ord a) => + (KnownNat n, KnownNat m, n <= m, Arbitrary a, Show a, Ord a) => Arbitrary (Range n m (Set a)) where arbitrary = genRangeSet @n @m @a arbitrary @@ -465,14 +454,14 @@ instance -- However, it will only show up while running tests and might indicate deeper -- problems, so I'd say that's ok. genRangeSet :: - forall (n :: Nat) (m :: Nat) (a :: *). - (Show a, KnownNat n, KnownNat m, LTE n m, Ord a) => + forall (n :: Nat) (m :: Nat) (a :: Type). + (Show a, KnownNat n, KnownNat m, n <= m, Ord a) => Gen a -> Gen (Range n m (Set a)) genRangeSet gc = (Set.fromList . fromRange <$> genRangeList @n @m @a gc) `QC.suchThatMap` checked -instance (KnownNat n, KnownNat m, LTE n m) => Arbitrary (Range n m Text) where +instance (KnownNat n, KnownNat m, n <= m) => Arbitrary (Range n m Text) where arbitrary = genRangeText arbitrary -- FUTUREWORK: the shrinking could be more general (like genRange) and offer more options @@ -480,27 +469,27 @@ instance (KnownNat n, KnownNat m, LTE n m) => Arbitrary (Range n m Text) where genRangeText :: forall (n :: Nat) (m :: Nat). - (KnownNat n, KnownNat m, LTE n m) => + (KnownNat n, KnownNat m, n <= m) => Gen Char -> Gen (Range n m Text) genRangeText = genRange fromString instance - (AsciiChars c, KnownNat n, KnownNat m, LTE n m, Arbitrary (AsciiChar c)) => + (AsciiChars c, KnownNat n, KnownNat m, n <= m, Arbitrary (AsciiChar c)) => Arbitrary (Range n m (AsciiText c)) where arbitrary = genRangeAsciiText (arbitrary @(AsciiChar c)) genRangeAsciiText :: forall (n :: Nat) (m :: Nat) (c :: Type). - (HasCallStack, KnownNat n, KnownNat m, LTE n m, AsciiChars c) => + (HasCallStack, KnownNat n, KnownNat m, n <= m, AsciiChars c) => Gen (AsciiChar c) -> Gen (Range n m (AsciiText c)) genRangeAsciiText = genRange @n @m fromAsciiChars genRange :: - forall (n :: Nat) (m :: Nat) (a :: *) (b :: *). - (Show b, Bounds b, KnownNat n, KnownNat m, LTE n m) => + forall (n :: Nat) (m :: Nat) (a :: Type) (b :: Type). + (Show b, Bounds b, KnownNat n, KnownNat m, n <= m) => ([a] -> b) -> Gen a -> Gen (Range n m b) @@ -513,17 +502,17 @@ genRange pack_ gc = where grange mi ma gelem = (`replicateM` gelem) =<< QC.chooseInt (mi, ma) -instance (KnownNat n, KnownNat m, LTE n m) => Arbitrary (Range n m Integer) where +instance (KnownNat n, KnownNat m, n <= m) => Arbitrary (Range n m Integer) where arbitrary = genIntegral -instance (KnownNat n, KnownNat m, LTE n m) => Arbitrary (Range n m Word) where +instance (KnownNat n, KnownNat m, n <= m) => Arbitrary (Range n m Word) where arbitrary = genIntegral genIntegral :: forall n m i. - (KnownNat n, KnownNat m, LTE n m, Integral i, Show i, Bounds i, Random i) => + (KnownNat n, KnownNat m, n <= m, Integral i, Show i, Bounds i, Random i) => Gen (Range n m i) genIntegral = unsafeRange @i @n @m <$> QC.choose (fromKnownNat (Proxy @n), fromKnownNat (Proxy @m)) -fromKnownNat :: forall (k :: Nat) (i :: *). (Num i, KnownNat k) => Proxy k -> i +fromKnownNat :: forall (k :: Nat) (i :: Type). (Num i, KnownNat k) => Proxy k -> i fromKnownNat p = fromIntegral $ natVal p diff --git a/libs/types-common/src/Data/SizedHashMap.hs b/libs/types-common/src/Data/SizedHashMap.hs index d11e54efa2..d52dbec0b5 100644 --- a/libs/types-common/src/Data/SizedHashMap.hs +++ b/libs/types-common/src/Data/SizedHashMap.hs @@ -44,7 +44,7 @@ size (SizedHashMap s _) = s empty :: forall k v. SizedHashMap k v empty = SizedHashMap 0 M.empty -insert :: forall k v. (Eq k, Hashable k) => k -> v -> SizedHashMap k v -> SizedHashMap k v +insert :: forall k v. Hashable k => k -> v -> SizedHashMap k v -> SizedHashMap k v insert k v (SizedHashMap n hm) = SizedHashMap n' hm' where n' = if M.member k hm then n else n + 1 @@ -59,10 +59,10 @@ elems (SizedHashMap _ hm) = M.elems hm toList :: forall k v. SizedHashMap k v -> [(k, v)] toList (SizedHashMap _ hm) = M.toList hm -lookup :: forall k v. (Eq k, Hashable k) => k -> SizedHashMap k v -> Maybe v +lookup :: forall k v. Hashable k => k -> SizedHashMap k v -> Maybe v lookup k (SizedHashMap _ hm) = M.lookup k hm -delete :: forall k v. (Eq k, Hashable k) => k -> SizedHashMap k v -> SizedHashMap k v +delete :: forall k v. Hashable k => k -> SizedHashMap k v -> SizedHashMap k v delete k (SizedHashMap n hm) = SizedHashMap n' hm' where n' = if M.member k hm then n - 1 else n diff --git a/libs/types-common/src/Data/Text/Ascii.hs b/libs/types-common/src/Data/Text/Ascii.hs index bc669d5bd6..428d7dc12b 100644 --- a/libs/types-common/src/Data/Text/Ascii.hs +++ b/libs/types-common/src/Data/Text/Ascii.hs @@ -4,6 +4,8 @@ {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE TypeFamilies #-} +-- GHC was complaining about the `(Subset c c' ~ 'True)` and `AsciiChars c` constraints +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- diff --git a/libs/types-common/src/Util/Options.hs b/libs/types-common/src/Util/Options.hs index 06d65cfb1f..fa79df845d 100644 --- a/libs/types-common/src/Util/Options.hs +++ b/libs/types-common/src/Util/Options.hs @@ -32,7 +32,6 @@ import Imports import Options.Applicative import Options.Applicative.Types import System.Exit (die) -import System.IO (hPutStrLn) import URI.ByteString import Util.Options.Common @@ -137,10 +136,6 @@ getOptions desc pars defaultPath = do Right o -> pure o -- Config doesn't exist but at least we have a CLI options parser (False, Just p) -> do - hPutStrLn stderr $ - "Config file at " - ++ path - ++ " does not exist, falling back to command-line arguments. \n" execParser (info (helper <*> p) mkDesc) -- No config, no parser :( (False, Nothing) -> do diff --git a/libs/types-common/src/Wire/Swagger.hs b/libs/types-common/src/Wire/Swagger.hs deleted file mode 100644 index 1701feb9d1..0000000000 --- a/libs/types-common/src/Wire/Swagger.hs +++ /dev/null @@ -1,49 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Wire.Swagger where - -import Data.Swagger.Build.Api -import Imports hiding (max, min) - -errorModel :: Model -errorModel = defineModel "Error" $ do - description "Basic error information" - errorProperties - -errorProperties :: ModelBuilder -errorProperties = do - property "code" int32' $ - description "HTTP status code" - property "label" string' $ - description "Textual classifier for programmatic consumption." - property "message" string' $ - description "More detailed error description." - --- | This model is used for the @/login/send@ endpoint to describe a --- 'Brig.API.Error.loginCodePending' error, but I can't find where it actually gets thrown. -pendingLoginError :: Model -pendingLoginError = defineModel "PendingLoginError" $ do - description "A login code is still pending." - errorProperties - property "expires_in" int32' $ - description "Number of seconds before the pending login code expires." - -int32Between :: Int32 -> Int32 -> DataType -int32Between m n = int32 (min m . max n) diff --git a/libs/types-common/test/Test/Properties.hs b/libs/types-common/test/Test/Properties.hs index 1340a367c1..e3863ce1f4 100644 --- a/libs/types-common/test/Test/Properties.hs +++ b/libs/types-common/test/Test/Properties.hs @@ -1,6 +1,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ViewPatterns #-} +{-# OPTIONS_GHC -Wno-redundant-constraints #-} {-# OPTIONS_GHC -fno-warn-orphans #-} -- This file is part of the Wire Server implementation. @@ -236,7 +237,7 @@ jsonRoundtrip = testProperty msg trip counterexample (show $ toJSON v) $ Right v === (Aeson.parseEither parseJSON . toJSON) v -jsonKeyRoundtrip :: forall a. (Arbitrary a, Typeable a, ToJSONKey a, FromJSONKey a, Eq a, Show a, Ord a) => TestTree +jsonKeyRoundtrip :: forall a. (Arbitrary a, Typeable a, ToJSONKey a, FromJSONKey a, Show a, Ord a) => TestTree jsonKeyRoundtrip = testProperty msg trip where msg = "json key round trip: " <> show (typeRep @a) diff --git a/libs/types-common/types-common.cabal b/libs/types-common/types-common.cabal index 96fb5b5526..0a414d5cea 100644 --- a/libs/types-common/types-common.cabal +++ b/libs/types-common/types-common.cabal @@ -37,7 +37,6 @@ library Util.Options.Common Util.Test Wire.Arbitrary - Wire.Swagger other-modules: Paths_types_common hs-source-dirs: src @@ -84,6 +83,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints ghc-prof-options: -fprof-auto-exported build-depends: @@ -122,9 +122,7 @@ library , schema-profunctor , scientific >=0.3.4 , servant-server - , singletons >=2.0 , string-conversions - , swagger >=0.3 , swagger2 , tagged >=0.8 , tasty >=0.11 @@ -139,9 +137,6 @@ library , vector >=0.11 , yaml >=0.8.22 - if impl(ghc >=8) - ghc-options: -fno-warn-redundant-constraints - default-language: Haskell2010 test-suite tests @@ -200,7 +195,7 @@ test-suite tests ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded + -threaded -Wredundant-constraints build-depends: aeson diff --git a/libs/wai-utilities/default.nix b/libs/wai-utilities/default.nix index 93a249b3c7..a51e40a0ce 100644 --- a/libs/wai-utilities/default.nix +++ b/libs/wai-utilities/default.nix @@ -22,7 +22,6 @@ , servant-server , streaming-commons , string-conversions -, swagger , swagger2 , text , tinylog @@ -56,7 +55,6 @@ mkDerivation { servant-server streaming-commons string-conversions - swagger swagger2 text tinylog diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs index 83aeac19a6..1c25a069c7 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs @@ -47,9 +47,8 @@ module Network.Wai.Utilities.Server ) where -import Control.Concurrent.Async import Control.Error.Util ((?:)) -import Control.Exception (throw, throwIO) +import Control.Exception (throw) import Control.Monad.Catch hiding (onError, onException) import Data.Aeson (decode, encode) import qualified Data.ByteString as BS @@ -124,30 +123,23 @@ newSettings (Server h p l m t) = do -- connections up to the given number of seconds. -- -- See also: https://gitlab.haskell.org/ghc/ghc/-/merge_requests/7681 -runSettingsWithShutdown :: Settings -> Application -> Maybe Word16 -> IO () +runSettingsWithShutdown :: Settings -> Application -> Maybe Int -> IO () runSettingsWithShutdown s app (fromMaybe defaultShutdownTime -> secs) = do initialization - latch <- newEmptyMVar - let s' = setInstallShutdownHandler (catchSignals latch) s - srv <- async $ runSettings s' app `finally` void (tryPutMVar latch ()) - takeMVar latch - await srv secs + let s' = + setInstallShutdownHandler catchSignals + . setGracefulShutdownTimeout (Just secs) + $ s + runSettings s' app where initialization :: IO () initialization = do spawnGCMetricsCollector - catchSignals latch closeSocket = do - let shutdown = closeSocket >> putMVar latch () - void $ installHandler sigINT (Sig.CatchOnce shutdown) Nothing - void $ installHandler sigTERM (Sig.CatchOnce shutdown) Nothing - await srv t = do - status <- poll srv - case status of - Nothing | t > 0 -> threadDelay 1000000 >> await srv (t - 1) - Just (Left ex) -> throwIO ex - _ -> cancel srv - -defaultShutdownTime :: Word16 + catchSignals closeSocket = do + void $ installHandler sigINT (Sig.CatchOnce closeSocket) Nothing + void $ installHandler sigTERM (Sig.CatchOnce closeSocket) Nothing + +defaultShutdownTime :: Int defaultShutdownTime = 30 compile :: Monad m => Routes a m b -> Tree (App m) diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Swagger.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Swagger.hs deleted file mode 100644 index a421584539..0000000000 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Swagger.hs +++ /dev/null @@ -1,64 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Network.Wai.Utilities.Swagger where - -import Data.Swagger.Build.Api -import qualified Data.Text as Text -import Data.Text.Encoding (decodeUtf8) -import Data.Text.Lazy (toStrict) -import Data.Text.Lazy.Builder -import Imports -import Network.HTTP.Types.Status (statusCode) -import Network.Wai.Routing (Meta (..), Routes, attach, examine) -import Network.Wai.Utilities.Error -import Wire.Swagger - -mkSwaggerApi :: Text -> [Model] -> Routes ApiBuilder m a -> ApiDecl -mkSwaggerApi base models sitemap = - let routes = groupBy ((==) `on` routePath) (examine sitemap) - in declare base "1.2" $ do - resourcePath "/" - produces "application/json" - authorisation ApiKey - mapM_ model models - forM_ routes $ \r -> - api (fixVars . decodeUtf8 $ routePath (head r)) $ - mapM_ routeMeta r - where - fixVars = Text.intercalate "/" . map var . Text.splitOn "/" - var t - | Text.null t = t - | Text.head t == ':' = "{" <> Text.tail t <> "}" - | otherwise = t - -document :: Text -> Text -> OperationBuilder -> Routes ApiBuilder m () -document x y = attach . operation x y - -errorResponse :: Error -> OperationBuilder -errorResponse e = errorResponse' e errorModel - -errorResponse' :: Error -> Model -> OperationBuilder -errorResponse' e md = response (statusCode (code e)) (renderError e) (model md) - -renderError :: Error -> Text -renderError e = toStrict . toLazyText $ "[label=" <> lbl <> "] " <> msg - where - lbl = fromLazyText (label e) - msg = fromLazyText (message e) diff --git a/libs/wai-utilities/wai-utilities.cabal b/libs/wai-utilities/wai-utilities.cabal index af8f7d5645..75824a03df 100644 --- a/libs/wai-utilities/wai-utilities.cabal +++ b/libs/wai-utilities/wai-utilities.cabal @@ -20,7 +20,6 @@ library Network.Wai.Utilities.Request Network.Wai.Utilities.Response Network.Wai.Utilities.Server - Network.Wai.Utilities.Swagger Network.Wai.Utilities.ZAuth other-modules: Paths_wai_utilities @@ -68,6 +67,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: aeson >=2.0.1.0 @@ -87,7 +87,6 @@ library , servant-server , streaming-commons >=0.1 , string-conversions - , swagger , swagger2 , text >=0.11 , tinylog >=0.8 diff --git a/libs/wire-api-federation/default.nix b/libs/wire-api-federation/default.nix index 3c7d80254e..9c2429d49f 100644 --- a/libs/wire-api-federation/default.nix +++ b/libs/wire-api-federation/default.nix @@ -15,6 +15,7 @@ , errors , exceptions , gitignoreSource +, HsOpenSSL , hspec , hspec-discover , http-media @@ -37,6 +38,7 @@ , servant-client-core , servant-server , singletons +, singletons-th , sop-core , streaming-commons , swagger2 @@ -66,6 +68,7 @@ mkDerivation { either errors exceptions + HsOpenSSL http-media http-types http2 @@ -83,6 +86,7 @@ mkDerivation { servant-client-core servant-server singletons + singletons-th sop-core streaming-commons swagger2 @@ -128,6 +132,7 @@ mkDerivation { servant-client-core servant-server singletons + singletons-th sop-core streaming-commons swagger2 diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API.hs b/libs/wire-api-federation/src/Wire/API/Federation/API.hs index 7fc6e981b0..d94f1c6096 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API.hs @@ -1,3 +1,6 @@ +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -29,6 +32,7 @@ module Wire.API.Federation.API ) where +import Data.Kind import Data.Proxy import GHC.TypeLits import Imports @@ -43,7 +47,7 @@ import Wire.API.Routes.Named -- Note: this type family being injective means that in most cases there is no need -- to add component annotations when invoking the federator client -type family FedApi (comp :: Component) = (api :: *) | api -> comp +type family FedApi (comp :: Component) = (api :: Type) | api -> comp type instance FedApi 'Galley = GalleyApi @@ -51,7 +55,7 @@ type instance FedApi 'Brig = BrigApi type instance FedApi 'Cargohold = CargoholdApi -type HasFedEndpoint comp api name = (HasUnsafeFedEndpoint comp api name, CallsFed comp name) +type HasFedEndpoint comp api name = (HasUnsafeFedEndpoint comp api name) -- | Like 'HasFedEndpoint', but doesn't propagate a 'CallsFed' constraint. -- Useful for tests, but unsafe in the sense that incorrect usage will allow @@ -59,9 +63,15 @@ type HasFedEndpoint comp api name = (HasUnsafeFedEndpoint comp api name, CallsFe type HasUnsafeFedEndpoint comp api name = 'Just api ~ LookupEndpoint (FedApi comp) name -- | Return a client for a named endpoint. +-- +-- This function introduces an 'AddAnnotation' constraint, which is +-- automatically solved by the @transitive-anns@ plugin, and pushes the +-- resulting information around in a side-channel. See the documentation at +-- 'Wire.API.MakesFederatedCall.exposeAnnotations' for a better understanding +-- of the information flow here. fedClient :: - forall (comp :: Component) (name :: Symbol) m api. - (CallsFed comp name, HasFedEndpoint comp api name, HasClient m api, m ~ FederatorClient comp) => + forall (comp :: Component) (name :: Symbol) m (showcomp :: Symbol) api x. + (AddAnnotation 'Remote showcomp name x, showcomp ~ ShowComponent comp, HasFedEndpoint comp api name, HasClient m api, m ~ FederatorClient comp) => Client m api fedClient = clientIn (Proxy @api) (Proxy @m) @@ -71,7 +81,7 @@ fedClientIn :: Client m api fedClientIn = clientIn (Proxy @api) (Proxy @m) --- | Like 'fedClientIn', but doesn't propagate a 'CallsFed' constraint. Inteded +-- | Like 'fedClientIn', but doesn't propagate a 'CallsFed' constraint. Intended -- to be used in test situations only. unsafeFedClientIn :: forall (comp :: Component) (name :: Symbol) m api. 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 fb32aa2451..158aedfa53 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 @@ -126,7 +126,13 @@ type GalleyApi = "on-client-removed" ClientRemovedRequest EmptyResponse - :<|> FedEndpoint "on-typing-indicator-updated" TypingDataUpdateRequest EmptyResponse + :<|> FedEndpointWithMods + '[ MakesFederatedCall 'Galley "on-typing-indicator-updated" + ] + "update-typing-indicator" + TypingDataUpdateRequest + TypingDataUpdateResponse + :<|> FedEndpoint "on-typing-indicator-updated" TypingDataUpdated EmptyResponse data TypingDataUpdateRequest = TypingDataUpdateRequest { tdurTypingStatus :: TypingStatus, @@ -136,6 +142,24 @@ data TypingDataUpdateRequest = TypingDataUpdateRequest deriving stock (Eq, Show, Generic) deriving (FromJSON, ToJSON) via (CustomEncoded TypingDataUpdateRequest) +data TypingDataUpdateResponse + = TypingDataUpdateSuccess TypingDataUpdated + | TypingDataUpdateError GalleyError + deriving stock (Eq, Show, Generic) + deriving (FromJSON, ToJSON) via (CustomEncoded TypingDataUpdateResponse) + +data TypingDataUpdated = TypingDataUpdated + { tudTime :: UTCTime, + tudOrigUserId :: Qualified UserId, + -- | Implicitely qualified by sender's domain + tudConvId :: ConvId, + -- | Implicitely qualified by receiver's domain + tudUsersInConv :: [UserId], + tudTypingStatus :: TypingStatus + } + deriving stock (Eq, Show, Generic) + deriving (FromJSON, ToJSON) via (CustomEncoded TypingDataUpdated) + data ClientRemovedRequest = ClientRemovedRequest { crrUser :: UserId, crrClient :: ClientId, diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Client.hs b/libs/wire-api-federation/src/Wire/API/Federation/Client.hs index 906017a6fe..0d2b32f987 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Client.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Client.hs @@ -47,6 +47,7 @@ import Data.Domain import qualified Data.Sequence as Seq import qualified Data.Set as Set import Data.Streaming.Network +import qualified Data.Text as Text import qualified Data.Text.Encoding as Text import qualified Data.Text.Encoding.Error as Text import qualified Data.Text.Lazy.Encoding as LText @@ -58,8 +59,9 @@ import qualified Network.HTTP.Media as HTTP import qualified Network.HTTP.Types as HTTP import qualified Network.HTTP2.Client as HTTP2 import qualified Network.Socket as NS -import qualified Network.TLS as TLS import qualified Network.Wai.Utilities.Error as Wai +import OpenSSL.Session (SSL, SSLContext) +import qualified OpenSSL.Session as SSL import Servant.Client import Servant.Client.Core import Servant.Types.SourceT @@ -122,7 +124,7 @@ connectSocket hostname port = $ getSocketFamilyTCP hostname port NS.AF_UNSPEC performHTTP2Request :: - Maybe TLS.ClientParams -> + Maybe SSLContext -> HTTP2.Request -> ByteString -> Int -> @@ -138,34 +140,42 @@ performHTTP2Request mtlsConfig req hostname port = try $ do pure $ resp $> foldMap byteString b withHTTP2Request :: - Maybe TLS.ClientParams -> + Maybe SSLContext -> HTTP2.Request -> ByteString -> Int -> (StreamingResponse -> IO a) -> IO a -withHTTP2Request mtlsConfig req hostname port k = do +withHTTP2Request mSSLCtx req hostname port k = do let clientConfig = HTTP2.ClientConfig - "https" - hostname - {- cacheLimit: -} 20 + { HTTP2.scheme = "https", + HTTP2.authority = hostname, + HTTP2.cacheLimit = 20 + } E.handle (E.throw . FederatorClientHTTP2Exception) $ bracket (connectSocket hostname port) NS.close $ \sock -> do - let withHTTP2Config k' = case mtlsConfig of + let withHTTP2Config k' = case mSSLCtx of Nothing -> bracket (HTTP2.allocSimpleConfig sock 4096) HTTP2.freeSimpleConfig k' - -- FUTUREWORK(federation): Use openssl - Just tlsConfig -> do - ctx <- E.handle (E.throw . FederatorClientTLSException) $ do - ctx <- TLS.contextNew sock tlsConfig - TLS.handshake ctx - pure ctx - bracket (allocTLSConfig ctx 4096) freeTLSConfig k' + Just sslCtx -> do + ssl <- E.handle (E.throw . FederatorClientTLSException) $ do + ssl <- SSL.connection sslCtx sock + -- We need to strip trailing dot because openssl doesn't ignore + -- it. https://github.com/openssl/openssl/issues/11560 + let hostnameStr = + Text.unpack $ case Text.decodeUtf8 hostname of + (Text.stripSuffix "." -> Just withoutTrailingDot) -> withoutTrailingDot + noTrailingDot -> noTrailingDot + SSL.setTlsextHostName ssl hostnameStr + SSL.enableHostnameValidation ssl hostnameStr + SSL.connect ssl + pure ssl + bracket (allocTLSConfig ssl 4096) freeTLSConfig k' withHTTP2Config $ \conf -> do HTTP2.run clientConfig conf $ \sendRequest -> sendRequest req $ \resp -> do let headers = headersFromTable (HTTP2.responseHeaders resp) - result = fromAction BS.null (HTTP2.getResponseBodyChunk resp) + result = fromAction BS.null $ HTTP2.getResponseBodyChunk resp case HTTP2.responseStatus resp of Nothing -> E.throw FederatorClientNoStatusCode Just status -> @@ -293,7 +303,6 @@ mkFailureResponse status domain path body -- | Run federator client synchronously. runFederatorClient :: - KnownComponent c => FederatorClientEnv -> FederatorClient c a -> IO (Either FederatorClientError a) @@ -303,7 +312,6 @@ runFederatorClient env = runFederatorClientToCodensity :: forall c a. - KnownComponent c => FederatorClientEnv -> FederatorClient c a -> Codensity IO (Either FederatorClientError a) @@ -317,7 +325,6 @@ runFederatorClientToCodensity env action = runExceptT $ do action runVersionedFederatorClientToCodensity :: - KnownComponent c => FederatorClientVersionedEnv -> FederatorClient c a -> ExceptT FederatorClientError (Codensity IO) a @@ -354,31 +361,23 @@ versionNegotiation = freeTLSConfig :: HTTP2.Config -> IO () freeTLSConfig cfg = free (HTTP2.confWriteBuffer cfg) -allocTLSConfig :: TLS.Context -> HTTP2.BufferSize -> IO HTTP2.Config -allocTLSConfig ctx bufsize = do +allocTLSConfig :: SSL -> HTTP2.BufferSize -> IO HTTP2.Config +allocTLSConfig ssl bufsize = do buf <- mallocBytes bufsize timmgr <- System.TimeManager.initialize $ 30 * 1000000 - ref <- newIORef mempty let readData :: Int -> IO ByteString - readData n = do - chunk <- readIORef ref - if BS.length chunk >= n - then case BS.splitAt n chunk of - (result, chunk') -> do - writeIORef ref chunk' - pure result - else do - chunk' <- TLS.recvData ctx - if BS.null chunk' - then pure chunk - else do - modifyIORef ref (<> chunk') - readData n + -- Sometimes the frame header says that the payload length is 0. Reading 0 + -- bytes multiple times seems to be causing errors in openssl. I cannot + -- figure out why. The previous implementation didn't try to read from the + -- socket when trying to read 0 bytes, so special handling for 0 maintains + -- that behaviour. + readData 0 = pure "" + readData n = SSL.read ssl n `catch` \(_ :: SSL.ConnectionAbruptlyTerminated) -> pure mempty pure HTTP2.Config { HTTP2.confWriteBuffer = buf, HTTP2.confBufferSize = bufsize, - HTTP2.confSendAll = TLS.sendData ctx . LBS.fromStrict, + HTTP2.confSendAll = SSL.write ssl, HTTP2.confReadN = readData, HTTP2.confPositionReadMaker = HTTP2.defaultPositionReadMaker, HTTP2.confTimeoutManager = timmgr diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs b/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs index 8c6367f249..509e73aa61 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs @@ -21,12 +21,13 @@ module Wire.API.Federation.Endpoint ) where +import Data.Kind import Servant.API import Wire.API.ApplyMods import Wire.API.Federation.Domain import Wire.API.Routes.Named -type FedEndpointWithMods (mods :: [*]) name input output = +type FedEndpointWithMods (mods :: [Type]) name input output = Named name ( ApplyMods diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Error.hs b/libs/wire-api-federation/src/Wire/API/Federation/Error.hs index b56149ae48..1f6f297e80 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Error.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Error.hs @@ -84,9 +84,9 @@ import qualified Data.Text.Lazy as LT import Imports import Network.HTTP.Types.Status import qualified Network.HTTP.Types.Status as HTTP -import qualified Network.HTTP2.Frame as HTTP2 -import Network.TLS +import qualified Network.HTTP2.Client as HTTP2 import qualified Network.Wai.Utilities.Error as Wai +import OpenSSL.Session (SomeSSLException) import Servant.Client import Wire.API.Error @@ -94,7 +94,7 @@ import Wire.API.Error data FederatorClientHTTP2Error = FederatorClientNoStatusCode | FederatorClientHTTP2Exception HTTP2.HTTP2Error - | FederatorClientTLSException TLSException + | FederatorClientTLSException SomeSSLException | FederatorClientConnectionError IOException deriving (Show, Typeable) @@ -213,7 +213,7 @@ federationRemoteHTTP2Error (FederatorClientTLSException e) = Wai.mkError (HTTP.mkStatus 525 "SSL Handshake Failure") "federation-tls-error" - (LT.fromStrict (displayTLSException e)) + (LT.pack (displayException e)) federationRemoteHTTP2Error (FederatorClientConnectionError e) = Wai.mkError federatorConnectionRefusedStatus @@ -241,22 +241,6 @@ federationRemoteResponseError status = <> LT.pack (show (HTTP.statusCode status)) ) -displayTLSException :: TLSException -> Text -displayTLSException (Terminated _ reason err) = T.pack reason <> ": " <> displayTLSError err -displayTLSException (HandshakeFailed err) = T.pack "handshake failed: " <> displayTLSError err -displayTLSException ConnectionNotEstablished = T.pack "connection not established" - -displayTLSError :: TLSError -> Text -displayTLSError (Error_Misc msg) = T.pack msg -displayTLSError (Error_Protocol (msg, _, _)) = "protocol error: " <> T.pack msg -displayTLSError (Error_Certificate msg) = "certificate error: " <> T.pack msg -displayTLSError (Error_HandshakePolicy msg) = "handshake policy error: " <> T.pack msg -displayTLSError Error_EOF = "end-of-file error" -displayTLSError (Error_Packet msg) = "packet error: " <> T.pack msg -displayTLSError (Error_Packet_unexpected actual expected) = - "unexpected packet: " <> T.pack expected <> ", " <> "got " <> T.pack actual -displayTLSError (Error_Packet_Parsing msg) = "packet parsing error: " <> T.pack msg - federationServantErrorToWai :: ClientError -> Wai.Error federationServantErrorToWai (DecodeFailure msg _) = federationInvalidBody msg -- the following error is never thrown by federator client diff --git a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/Runner.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/Runner.hs index 44bc77ceff..7172b0f5d5 100644 --- a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/Runner.hs +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/Runner.hs @@ -60,7 +60,7 @@ testObject obj path = do pure exists -testFromJSONObjects :: forall a. (Typeable a, ToJSON a, FromJSON a, Eq a, Show a) => [(a, FilePath)] -> IO () +testFromJSONObjects :: forall a. (Typeable a, FromJSON a, Eq a, Show a) => [(a, FilePath)] -> IO () testFromJSONObjects = traverse_ (uncurry testFromJSONObject) testFromJSONObject :: forall a. (Typeable a, FromJSON a, Eq a, Show a) => a -> FilePath -> IO () diff --git a/libs/wire-api-federation/wire-api-federation.cabal b/libs/wire-api-federation/wire-api-federation.cabal index 81eae46d30..42151f8e48 100644 --- a/libs/wire-api-federation/wire-api-federation.cabal +++ b/libs/wire-api-federation/wire-api-federation.cabal @@ -73,6 +73,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: aeson >=2.0.1.0 @@ -85,6 +86,7 @@ library , either , errors , exceptions + , HsOpenSSL , http-media , http-types , http2 @@ -102,6 +104,7 @@ library , servant-client-core , servant-server , singletons + , singletons-th , sop-core , streaming-commons , swagger2 @@ -179,7 +182,7 @@ test-suite spec ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -rtsopts -with-rtsopts=-N + -threaded -rtsopts -with-rtsopts=-N -Wredundant-constraints build-tool-depends: hspec-discover:hspec-discover build-depends: @@ -214,6 +217,7 @@ test-suite spec , servant-client-core , servant-server , singletons + , singletons-th , sop-core , streaming-commons , swagger2 diff --git a/libs/wire-api/default.nix b/libs/wire-api/default.nix index 8564a5767a..8b7f20896e 100644 --- a/libs/wire-api/default.nix +++ b/libs/wire-api/default.nix @@ -79,9 +79,10 @@ , servant-swagger , servant-swagger-ui , singletons +, singletons-base +, singletons-th , sop-core , string-conversions -, swagger , swagger2 , tagged , tasty @@ -90,6 +91,7 @@ , tasty-quickcheck , text , time +, transitive-anns , types-common , unliftio , unordered-containers @@ -177,13 +179,15 @@ mkDerivation { servant-swagger servant-swagger-ui singletons + singletons-base + singletons-th sop-core string-conversions - swagger swagger2 tagged text time + transitive-anns types-common unordered-containers uri-bytestring diff --git a/libs/wire-api/src/Wire/API/ApplyMods.hs b/libs/wire-api/src/Wire/API/ApplyMods.hs index ad65fdb28e..70d5dc9811 100644 --- a/libs/wire-api/src/Wire/API/ApplyMods.hs +++ b/libs/wire-api/src/Wire/API/ApplyMods.hs @@ -17,8 +17,9 @@ module Wire.API.ApplyMods where +import Data.Kind import Servant.API -type family ApplyMods (mods :: [*]) api where +type family ApplyMods (mods :: [Type]) api where ApplyMods '[] api = api ApplyMods (x ': xs) api = x :> ApplyMods xs api diff --git a/libs/wire-api/src/Wire/API/Call/Config.hs b/libs/wire-api/src/Wire/API/Call/Config.hs index f01c1274ff..e937e71499 100644 --- a/libs/wire-api/src/Wire/API/Call/Config.hs +++ b/libs/wire-api/src/Wire/API/Call/Config.hs @@ -239,7 +239,7 @@ parseTurnURI = parseOnly (parser <* endOfInput) parseScheme = parse "parseScheme" parseHost = parse "parseHost" parseTransport = parse "parseTransport" - parse :: (BC.FromByteString b, Monad m, MonadFail m) => String -> Text -> m b + parse :: (BC.FromByteString b, MonadFail m) => String -> Text -> m b parse err x = case BC.fromByteString (TE.encodeUtf8 x) of Just ok -> pure ok Nothing -> fail (err ++ " failed when parsing: " ++ show x) diff --git a/libs/wire-api/src/Wire/API/Connection.hs b/libs/wire-api/src/Wire/API/Connection.hs index 6da96c6345..cc3ccb3704 100644 --- a/libs/wire-api/src/Wire/API/Connection.hs +++ b/libs/wire-api/src/Wire/API/Connection.hs @@ -37,11 +37,6 @@ module Wire.API.Connection ConnectionRequest (..), ConnectionUpdate (..), ListConnectionsRequestPaginated, - - -- * Swagger - modelConnectionList, - modelConnection, - modelConnectionUpdate, ) where @@ -54,7 +49,6 @@ import Data.Qualified (Qualified (qUnqualified), deprecatedSchema) import Data.Range import Data.Schema import qualified Data.Swagger as S -import qualified Data.Swagger.Build.Api as Doc import Data.Text as Text import Imports import Servant.API @@ -94,13 +88,6 @@ instance ToSchema UserConnectionList where <$> clConnections .= field "connections" (array schema) <*> clHasMore .= fieldWithDocModifier "has_more" (description ?~ "Indicator that the server has more connections than returned.") schema -modelConnectionList :: Doc.Model -modelConnectionList = Doc.defineModel "UserConnectionList" $ do - Doc.description "A list of user connections." - Doc.property "connections" (Doc.unique $ Doc.array (Doc.ref modelConnection)) Doc.end - Doc.property "has_more" Doc.bool' $ - Doc.description "Indicator that the server has more connections than returned." - -------------------------------------------------------------------------------- -- UserConnection @@ -135,24 +122,6 @@ instance ToSchema UserConnection where <* (fmap qUnqualified . ucConvId) .= maybe_ (optField "conversation" (deprecatedSchema "qualified_conversation" schema)) -modelConnection :: Doc.Model -modelConnection = Doc.defineModel "Connection" $ do - Doc.description "Directed connection between two users" - Doc.property "from" Doc.bytes' $ - Doc.description "User ID" - Doc.property "to" Doc.bytes' $ - Doc.description "User ID" - Doc.property "status" typeRelation $ - Doc.description "Relation status" - Doc.property "last_update" Doc.dateTime' $ - Doc.description "Timestamp of last update" - Doc.property "message" Doc.string' $ do - Doc.description "Message" - Doc.optional - Doc.property "conversation" Doc.bytes' $ do - Doc.description "Conversation ID" - Doc.optional - -------------------------------------------------------------------------------- -- Relation @@ -221,19 +190,6 @@ relationDropHistory = \case MissingLegalholdConsentFromSent -> MissingLegalholdConsent MissingLegalholdConsentFromCancelled -> MissingLegalholdConsent -typeRelation :: Doc.DataType -typeRelation = - Doc.string $ - Doc.enum - [ "accepted", - "blocked", - "pending", - "ignored", - "sent", - "cancelled", - "missing-legalhold-consent" - ] - instance ToSchema Relation where schema = enum @Text "Relation" $ @@ -307,9 +263,3 @@ instance ToSchema ConnectionUpdate where object "ConnectionUpdate" $ ConnectionUpdate <$> cuStatus .= fieldWithDocModifier "status" (description ?~ "New relation status") schema - -modelConnectionUpdate :: Doc.Model -modelConnectionUpdate = Doc.defineModel "ConnectionUpdate" $ do - Doc.description "Connection update" - Doc.property "status" typeRelation $ - Doc.description "New relation status" diff --git a/libs/wire-api/src/Wire/API/Conversation.hs b/libs/wire-api/src/Wire/API/Conversation.hs index 08982bc472..ee9b7c6e2a 100644 --- a/libs/wire-api/src/Wire/API/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Conversation.hs @@ -80,20 +80,6 @@ module Wire.API.Conversation -- * re-exports module Wire.API.Conversation.Member, - - -- * Swagger - modelConversation, - modelConversations, - modelConversationIds, - modelInvite, - modelNewConversation, - modelTeamInfo, - modelConversationUpdateName, - modelConversationAccessData, - modelConversationReceiptModeUpdate, - modelConversationMessageTimerUpdate, - typeConversationType, - typeAccess, ) where @@ -114,7 +100,6 @@ 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 @@ -294,43 +279,6 @@ conversationSchema v = <*> cnvMembers .= field "members" schema <*> cnvProtocol .= protocolSchema -modelConversation :: Doc.Model -modelConversation = Doc.defineModel "Conversation" $ do - Doc.description "A conversation object as returned from the server" - Doc.property "id" Doc.bytes' $ - Doc.description "Conversation ID" - Doc.property "type" typeConversationType $ - Doc.description "The conversation type of this object (0 = regular, 1 = self, 2 = 1:1, 3 = connect)" - Doc.property "creator" Doc.bytes' $ - Doc.description "The creator's user ID." - -- TODO: Doc.property "access" - -- Doc.property "access_role" - Doc.property "name" Doc.string' $ do - Doc.description "The conversation name (can be null)" - Doc.property "members" (Doc.ref modelConversationMembers) $ - Doc.description "The current set of conversation members" - -- Doc.property "team" - Doc.property "message_timer" (Doc.int64 (Doc.min 0)) $ do - Doc.description "Per-conversation message timer (can be null)" - --- | This is used to describe a @ConversationList ConvId@. --- --- FUTUREWORK: Create a new ConversationIdList type instead. -modelConversationIds :: Doc.Model -modelConversationIds = Doc.defineModel "ConversationIds" $ do - Doc.description "Object holding a list of conversation IDs" - Doc.property "conversations" (Doc.unique $ Doc.array Doc.string') Doc.end - Doc.property "has_more" Doc.bool' $ - Doc.description "Indicator that the server has more IDs than returned" - --- | This is used to describe a @ConversationList Conversation@. -modelConversations :: Doc.Model -modelConversations = Doc.defineModel "Conversations" $ do - Doc.description "Object holding a list of conversations" - Doc.property "conversations" (Doc.unique $ Doc.array (Doc.ref modelConversation)) Doc.end - Doc.property "has_more" Doc.bool' $ - Doc.description "Indicator that the server has more conversations than returned" - -- | Limited view of a 'Conversation'. Is used to inform users with an invite -- link about the conversation. data ConversationCoverView = ConversationCoverView @@ -485,9 +433,6 @@ instance ToSchema Access where element "code" CodeAccess ] -typeAccess :: Doc.DataType -typeAccess = Doc.string . Doc.enum $ cs . A.encode <$> [(minBound :: Access) ..] - -- | AccessRoles define who can join conversations. The roles are -- "supersets", i.e. Activated includes Team and NonActivated includes -- Activated. @@ -547,7 +492,7 @@ data AccessRole genAccessRolesV2 :: [AccessRole] -> [AccessRole] -> IO (Either String (Set AccessRole)) genAccessRolesV2 = genEnumSet -genEnumSet :: forall a. (Bounded a, Enum a, Ord a, Eq a, Show a) => [a] -> [a] -> IO (Either String (Set a)) +genEnumSet :: forall a. (Bounded a, Enum a, Ord a, Show a) => [a] -> [a] -> IO (Either String (Set a)) genEnumSet with without = if disjointOrd with without then do @@ -629,9 +574,6 @@ instance ToSchema ConvType where element 3 ConnectConv ] -typeConversationType :: Doc.DataType -typeConversationType = Doc.int32 $ Doc.enum [0, 1, 2, 3] - -- | Define whether receipts should be sent in the given conversation -- This datatype is defined as an int32 but the Backend does not -- interpret it in any way, rather just stores and forwards it @@ -653,29 +595,6 @@ instance ToSchema ReceiptMode where -------------------------------------------------------------------------------- -- create --- | Used to describe a 'NewConv'. -modelNewConversation :: Doc.Model -modelNewConversation = Doc.defineModel "NewConversation" $ do - Doc.description "JSON object to create a new conversation" - Doc.property "users" (Doc.unique $ Doc.array Doc.bytes') $ - Doc.description "List of user IDs (excluding the requestor) to be part of this conversation" - Doc.property "qualified_users" (Doc.unique . Doc.array $ Doc.bytes') $ - Doc.description "List of qualified user IDs to be part of this conversation" - Doc.property "name" Doc.string' $ do - Doc.description "The conversation name" - Doc.optional - Doc.property "team" (Doc.ref modelTeamInfo) $ do - Doc.description "Team information of this conversation" - Doc.optional - -- TODO: Doc.property "access" - -- Doc.property "access_role" - Doc.property "message_timer" (Doc.int64 (Doc.min 0)) $ do - Doc.description "Per-conversation message timer" - Doc.optional - Doc.property "receipt_mode" (Doc.int32 (Doc.min 0)) $ do - Doc.description "Conversation receipt mode" - Doc.optional - data NewConv = NewConv { newConvUsers :: [UserId], -- | A list of qualified users, which can include some local qualified users @@ -690,10 +609,7 @@ data NewConv = NewConv -- | Every member except for the creator will have this role newConvUsersRole :: RoleName, -- | The protocol of the conversation. It can be Proteus or MLS (1.0). - newConvProtocol :: ProtocolTag, - -- | ID of the client creating the conversation. Only needed for MLS - -- conversations. - newConvCreatorClient :: Maybe ClientId + newConvProtocol :: ProtocolTag } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform NewConv) @@ -753,7 +669,6 @@ newConvSchema sch = <|> pure roleNameWireAdmin ) <*> newConvProtocol .= protocolTagSchema - <*> newConvCreatorClient .= maybe_ (optField "creator_client" schema) where usersDesc = "List of user IDs (excluding the requestor) to be \ @@ -798,14 +713,6 @@ instance ToSchema ConvTeamInfo where c :: ToJSON a => a -> ValueSchema SwaggerDoc () c val = mkSchema mempty (const (pure ())) (const (pure (toJSON val))) -modelTeamInfo :: Doc.Model -modelTeamInfo = Doc.defineModel "TeamInfo" $ do - Doc.description "Team information" - Doc.property "teamid" Doc.bytes' $ - Doc.description "Team ID" - Doc.property "managed" Doc.bool' $ - Doc.description managedDesc - -------------------------------------------------------------------------------- -- invite @@ -847,12 +754,6 @@ instance ToSchema InviteQualified where newInvite :: List1 UserId -> Invite newInvite us = Invite us roleNameWireAdmin -modelInvite :: Doc.Model -modelInvite = Doc.defineModel "Invite" $ do - Doc.description "Add users to a conversation" - Doc.property "users" (Doc.unique $ Doc.array Doc.bytes') $ - Doc.description "List of user IDs to add to a conversation" - -------------------------------------------------------------------------------- -- update @@ -875,12 +776,6 @@ instance ToSchema ConversationRename where where desc = "The new conversation name" -modelConversationUpdateName :: Doc.Model -modelConversationUpdateName = Doc.defineModel "ConversationUpdateName" $ do - Doc.description "Contains conversation name to update" - Doc.property "name" Doc.string' $ - Doc.description "The new conversation name" - data ConversationAccessData = ConversationAccessData { cupAccess :: Set Access, cupAccessRoles :: Set AccessRole @@ -906,14 +801,6 @@ instance ToSchema ConversationAccessData where instance ToSchema (Versioned 'V2 ConversationAccessData) where schema = Versioned <$> unVersioned .= conversationAccessDataSchema V2 -modelConversationAccessData :: Doc.Model -modelConversationAccessData = Doc.defineModel "ConversationAccessData" $ do - Doc.description "Contains conversation properties to update" - Doc.property "access" (Doc.unique $ Doc.array typeAccess) $ - Doc.description "List of conversation access modes." - Doc.property "access_role" Doc.bytes' $ - Doc.description "Conversation access role: private|team|activated|non_activated" - data ConversationReceiptModeUpdate = ConversationReceiptModeUpdate { cruReceiptMode :: ReceiptMode } @@ -932,15 +819,6 @@ instance ToSchema ConversationReceiptModeUpdate where \clients whether certain types of receipts should be sent in the given \ \conversation or not. How this value is interpreted is up to clients." -modelConversationReceiptModeUpdate :: Doc.Model -modelConversationReceiptModeUpdate = Doc.defineModel "conversationReceiptModeUpdate" $ do - Doc.description - "Contains conversation receipt mode to update to. Receipt mode tells \ - \clients whether certain types of receipts should be sent in the given \ - \conversation or not. How this value is interpreted is up to clients." - Doc.property "receipt_mode" Doc.int32' $ - Doc.description "Receipt mode: int32" - data ConversationMessageTimerUpdate = ConversationMessageTimerUpdate { -- | New message timer cupMessageTimer :: Maybe Milliseconds @@ -957,12 +835,6 @@ instance ToSchema ConversationMessageTimerUpdate where $ ConversationMessageTimerUpdate <$> cupMessageTimer .= optField "message_timer" (maybeWithDefault A.Null schema) -modelConversationMessageTimerUpdate :: Doc.Model -modelConversationMessageTimerUpdate = Doc.defineModel "ConversationMessageTimerUpdate" $ do - Doc.description "Contains conversation properties to update" - Doc.property "message_timer" Doc.int64' $ - Doc.description "Conversation message timer (in milliseconds); can be null" - data ConversationJoin = ConversationJoin { cjUsers :: NonEmpty (Qualified UserId), cjRole :: RoleName diff --git a/libs/wire-api/src/Wire/API/Conversation/Action.hs b/libs/wire-api/src/Wire/API/Conversation/Action.hs index 83edcf73e2..5d98193fd3 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Action.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Action.hs @@ -36,6 +36,7 @@ import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson as A import qualified Data.Aeson.KeyMap as A import Data.Id +import Data.Kind import qualified Data.List.NonEmpty as NonEmptyList import Data.Qualified (Qualified) import Data.Schema hiding (tag) @@ -52,7 +53,7 @@ import Wire.Arbitrary (Arbitrary (..)) -- | We use this type family instead of a sum type to be able to define -- individual effects per conversation action. See 'HasConversationActionEffects'. -type family ConversationAction (tag :: ConversationActionTag) :: * where +type family ConversationAction (tag :: ConversationActionTag) :: Type where ConversationAction 'ConversationJoinTag = ConversationJoin ConversationAction 'ConversationLeaveTag = () ConversationAction 'ConversationMemberUpdateTag = ConversationMemberUpdate diff --git a/libs/wire-api/src/Wire/API/Conversation/Code.hs b/libs/wire-api/src/Wire/API/Conversation/Code.hs index 2120f5a680..03e3dc30aa 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Code.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Code.hs @@ -26,9 +26,6 @@ module Wire.API.Conversation.Code -- * re-exports Code.Key (..), Value (..), - - -- * Swagger - modelConversationCode, ) where @@ -40,7 +37,6 @@ import Data.Code as Code import Data.Misc (HttpsUrl (HttpsUrl)) import Data.Schema import qualified Data.Swagger as S -import qualified Data.Swagger.Build.Api as Doc import Imports import qualified URI.ByteString as URI import Wire.Arbitrary (Arbitrary, GenericUniform (..)) @@ -54,17 +50,6 @@ data ConversationCode = ConversationCode deriving (Arbitrary) via (GenericUniform ConversationCode) deriving (FromJSON, ToJSON, S.ToSchema) via Schema ConversationCode -modelConversationCode :: Doc.Model -modelConversationCode = Doc.defineModel "ConversationCode" $ do - Doc.description "Contains conversation properties to update" - Doc.property "key" Doc.string' $ - Doc.description "Stable conversation identifier" - Doc.property "code" Doc.string' $ - Doc.description "Conversation code (random)" - Doc.property "uri" Doc.string' $ do - Doc.description "Full URI (containing key/code) to join a conversation" - Doc.optional - instance ToSchema ConversationCode where schema = objectWithDocModifier diff --git a/libs/wire-api/src/Wire/API/Conversation/Member.hs b/libs/wire-api/src/Wire/API/Conversation/Member.hs index f79d8e530e..b6f76ec19e 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Member.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Member.hs @@ -30,13 +30,6 @@ module Wire.API.Conversation.Member MemberUpdate (..), memberUpdate, OtherMemberUpdate (..), - - -- * Swagger - modelConversationMembers, - modelOtherMember, - modelMember, - modelMemberUpdate, - modelOtherMemberUpdate, ) where @@ -48,11 +41,10 @@ import Data.Id import Data.Qualified import Data.Schema import qualified Data.Swagger as S -import qualified Data.Swagger.Build.Api as Doc import Imports import qualified Test.QuickCheck as QC import Wire.API.Conversation.Role -import Wire.API.Provider.Service (ServiceRef, modelServiceRef) +import Wire.API.Provider.Service (ServiceRef) import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) data ConvMembers = ConvMembers @@ -78,14 +70,6 @@ instance ToSchema ConvMembers where (description ?~ "All other current users of this conversation") (array schema) -modelConversationMembers :: Doc.Model -modelConversationMembers = Doc.defineModel "ConversationMembers" $ do - Doc.description "Object representing users of a conversation." - Doc.property "self" (Doc.ref modelMember) $ - Doc.description "The user ID of the requestor" - Doc.property "others" (Doc.unique (Doc.array (Doc.ref modelOtherMember))) $ - Doc.description "All other current users of this conversation" - -------------------------------------------------------------------------------- -- Members @@ -133,29 +117,6 @@ instance ToSchema Member where c :: ToJSON a => a -> ValueSchema SwaggerDoc () c val = mkSchema mempty (const (pure ())) (const (pure (toJSON val))) -modelMember :: Doc.Model -modelMember = Doc.defineModel "Member" $ do - Doc.property "id" Doc.bytes' $ - Doc.description "User ID" - Doc.property "otr_muted_ref" Doc.bytes' $ do - Doc.description "A reference point for (un)muting" - Doc.optional - Doc.property "otr_archived" Doc.bool' $ do - Doc.description "Whether the conversation is archived" - Doc.optional - Doc.property "otr_archived_ref" Doc.bytes' $ do - Doc.description "A reference point for (un)archiving" - Doc.optional - Doc.property "hidden" Doc.bool' $ do - Doc.description "Whether the conversation is hidden" - Doc.optional - Doc.property "hidden_ref" Doc.bytes' $ do - Doc.description "A reference point for (un)hiding" - Doc.optional - Doc.property "service" (Doc.ref modelServiceRef) $ do - Doc.description "The reference to the owning service, if the member is a 'bot'." - Doc.optional - -- | The semantics of the possible different values is entirely up to clients, -- the server will not interpret this value in any way. newtype MutedStatus = MutedStatus {fromMutedStatus :: Int32} @@ -187,14 +148,6 @@ instance ToSchema OtherMember where instance Ord OtherMember where compare a b = compare (omQualifiedId a) (omQualifiedId b) -modelOtherMember :: Doc.Model -modelOtherMember = Doc.defineModel "OtherMember" $ do - Doc.property "id" Doc.bytes' $ - Doc.description "User ID" - Doc.property "service" (Doc.ref modelServiceRef) $ do - Doc.description "The reference to the owning service, if the member is a 'bot'." - Doc.optional - -------------------------------------------------------------------------------- -- Member Updates @@ -214,25 +167,6 @@ data MemberUpdate = MemberUpdate memberUpdate :: MemberUpdate memberUpdate = MemberUpdate Nothing Nothing Nothing Nothing Nothing Nothing -modelMemberUpdate :: Doc.Model -modelMemberUpdate = Doc.defineModel "MemberUpdate" $ do - Doc.description "Update user properties relative to a conversation" - Doc.property "otr_muted_ref" Doc.bytes' $ do - Doc.description "A reference point for (un)muting" - Doc.optional - Doc.property "otr_archived" Doc.bool' $ do - Doc.description "Whether to notify on conversation updates" - Doc.optional - Doc.property "otr_archived_ref" Doc.bytes' $ do - Doc.description "A reference point for (un)archiving" - Doc.optional - Doc.property "hidden" Doc.bool' $ do - Doc.description "Whether the conversation is hidden" - Doc.optional - Doc.property "hidden_ref" Doc.bytes' $ do - Doc.description "A reference point for (un)hiding" - Doc.optional - instance ToSchema MemberUpdate where schema = (`withParser` (either fail pure . validateMemberUpdate)) @@ -272,13 +206,6 @@ data OtherMemberUpdate = OtherMemberUpdate deriving stock (Eq, Show, Generic) deriving (FromJSON, ToJSON, S.ToSchema) via (Schema OtherMemberUpdate) -modelOtherMemberUpdate :: Doc.Model -modelOtherMemberUpdate = Doc.defineModel "otherMemberUpdate" $ do - Doc.description "Update user properties of other members relative to a conversation" - Doc.property "conversation_role" Doc.string' $ do - Doc.description "Name of the conversation role updated to" - Doc.optional - instance Arbitrary OtherMemberUpdate where arbitrary = OtherMemberUpdate . Just <$> arbitrary diff --git a/libs/wire-api/src/Wire/API/Conversation/Role.hs b/libs/wire-api/src/Wire/API/Conversation/Role.hs index e215b72db8..4ab4642498 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Role.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Role.hs @@ -56,11 +56,6 @@ module Wire.API.Conversation.Role isValidRoleName, roleActions, toConvRole, - - -- * Swagger - modelConversationRole, - modelConversationRolesList, - typeConversationRoleAction, ) where @@ -78,13 +73,55 @@ import Data.Schema import qualified Data.Set as Set import Data.Singletons.TH import qualified Data.Swagger as S -import qualified Data.Swagger.Build.Api as Doc import qualified Deriving.Swagger as S import GHC.TypeLits import Imports import qualified Test.QuickCheck as QC import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) +-------------------------------------------------------------------------------- +-- Action + +newtype Actions = Actions + { allowedActions :: Set Action + } + deriving stock (Eq, Show, Generic) + deriving newtype (Arbitrary) + +allActions :: Actions +allActions = Actions $ Set.fromList [minBound .. maxBound] + +-- | These conversation-level permissions. Analogous to the team-level permissions called +-- 'Perm' (or 'Permissions'). +data Action + = AddConversationMember + | RemoveConversationMember + | ModifyConversationName + | ModifyConversationMessageTimer + | ModifyConversationReceiptMode + | ModifyConversationAccess + | ModifyOtherConversationMember + | LeaveConversation + | DeleteConversation + deriving stock (Eq, Ord, Show, Enum, Bounded, Generic) + deriving (Arbitrary) via (GenericUniform Action) + deriving (S.ToSchema) via (S.CustomSwagger '[S.ConstructorTagModifier S.CamelToSnake] Action) + +type family ActionName (a :: Action) :: Symbol where + ActionName 'AddConversationMember = "add_conversation_member" + ActionName 'RemoveConversationMember = "remove_conversation_member" + ActionName 'ModifyConversationName = "modify_conversation_name" + ActionName 'ModifyConversationMessageTimer = "modify_conversation_message_timer" + ActionName 'ModifyConversationReceiptMode = "modify_conversation_receipt_mode" + ActionName 'ModifyConversationAccess = "modify_conversation_access" + ActionName 'ModifyOtherConversationMember = "modify_other_conversation_member" + ActionName 'LeaveConversation = "leave_conversation" + ActionName 'DeleteConversation = "delete_conversation" + +A.deriveJSON A.defaultOptions {A.constructorTagModifier = A.camelTo2 '_'} ''Action + +$(genSingletons [''Action]) + -------------------------------------------------------------------------------- -- Role @@ -100,17 +137,6 @@ data ConversationRole deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ConversationRole) -modelConversationRole :: Doc.Model -modelConversationRole = Doc.defineModel "ConversationRole" $ do - Doc.description "Conversation role" - Doc.property "conversation_role" Doc.string' $ - Doc.description - "role name, between 2 and 128 chars, 'wire_' prefix \ - \is reserved for roles designed by Wire (i.e., no \ - \custom roles can have the same prefix)" - Doc.property "actions" (Doc.array typeConversationRoleAction) $ - Doc.description "The set of actions allowed for this role" - instance S.ToSchema ConversationRole where declareNamedSchema _ = do conversationRoleSchema <- @@ -176,12 +202,6 @@ data ConversationRolesList = ConversationRolesList deriving (Arbitrary) via (GenericUniform ConversationRolesList) deriving (S.ToSchema) via (S.CustomSwagger '[S.FieldLabelModifier (S.LabelMappings '["convRolesList" 'S.:-> "conversation_roles"])] ConversationRolesList) -modelConversationRolesList :: Doc.Model -modelConversationRolesList = Doc.defineModel "ConversationRolesList" $ do - Doc.description "list of roles allowed in the given conversation" - Doc.property "conversation_roles" (Doc.unique $ Doc.array (Doc.ref modelConversationRole)) $ - Doc.description "the array of conversation roles" - instance ToJSON ConversationRolesList where toJSON (ConversationRolesList r) = A.object @@ -251,61 +271,3 @@ isValidRoleName = *> count 126 (optional (satisfy chars)) *> endOfInput chars = inClass "a-z0-9_" - --------------------------------------------------------------------------------- --- Action - -newtype Actions = Actions - { allowedActions :: Set Action - } - deriving stock (Eq, Show, Generic) - deriving newtype (Arbitrary) - -allActions :: Actions -allActions = Actions $ Set.fromList [minBound .. maxBound] - --- | These conversation-level permissions. Analogous to the team-level permissions called --- 'Perm' (or 'Permissions'). -data Action - = AddConversationMember - | RemoveConversationMember - | ModifyConversationName - | ModifyConversationMessageTimer - | ModifyConversationReceiptMode - | ModifyConversationAccess - | ModifyOtherConversationMember - | LeaveConversation - | DeleteConversation - deriving stock (Eq, Ord, Show, Enum, Bounded, Generic) - deriving (Arbitrary) via (GenericUniform Action) - deriving (S.ToSchema) via (S.CustomSwagger '[S.ConstructorTagModifier S.CamelToSnake] Action) - -type family ActionName (a :: Action) :: Symbol where - ActionName 'AddConversationMember = "add_conversation_member" - ActionName 'RemoveConversationMember = "remove_conversation_member" - ActionName 'ModifyConversationName = "modify_conversation_name" - ActionName 'ModifyConversationMessageTimer = "modify_conversation_message_timer" - ActionName 'ModifyConversationReceiptMode = "modify_conversation_receipt_mode" - ActionName 'ModifyConversationAccess = "modify_conversation_access" - ActionName 'ModifyOtherConversationMember = "modify_other_conversation_member" - ActionName 'LeaveConversation = "leave_conversation" - ActionName 'DeleteConversation = "delete_conversation" - -typeConversationRoleAction :: Doc.DataType -typeConversationRoleAction = - Doc.string $ - Doc.enum - [ "add_conversation_member", - "remove_conversation_member", - "modify_conversation_name", - "modify_conversation_message_timer", - "modify_conversation_receipt_mode", - "modify_conversation_access", - "modify_other_conversation_member", - "leave_conversation", - "delete_conversation" - ] - -A.deriveJSON A.defaultOptions {A.constructorTagModifier = A.camelTo2 '_'} ''Action - -$(genSingletons [''Action]) diff --git a/libs/wire-api/src/Wire/API/Conversation/Typing.hs b/libs/wire-api/src/Wire/API/Conversation/Typing.hs index 4f297761d0..fb6625af52 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Typing.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Typing.hs @@ -17,25 +17,15 @@ module Wire.API.Conversation.Typing ( TypingStatus (..), - - -- * Swagger - modelTyping, - typeTypingStatus, ) where import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Schema import qualified Data.Swagger as S -import qualified Data.Swagger.Build.Api as Doc import Imports import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -modelTyping :: Doc.Model -modelTyping = Doc.defineModel "Typing" $ do - Doc.description "Data to describe typing info" - Doc.property "status" typeTypingStatus $ Doc.description "typing status" - data TypingStatus = StartedTyping | StoppedTyping @@ -53,11 +43,3 @@ typingStatusSchema = enum @Text "TypingStatus" $ element "started" StartedTyping <> element "stopped" StoppedTyping - -typeTypingStatus :: Doc.DataType -typeTypingStatus = - Doc.string $ - Doc.enum - [ "started", - "stopped" - ] diff --git a/libs/wire-api/src/Wire/API/Error.hs b/libs/wire-api/src/Wire/API/Error.hs index c34bf13a5f..6f2d7a9725 100644 --- a/libs/wire-api/src/Wire/API/Error.hs +++ b/libs/wire-api/src/Wire/API/Error.hs @@ -46,6 +46,7 @@ where import Control.Lens (at, (%~), (.~), (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson as A +import Data.Kind import Data.Metrics.Servant import Data.Proxy import Data.SOP @@ -57,7 +58,6 @@ import GHC.TypeLits import Imports hiding (All) import Network.HTTP.Types.Status import qualified Network.Wai.Utilities.Error as Wai -import Numeric.Natural import Polysemy import Polysemy.Error import Servant @@ -204,8 +204,12 @@ errorResponseSwagger = addStaticErrorToSwagger :: forall e. KnownError e => S.Swagger -> S.Swagger addStaticErrorToSwagger = - S.allOperations . S.responses . S.responses . at (fromIntegral (eCode err)) - %~ Just . addRef + S.allOperations + . S.responses + . S.responses + . at (fromIntegral (eCode err)) + %~ Just + . addRef where err = dynError @e resp = errorResponseSwagger @e @@ -261,13 +265,13 @@ instance APIError Wai.Error where instance APIError DynError where toWai (DynError c l m) = Wai.mkError (toEnum (fromIntegral c)) (LT.fromStrict l) (LT.fromStrict m) -instance KnownError e => APIError (SStaticError e) where +instance APIError (SStaticError e) where toWai = toWai . dynError' -------------------------------------------------------------------------------- -- MultiVerb support -type family RespondWithStaticError (s :: StaticError) :: * where +type family RespondWithStaticError (s :: StaticError) :: Type where RespondWithStaticError ('StaticError s l m) = RespondAs JSON s m DynError type family StaticErrorStatus (s :: StaticError) :: Nat where diff --git a/libs/wire-api/src/Wire/API/Error/Brig.hs b/libs/wire-api/src/Wire/API/Error/Brig.hs index 7b1b337036..949d3c4572 100644 --- a/libs/wire-api/src/Wire/API/Error/Brig.hs +++ b/libs/wire-api/src/Wire/API/Error/Brig.hs @@ -43,7 +43,7 @@ data BrigError | HandleNotFound | UserCreationRestricted | BlacklistedPhone - | WhitelistError + | AllowlistError | InvalidInvitationCode | MissingIdentity | BlacklistedEmail @@ -135,7 +135,7 @@ type instance MapError 'MLSDuplicatePublicKey = 'StaticError 400 "mls-duplicate- type instance MapError 'BlacklistedPhone = 'StaticError 403 "blacklisted-phone" "The given phone number has been blacklisted due to suspected abuse or a complaint" -type instance MapError 'WhitelistError = 'StaticError 403 "unauthorized" "Unauthorized e-mail address or phone number." +type instance MapError 'AllowlistError = 'StaticError 403 "unauthorized" "Unauthorized e-mail address or phone number." type instance MapError 'InvalidInvitationCode = 'StaticError 400 "invalid-invitation-code" "Invalid invitation code." diff --git a/libs/wire-api/src/Wire/API/Error/Empty.hs b/libs/wire-api/src/Wire/API/Error/Empty.hs index 32bbf598fd..e7137fc6fb 100644 --- a/libs/wire-api/src/Wire/API/Error/Empty.hs +++ b/libs/wire-api/src/Wire/API/Error/Empty.hs @@ -52,7 +52,7 @@ instance responseUnrender _ output = guard (responseStatusCode output == statusVal (Proxy @s)) instance - (KnownStatus s, KnownSymbol desc) => + KnownSymbol desc => IsSwaggerResponse (EmptyErrorForLegacyReasons s desc) where responseSwagger = diff --git a/libs/wire-api/src/Wire/API/Error/Galley.hs b/libs/wire-api/src/Wire/API/Error/Galley.hs index 65596d70fa..5019282d33 100644 --- a/libs/wire-api/src/Wire/API/Error/Galley.hs +++ b/libs/wire-api/src/Wire/API/Error/Galley.hs @@ -31,8 +31,7 @@ where import Control.Lens ((%~)) import Data.Aeson (FromJSON (..), ToJSON (..)) -import Data.Singletons.CustomStar (genSingletons) -import Data.Singletons.Prelude (Show_) +import Data.Singletons.TH (genSingletons) import qualified Data.Swagger as S import Data.Tagged import GHC.TypeLits @@ -40,6 +39,7 @@ import Imports import qualified Network.Wai.Utilities.Error as Wai import Polysemy import Polysemy.Error +import Prelude.Singletons (Show_) import Wire.API.Conversation.Role import Wire.API.Error import qualified Wire.API.Error.Brig as BrigError @@ -117,6 +117,8 @@ data GalleyError | UserLegalHoldNotPending | -- Team Member errors BulkGetMemberLimitExceeded + | -- Team Notification errors + InvalidTeamNotificationId deriving (Show, Eq, Generic) deriving (FromJSON, ToJSON) via (CustomEncoded GalleyError) @@ -178,6 +180,8 @@ type instance MapError 'ConvNotFound = 'StaticError 404 "no-conversation" "Conve type instance MapError 'ConvAccessDenied = 'StaticError 403 "access-denied" "Conversation access denied" +type instance MapError 'InvalidTeamNotificationId = 'StaticError 400 "invalid-notification-id" "Could not parse notification id (must be UUIDv1)." + type instance MapError 'MLSNotEnabled = 'StaticError diff --git a/libs/wire-api/src/Wire/API/Event/Conversation.hs b/libs/wire-api/src/Wire/API/Event/Conversation.hs index 326fce09cf..2b3f21610e 100644 --- a/libs/wire-api/src/Wire/API/Event/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Event/Conversation.hs @@ -133,7 +133,7 @@ data EventType | MLSMessageAdd | MLSWelcome | Typing - deriving stock (Eq, Show, Generic, Enum, Bounded) + deriving stock (Eq, Show, Generic, Enum, Bounded, Ord) deriving (Arbitrary) via (GenericUniform EventType) deriving (FromJSON, ToJSON, S.ToSchema) via Schema EventType diff --git a/libs/wire-api/src/Wire/API/Event/Team.hs b/libs/wire-api/src/Wire/API/Event/Team.hs index 5dfec797c7..7c78ede309 100644 --- a/libs/wire-api/src/Wire/API/Event/Team.hs +++ b/libs/wire-api/src/Wire/API/Event/Team.hs @@ -32,15 +32,6 @@ module Wire.API.Event.Team -- * EventData EventData (..), - - -- * Swagger - modelEvent, - modelMemberEvent, - modelMemberData, - modelConvEvent, - modelConversationData, - modelUpdateEvent, - typeEventType, ) where @@ -53,11 +44,10 @@ import Data.Id (ConvId, TeamId, UserId) import Data.Json.Util import Data.Schema import qualified Data.Swagger as S -import qualified Data.Swagger.Build.Api as Doc import Data.Time (UTCTime) import Imports import qualified Test.QuickCheck as QC -import Wire.API.Team (Team, TeamUpdateData, modelUpdateData) +import Wire.API.Team (Team, TeamUpdateData) import Wire.API.Team.Permission (Permissions) import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) @@ -235,65 +225,3 @@ genEventData = \case ConvDelete -> EdConvDelete <$> arbitrary makeLenses ''Event - ----------------------------------------------------------------------- --- swagger1.2 stuff, to be removed in --- https://wearezeta.atlassian.net/browse/SQSERVICES-1096 - -typeEventType :: Doc.DataType -typeEventType = - Doc.string $ - Doc.enum - [ "team.create", - "team.delete", - "team.update", - "team.member-join", - "team.member-leave", - "team.conversation-create", - "team.conversation-delete" - ] - -modelEvent :: Doc.Model -modelEvent = Doc.defineModel "TeamEvent" $ do - Doc.description "team event data" - Doc.property "type" typeEventType $ - Doc.description "event type" - Doc.property "team" Doc.bytes' $ - Doc.description "team ID" - Doc.property "time" Doc.dateTime' $ - Doc.description "date and time this event occurred" - -- This doesn't really seem to work in swagger-ui. - -- The children/subTypes are not displayed. - Doc.children - "type" - [ modelMemberEvent, - modelConvEvent, - modelUpdateEvent - ] - -modelMemberEvent :: Doc.Model -modelMemberEvent = Doc.defineModel "TeamMemberEvent" $ do - Doc.description "team member event" - Doc.property "data" (Doc.ref modelMemberData) $ Doc.description "member data" - -modelMemberData :: Doc.Model -modelMemberData = - Doc.defineModel "MemberData" $ - Doc.property "user" Doc.bytes' $ - Doc.description "user ID" - -modelConvEvent :: Doc.Model -modelConvEvent = Doc.defineModel "TeamConversationEvent" $ do - Doc.description "team conversation event" - Doc.property "data" (Doc.ref modelConversationData) $ Doc.description "conversation data" - -modelConversationData :: Doc.Model -modelConversationData = - Doc.defineModel "ConversationData" $ - Doc.property "conv" Doc.bytes' $ - Doc.description "conversation ID" - -modelUpdateEvent :: Doc.Model -modelUpdateEvent = Doc.defineModel "TeamUpdateEvent" $ do - Doc.description "team update event" - Doc.property "data" (Doc.ref modelUpdateData) $ Doc.description "update data" diff --git a/libs/wire-api/src/Wire/API/Internal/BulkPush.hs b/libs/wire-api/src/Wire/API/Internal/BulkPush.hs index 7a14fce1fc..ffa578a217 100644 --- a/libs/wire-api/src/Wire/API/Internal/BulkPush.hs +++ b/libs/wire-api/src/Wire/API/Internal/BulkPush.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE TemplateHaskell #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -19,9 +17,12 @@ module Wire.API.Internal.BulkPush where +import Control.Lens import Data.Aeson -import Data.Aeson.TH (deriveJSON) import Data.Id +import Data.Schema (ValueSchema) +import qualified Data.Schema as S +import qualified Data.Swagger as Swagger import Imports import Wire.API.Internal.Notification @@ -35,13 +36,14 @@ data PushTarget = PushTarget Show, Generic ) + deriving (FromJSON, ToJSON, Swagger.ToSchema) via S.Schema PushTarget -instance FromJSON PushTarget where - parseJSON = withObject "push target object" $ \hm -> - PushTarget <$> (hm .: "user_id") <*> (hm .: "conn_id") - -instance ToJSON PushTarget where - toJSON (PushTarget u c) = object ["user_id" .= u, "conn_id" .= c] +instance S.ToSchema PushTarget where + schema = + S.object "PushTarget" $ + PushTarget + <$> ptUserId S..= S.field "user_id" S.schema + <*> ptConnId S..= S.field "conn_id" S.schema newtype BulkPushRequest = BulkPushRequest { fromBulkPushRequest :: [(Notification, [PushTarget])] @@ -51,23 +53,32 @@ newtype BulkPushRequest = BulkPushRequest Show, Generic ) + deriving (ToJSON, FromJSON, Swagger.ToSchema) via S.Schema BulkPushRequest -instance FromJSON BulkPushRequest where - parseJSON = withObject "bulkpush request body" $ \hm -> - BulkPushRequest <$> (mapM run =<< (hm .: "bulkpush_req")) - where - run = withObject "object with notifcation, targets" $ \hm -> - (,) <$> (hm .: "notification") <*> (hm .: "targets") - -instance ToJSON BulkPushRequest where - toJSON (BulkPushRequest ns) = object ["bulkpush_req" .= (run <$> ns)] +instance S.ToSchema BulkPushRequest where + schema = + S.object "BulkPushRequest" $ + BulkPushRequest + <$> fromBulkPushRequest S..= S.field "bulkpush_req" (S.array bulkpushReqItemSchema) where - run (n, ps) = object ["notification" .= n, "targets" .= ps] + bulkpushReqItemSchema :: ValueSchema S.NamedSwaggerDoc (Notification, [PushTarget]) + bulkpushReqItemSchema = + S.object "(Notification, [PushTarget])" $ + (,) + <$> fst S..= S.field "notification" S.schema + <*> snd S..= S.field "targets" (S.array S.schema) data PushStatus = PushStatusOk | PushStatusGone deriving (Eq, Show, Bounded, Enum, Generic) + deriving (FromJSON, ToJSON, Swagger.ToSchema) via S.Schema PushStatus -$(deriveJSON (defaultOptions {constructorTagModifier = camelTo2 '_'}) ''PushStatus) +instance S.ToSchema PushStatus where + schema = + S.enum @Text "PushStatus" $ + mconcat + [ S.element "push_status_ok" PushStatusOk, + S.element "push_status_gone" PushStatusGone + ] newtype BulkPushResponse = BulkPushResponse { fromBulkPushResponse :: [(NotificationId, PushTarget, PushStatus)] @@ -77,15 +88,18 @@ newtype BulkPushResponse = BulkPushResponse Show, Generic ) + deriving (FromJSON, ToJSON, Swagger.ToSchema) via S.Schema BulkPushResponse -instance FromJSON BulkPushResponse where - parseJSON = withObject "bulkpush response body" $ \hm -> - BulkPushResponse <$> (mapM run =<< (hm .: "bulkpush_resp")) - where - run = withObject "object with notifId, target, status" $ \hm -> - (,,) <$> (hm .: "notif_id") <*> (hm .: "target") <*> (hm .: "status") - -instance ToJSON BulkPushResponse where - toJSON (BulkPushResponse ns) = object ["bulkpush_resp" .= (run <$> ns)] +instance S.ToSchema BulkPushResponse where + schema = + S.object "BulkPushResponse" $ + BulkPushResponse + <$> fromBulkPushResponse S..= S.field "bulkpush_resp" (S.array bulkPushResponseSchema) where - run (n, p, s) = object ["notif_id" .= n, "target" .= p, "status" .= s] + bulkPushResponseSchema :: ValueSchema S.NamedSwaggerDoc (NotificationId, PushTarget, PushStatus) + bulkPushResponseSchema = + S.object "(NotificationId, PushTarget, PushStatus)" $ + (,,) + <$> view _1 S..= S.field "notif_id" S.schema + <*> view _2 S..= S.field "target" S.schema + <*> view _3 S..= S.field "status" S.schema diff --git a/libs/wire-api/src/Wire/API/Internal/Notification.hs b/libs/wire-api/src/Wire/API/Internal/Notification.hs index 5bdfaa6e82..a15aee4fa9 100644 --- a/libs/wire-api/src/Wire/API/Internal/Notification.hs +++ b/libs/wire-api/src/Wire/API/Internal/Notification.hs @@ -45,6 +45,8 @@ import Control.Lens (makeLenses) import Data.Aeson import Data.Id import Data.List1 +import qualified Data.Schema as S +import qualified Data.Swagger as Swagger import Imports import Wire.API.Notification @@ -57,21 +59,15 @@ data Notification = Notification ntfPayload :: !(List1 Object) } deriving (Eq, Show) + deriving (FromJSON, ToJSON, Swagger.ToSchema) via S.Schema Notification -instance FromJSON Notification where - parseJSON = withObject "notification" $ \o -> - Notification - <$> o .: "id" - <*> o .:? "transient" .!= False - <*> o .: "payload" - -instance ToJSON Notification where - toJSON (Notification i t p) = - object - [ "id" .= i, - "transient" .= t, - "payload" .= p - ] +instance S.ToSchema Notification where + schema = + S.object "Notification" $ + Notification + <$> ntfId S..= S.field "id" S.schema + <*> ntfTransient S..= (fromMaybe False <$> S.optField "transient" S.schema) + <*> (toNonEmpty . ntfPayload) S..= fmap List1 (S.field "payload" (S.nonEmptyArray S.jsonObject)) -------------------------------------------------------------------------------- -- NotificationTarget diff --git a/libs/wire-api/src/Wire/API/MLS/Extension.hs b/libs/wire-api/src/Wire/API/MLS/Extension.hs index 406adfa7e8..5093398adf 100644 --- a/libs/wire-api/src/Wire/API/MLS/Extension.hs +++ b/libs/wire-api/src/Wire/API/MLS/Extension.hs @@ -44,6 +44,7 @@ module Wire.API.MLS.Extension where import Data.Binary +import Data.Kind import Data.Singletons.TH import Data.Time.Clock.POSIX import Imports @@ -86,7 +87,7 @@ data ExtensionTag $(genSingletons [''ExtensionTag]) -type family ExtensionType (t :: ExtensionTag) :: * where +type family ExtensionType (t :: ExtensionTag) :: Type where ExtensionType 'CapabilitiesExtensionTag = Capabilities ExtensionType 'LifetimeExtensionTag = Lifetime diff --git a/libs/wire-api/src/Wire/API/MLS/Message.hs b/libs/wire-api/src/Wire/API/MLS/Message.hs index 2393aa06e2..c70f736bfb 100644 --- a/libs/wire-api/src/Wire/API/MLS/Message.hs +++ b/libs/wire-api/src/Wire/API/MLS/Message.hs @@ -49,8 +49,8 @@ import Data.Binary import Data.Binary.Get import Data.Binary.Put import qualified Data.ByteArray as BA --- import qualified Data.ByteString as BS import Data.Json.Util +import Data.Kind import Data.Schema import Data.Singletons.TH import qualified Data.Swagger as S @@ -74,7 +74,7 @@ $(genSingletons [''WireFormatTag]) instance ParseMLS WireFormatTag where parseMLS = parseMLSEnum @Word8 "wire format" -data family MessageExtraFields (tag :: WireFormatTag) :: * +data family MessageExtraFields (tag :: WireFormatTag) :: Type data instance MessageExtraFields 'MLSPlainText = MessageExtraFields { msgSignature :: ByteString, @@ -226,7 +226,7 @@ instance ParseMLS SomeMessage where MLSPlainText -> SomeMessage SMLSPlainText <$> parseMLS MLSCipherText -> SomeMessage SMLSCipherText <$> parseMLS -data family Sender (tag :: WireFormatTag) :: * +data family Sender (tag :: WireFormatTag) :: Type data instance Sender 'MLSCipherText = EncryptedSender {esData :: ByteString} deriving (Eq, Show) @@ -268,7 +268,7 @@ instance SerialiseMLS (Sender 'MLSPlainText) where put x serialiseMLS NewMemberSender = serialiseMLS NewMemberSenderTag -data family MessagePayload (tag :: WireFormatTag) :: * +data family MessagePayload (tag :: WireFormatTag) :: Type deriving instance Eq (MessagePayload 'MLSPlainText) diff --git a/libs/wire-api/src/Wire/API/MLS/Serialisation.hs b/libs/wire-api/src/Wire/API/MLS/Serialisation.hs index 3e99b33cfb..0881c31773 100644 --- a/libs/wire-api/src/Wire/API/MLS/Serialisation.hs +++ b/libs/wire-api/src/Wire/API/MLS/Serialisation.hs @@ -58,6 +58,7 @@ import Data.Binary.Put import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as LBS import Data.Json.Util +import Data.Kind import Data.Proxy import Data.Schema import qualified Data.Swagger as S @@ -125,7 +126,7 @@ serialiseMLSOptional p (Just x) = do -- corresponding enumeration index. This makes it possible to parse enumeration -- types that don't contain an explicit constructor for a "reserved" value. parseMLSEnum :: - forall (w :: *) a. + forall (w :: Type) a. (Bounded a, Enum a, Integral w, Binary w) => String -> Get a diff --git a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs index e867b555e0..56cf5c57c3 100644 --- a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs +++ b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs @@ -1,5 +1,6 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE TemplateHaskell #-} +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- diff --git a/libs/wire-api/src/Wire/API/MakesFederatedCall.hs b/libs/wire-api/src/Wire/API/MakesFederatedCall.hs index a6abb32dc0..0e0f627db4 100644 --- a/libs/wire-api/src/Wire/API/MakesFederatedCall.hs +++ b/libs/wire-api/src/Wire/API/MakesFederatedCall.hs @@ -15,6 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . {-# LANGUAGE OverloadedLists #-} +{-# OPTIONS_GHC -fno-warn-redundant-constraints #-} module Wire.API.MakesFederatedCall ( CallsFed, @@ -22,11 +23,17 @@ module Wire.API.MakesFederatedCall Component (..), callsFed, unsafeCallsFed, + AddAnnotation, + Location (..), + ShowComponent, + Annotation, + exposeAnnotations, ) where import Data.Aeson (Value (..)) import Data.Constraint +import Data.Kind import Data.Metrics.Servant import Data.Proxy import Data.Swagger.Operation (addExtensions) @@ -38,9 +45,33 @@ import Servant.Client import Servant.Server import Servant.Swagger import Test.QuickCheck (Arbitrary) +import TransitiveAnns.Types import Unsafe.Coerce (unsafeCoerce) import Wire.Arbitrary (GenericUniform (..)) +-- | This function exists only to provide a convenient place for the +-- @transitive-anns@ plugin to solve the 'ToHasAnnotations' constraint. This is +-- highly magical and warrants a note. +-- +-- The call @'exposeAnnotations' (some expr here)@ will expand to @some expr +-- here@, additionally generating wanted 'HasAnnotation' constraints for every +-- 'AddAnnotation' constraint in the _transitive call closure_ of @some expr +-- here@. +-- +-- The use case is always going to be @'callsFed' ('exposeAnnotations' expr)@, +-- where 'exposeAnnotations' re-introduces all of the constraints we've been +-- squirreling away, and 'callsFed' is responsible for discharging them. It +-- would be very desirable to combine these into one call, but the semantics of +-- solving 'ToHasAnnotations' attaches the wanted calls to the same place as +-- the call itself, which means the wanteds appear just after our opportunity +-- to solve them via 'callsFed'. This is likely not a hard limitation. +-- +-- The @x@ parameter here is intentionally ambiguous, existing as a unique +-- skolem to prevent GHC from caching the results of solving +-- 'ToHasAnnotations'. Callers needn't worry about it. +exposeAnnotations :: ToHasAnnotations x => a -> a +exposeAnnotations = id + data Component = Brig | Galley @@ -55,7 +86,7 @@ data Component -- The only way to discharge this constraint is via 'callsFed', which should be -- invoked for each federated call when connecting handlers to the server -- definition. -class CallsFed (comp :: Component) (name :: Symbol) +type CallsFed (comp :: Component) = HasAnnotation 'Remote (ShowComponent comp) -- | A typeclass with the same layout as 'CallsFed', which exists only so we -- can discharge 'CallsFeds' constraints by unsafely coercing this one. @@ -72,7 +103,7 @@ synthesizeCallsFed = unsafeCoerce $ Dict @Nullary -- constraints on handlers. data MakesFederatedCall (comp :: Component) (name :: Symbol) -instance (HasServer api ctx) => HasServer (MakesFederatedCall comp name :> api :: *) ctx where +instance (HasServer api ctx) => HasServer (MakesFederatedCall comp name :> api :: Type) ctx where -- \| This should have type @CallsFed comp name => ServerT api m@, but GHC -- complains loudly thinking this is a polytype. We need to introduce the -- 'CallsFed' constraint so that we can eliminate it via @@ -82,22 +113,22 @@ instance (HasServer api ctx) => HasServer (MakesFederatedCall comp name :> api : route _ ctx f = route (Proxy @api) ctx $ fmap ($ synthesizeCallsFed @comp @name) f hoistServerWithContext _ ctx f s = hoistServerWithContext (Proxy @api) ctx f . s -instance HasLink api => HasLink (MakesFederatedCall comp name :> api :: *) where +instance HasLink api => HasLink (MakesFederatedCall comp name :> api :: Type) where type MkLink (MakesFederatedCall comp name :> api) x = MkLink api x toLink f _ l = toLink f (Proxy @api) l -instance RoutesToPaths api => RoutesToPaths (MakesFederatedCall comp name :> api :: *) where +instance RoutesToPaths api => RoutesToPaths (MakesFederatedCall comp name :> api :: Type) where getRoutes = getRoutes @api -- | Get a symbol representation of our component. -type family ShowComponent (x :: Component) :: Symbol where +type family ShowComponent (x :: Component) = (res :: Symbol) | res -> x where ShowComponent 'Brig = "brig" ShowComponent 'Galley = "galley" ShowComponent 'Cargohold = "cargohold" -- | 'MakesFederatedCall' annotates the swagger documentation with an extension -- tag @x-wire-makes-federated-calls-to@. -instance (HasSwagger api, KnownSymbol name, KnownSymbol (ShowComponent comp)) => HasSwagger (MakesFederatedCall comp name :> api :: *) where +instance (HasSwagger api, KnownSymbol name, KnownSymbol (ShowComponent comp)) => HasSwagger (MakesFederatedCall comp name :> api :: Type) where toSwagger _ = toSwagger (Proxy @api) & addExtensions @@ -116,7 +147,7 @@ mergeJSONArray :: Value -> Value -> Value mergeJSONArray (Array x) (Array y) = Array $ x <> y mergeJSONArray _ _ = error "impossible! bug in construction of federated calls JSON" -instance HasClient m api => HasClient m (MakesFederatedCall comp name :> api :: *) where +instance HasClient m api => HasClient m (MakesFederatedCall comp name :> api :: Type) where type Client m (MakesFederatedCall comp name :> api) = Client m api clientWithRoute p _ = clientWithRoute p $ Proxy @api hoistClientMonad p _ f c = hoistClientMonad p (Proxy @api) f c @@ -126,6 +157,10 @@ instance HasClient m api => HasClient m (MakesFederatedCall comp name :> api :: class SolveCallsFed c r a where -- | Safely discharge a 'CallsFed' constraint. Intended to be used when -- connecting your handler to the server router. + -- + -- This function should always be called with an argument of + -- 'exposeAnnotations'. See the documentation there for more information on + -- why. callsFed :: (c => r) -> a instance (c ~ ((k, d) :: Constraint), SolveCallsFed d r a) => SolveCallsFed c r (Dict k -> a) where diff --git a/libs/wire-api/src/Wire/API/Message.hs b/libs/wire-api/src/Wire/API/Message.hs index e0dc107f81..204c99b519 100644 --- a/libs/wire-api/src/Wire/API/Message.hs +++ b/libs/wire-api/src/Wire/API/Message.hs @@ -57,12 +57,6 @@ module Wire.API.Message UserClients (..), ReportMissing (..), IgnoreMissing (..), - - -- * Swagger - modelNewOtrMessage, - modelOtrRecipients, - modelClientMismatch, - typePriority, ) where @@ -82,7 +76,6 @@ import Data.Schema import Data.Serialize (runGet) import qualified Data.Set as Set import qualified Data.Swagger as S -import qualified Data.Swagger.Build.Api as Doc import qualified Data.Text.Read as Reader import qualified Data.UUID as UUID import Imports @@ -91,7 +84,7 @@ import qualified Proto.Otr_Fields as Proto.Otr import Servant (FromHttpApiData (..)) import qualified Wire.API.Message.Proto as Proto import Wire.API.ServantProto (FromProto (..), ToProto (..)) -import Wire.API.User.Client (QualifiedUserClientMap (QualifiedUserClientMap), QualifiedUserClients, UserClientMap (..), UserClients (..), modelOtrClientMap, modelUserClients) +import Wire.API.User.Client (QualifiedUserClientMap (QualifiedUserClientMap), QualifiedUserClients, UserClientMap (..), UserClients (..)) import Wire.Arbitrary (Arbitrary (..), GenericUniform (..)) -------------------------------------------------------------------------------- @@ -152,31 +145,6 @@ newOtrMessageMetadata msg = (newOtrNativePriority msg) (newOtrData msg) -modelNewOtrMessage :: Doc.Model -modelNewOtrMessage = Doc.defineModel "NewOtrMessage" $ do - Doc.description "OTR message per recipient" - Doc.property "sender" Doc.bytes' $ - Doc.description "The sender's client ID" - Doc.property "recipients" (Doc.ref modelOtrRecipients) $ - Doc.description "Per-recipient data (i.e. ciphertext)." - Doc.property "native_push" Doc.bool' $ do - Doc.description "Whether to issue a native push to offline clients." - Doc.optional - Doc.property "transient" Doc.bool' $ do - Doc.description "Whether to put this message into the notification queue." - Doc.optional - Doc.property "native_priority" typePriority $ do - Doc.description "The native push priority (default 'high')." - Doc.optional - Doc.property "data" Doc.bytes' $ do - Doc.description - "Extra (symmetric) data (i.e. ciphertext) that is replicated \ - \for each recipient." - Doc.optional - Doc.property "report_missing" (Doc.unique $ Doc.array Doc.bytes') $ do - Doc.description "List of user IDs" - Doc.optional - instance ToSchema NewOtrMessage where schema = object "new-otr-message" $ @@ -329,14 +297,6 @@ data Priority = LowPriority | HighPriority deriving (Arbitrary) via (GenericUniform Priority) deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema Priority -typePriority :: Doc.DataType -typePriority = - Doc.string $ - Doc.enum - [ "low", - "high" - ] - instance ToSchema Priority where schema = enum @Text "Priority" $ @@ -368,13 +328,6 @@ newtype OtrRecipients = OtrRecipients deriving stock (Eq, Show) deriving newtype (ToSchema, A.ToJSON, A.FromJSON, Semigroup, Monoid, Arbitrary) --- FUTUREWORK: Remove when 'NewOtrMessage' has ToSchema -modelOtrRecipients :: Doc.Model -modelOtrRecipients = Doc.defineModel "OtrRecipients" $ do - Doc.description "Recipients of OTR content." - Doc.property "" (Doc.ref modelOtrClientMap) $ - Doc.description "Mapping of user IDs to 'OtrClientMap's." - protoToOtrRecipients :: [Proto.UserEntry] -> OtrRecipients protoToOtrRecipients = OtrRecipients @@ -549,18 +502,6 @@ instance Arbitrary ClientMismatch where <*> arbitrary <*> arbitrary -modelClientMismatch :: Doc.Model -modelClientMismatch = Doc.defineModel "ClientMismatch" $ do - Doc.description "Map of missing, redundant or deleted clients." - Doc.property "time" Doc.dateTime' $ - Doc.description "Server timestamp (date and time)" - Doc.property "missing" (Doc.ref modelUserClients) $ - Doc.description "Map of missing clients per user." - Doc.property "redundant" (Doc.ref modelUserClients) $ - Doc.description "Map of redundant clients per user." - Doc.property "deleted" (Doc.ref modelUserClients) $ - Doc.description "Map of deleted clients per user." - instance ToSchema ClientMismatch where schema = object "ClientMismatch" $ diff --git a/libs/wire-api/src/Wire/API/Notification.hs b/libs/wire-api/src/Wire/API/Notification.hs index 954cc65d78..457947e547 100644 --- a/libs/wire-api/src/Wire/API/Notification.hs +++ b/libs/wire-api/src/Wire/API/Notification.hs @@ -34,17 +34,14 @@ module Wire.API.Notification queuedHasMore, queuedTime, GetNotificationsResponse (..), - - -- * Swagger - modelEvent, - modelNotification, - modelNotificationList, ) where -import Control.Lens (makeLenses) +import Control.Lens (makeLenses, (.~)) +import Control.Lens.Operators ((?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson.Types as Aeson +import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap import Data.Id import Data.Json.Util import Data.List.NonEmpty (NonEmpty) @@ -53,7 +50,6 @@ import Data.Schema import Data.String.Conversions (cs) import Data.Swagger (ToParamSchema (..)) import qualified Data.Swagger as S -import qualified Data.Swagger.Build.Api as Doc import Data.Time.Clock (UTCTime) import Imports import Servant @@ -67,11 +63,23 @@ type NotificationId = Id QueuedNotification -- (e.g. visible in 'modelEvent'). Can we specify it in a better way? type Event = Aeson.Object -modelEvent :: Doc.Model -modelEvent = Doc.defineModel "NotificationEvent" $ do - Doc.description "A single event" - Doc.property "type" Doc.string' $ - Doc.description "Event type" +-- | Schema for an `Event` object. +-- +-- This is basically a schema for a JSON object with some pre-defined structure. +eventSchema :: ValueSchema NamedSwaggerDoc Event +eventSchema = mkSchema sdoc Aeson.parseJSON (Just . Aeson.toJSON) + where + sdoc :: NamedSwaggerDoc + sdoc = + swaggerDoc @Aeson.Object + & S.schema . S.title ?~ "Event" + & S.schema . S.description ?~ "A single notification event" + & S.schema . S.properties + .~ InsOrdHashMap.fromList + [ ( "type", + S.Inline (S.toSchema (Proxy @Text) & S.description ?~ "Event type") + ) + ] -------------------------------------------------------------------------------- -- QueuedNotification @@ -89,23 +97,18 @@ queuedNotification = QueuedNotification instance ToSchema QueuedNotification where schema = - object "QueuedNotification" $ + objectWithDocModifier "QueuedNotification" queuedNotificationDoc $ QueuedNotification <$> _queuedNotificationId .= field "id" schema <*> _queuedNotificationPayload - .= field "payload" (nonEmptyArray jsonObject) + .= fieldWithDocModifier "payload" payloadDoc (nonEmptyArray eventSchema) + where + queuedNotificationDoc = description ?~ "A single notification" + payloadDoc d = d & description ?~ "List of events" makeLenses ''QueuedNotification -modelNotification :: Doc.Model -modelNotification = Doc.defineModel "Notification" $ do - Doc.description "A single notification" - Doc.property "id" Doc.bytes' $ - Doc.description "Notification ID" - Doc.property "payload" (Doc.array (Doc.ref modelEvent)) $ - Doc.description "List of events" - data QueuedNotificationList = QueuedNotificationList { _queuedNotifications :: [QueuedNotification], _queuedHasMore :: Bool, @@ -118,24 +121,20 @@ data QueuedNotificationList = QueuedNotificationList queuedNotificationList :: [QueuedNotification] -> Bool -> Maybe UTCTime -> QueuedNotificationList queuedNotificationList = QueuedNotificationList -modelNotificationList :: Doc.Model -modelNotificationList = Doc.defineModel "NotificationList" $ do - Doc.description "Zero or more notifications" - Doc.property "notifications" (Doc.array (Doc.ref modelNotification)) $ - Doc.description "Notifications" - Doc.property "has_more" Doc.bool' $ - Doc.description "Whether there are still more notifications." - instance ToSchema QueuedNotificationList where schema = - object "QueuedNotificationList" $ + objectWithDocModifier "QueuedNotificationList" queuedNotificationListDoc $ QueuedNotificationList <$> _queuedNotifications - .= field "notifications" (array schema) + .= fieldWithDocModifier "notifications" notificationsDoc (array schema) <*> _queuedHasMore - .= fmap (fromMaybe False) (optField "has_more" schema) + .= fmap (fromMaybe False) (optFieldWithDocModifier "has_more" hasMoreDoc schema) <*> _queuedTime .= maybe_ (optField "time" utcTimeSchema) + where + queuedNotificationListDoc = description ?~ "Zero or more notifications" + notificationsDoc = description ?~ "Notifications" + hasMoreDoc = description ?~ "Whether there are still more notifications." makeLenses ''QueuedNotificationList diff --git a/libs/wire-api/src/Wire/API/Properties.hs b/libs/wire-api/src/Wire/API/Properties.hs index 83a04aa07c..a2bee4aab8 100644 --- a/libs/wire-api/src/Wire/API/Properties.hs +++ b/libs/wire-api/src/Wire/API/Properties.hs @@ -22,10 +22,6 @@ module Wire.API.Properties PropertyKey (..), RawPropertyValue (..), PropertyValue (..), - - -- * Swagger - modelPropertyValue, - modelPropertyDictionary, ) where @@ -35,7 +31,6 @@ import qualified Data.Aeson as A import Data.ByteString.Conversion import Data.Hashable (Hashable) import qualified Data.Swagger as S -import qualified Data.Swagger.Build.Api as Doc import Data.Text.Ascii import Imports import Servant @@ -50,11 +45,6 @@ instance S.ToSchema PropertyKeysAndValues where S.NamedSchema (Just "PropertyKeysAndValues") $ mempty & S.type_ ?~ S.SwaggerObject -modelPropertyDictionary :: Doc.Model -modelPropertyDictionary = - Doc.defineModel "PropertyDictionary" $ - Doc.description "A JSON object with properties as attribute/value pairs." - newtype PropertyKey = PropertyKey {propertyKeyName :: AsciiPrintable} deriving stock (Eq, Ord, Show, Generic) @@ -102,8 +92,3 @@ instance ToJSON PropertyValue where instance Show PropertyValue where show = show . propertyValue - -modelPropertyValue :: Doc.Model -modelPropertyValue = - Doc.defineModel "PropertyValue" $ - Doc.description "A property value is any valid JSON value." diff --git a/libs/wire-api/src/Wire/API/Provider/Service.hs b/libs/wire-api/src/Wire/API/Provider/Service.hs index 329c1e0ad6..484c5fe6c6 100644 --- a/libs/wire-api/src/Wire/API/Provider/Service.hs +++ b/libs/wire-api/src/Wire/API/Provider/Service.hs @@ -47,9 +47,6 @@ module Wire.API.Provider.Service -- * UpdateServiceWhitelist UpdateServiceWhitelist (..), - - -- * Swagger - modelServiceRef, ) where @@ -69,7 +66,6 @@ import Data.Proxy import Data.Range (Range) import Data.Schema import qualified Data.Swagger as S -import qualified Data.Swagger.Build.Api as Doc import qualified Data.Text as Text import Data.Text.Ascii import qualified Data.Text.Encoding as Text @@ -102,14 +98,6 @@ makeLenses ''ServiceRef newServiceRef :: ServiceId -> ProviderId -> ServiceRef newServiceRef = ServiceRef -modelServiceRef :: Doc.Model -modelServiceRef = Doc.defineModel "ServiceRef" $ do - Doc.description "Service Reference" - Doc.property "id" Doc.bytes' $ - Doc.description "Service ID" - Doc.property "provider" Doc.bytes' $ - Doc.description "Provider ID" - -------------------------------------------------------------------------------- -- ServiceKey diff --git a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs index 60be42a427..555c5fa31a 100644 --- a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs +++ b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs @@ -44,10 +44,11 @@ import qualified Data.Aeson as JSON import qualified Data.ByteString.Builder as BB import qualified Data.ByteString.Char8 as C8 import Data.ByteString.Conversion -import Data.Range (LTE, Range, fromRange) +import Data.Range (Range, fromRange) import qualified Data.Range as Range import qualified Data.Set as Set import qualified Data.Text.Encoding as Text +import Data.Type.Ord import GHC.TypeLits (KnownNat, Nat) import Imports import Wire.Arbitrary (Arbitrary (..), GenericUniform (..)) @@ -180,10 +181,10 @@ newtype QueryAnyTags (m :: Nat) (n :: Nat) = QueryAnyTags {queryAnyTagsRange :: Range m n (Set (QueryAllTags m n))} deriving stock (Eq, Show, Ord) -instance (KnownNat m, KnownNat n, LTE m n) => Arbitrary (QueryAnyTags m n) where +instance (KnownNat m, KnownNat n, m <= n) => Arbitrary (QueryAnyTags m n) where arbitrary = QueryAnyTags <$> arbitrary -queryAnyTags :: LTE m n => MatchAny -> Maybe (QueryAnyTags m n) +queryAnyTags :: (KnownNat m, KnownNat n, m <= n) => MatchAny -> Maybe (QueryAnyTags m n) queryAnyTags t = do x <- mapM queryAllTags (Set.toList (matchAnySet t)) QueryAnyTags <$> Range.checked (Set.fromList x) @@ -199,7 +200,7 @@ instance ToByteString (QueryAnyTags m n) where . queryAnyTagsRange -- | QueryAny ::= QueryAll { "," QueryAll } -instance LTE m n => FromByteString (QueryAnyTags m n) where +instance (KnownNat n, KnownNat m, m <= n) => FromByteString (QueryAnyTags m n) where parser = do bs <- C8.split ',' <$> parser ts <- mapM (either fail pure . runParser parser) bs @@ -211,10 +212,10 @@ newtype QueryAllTags (m :: Nat) (n :: Nat) = QueryAllTags {queryAllTagsRange :: Range m n (Set ServiceTag)} deriving stock (Eq, Show, Ord) -instance (KnownNat m, KnownNat n, LTE m n) => Arbitrary (QueryAllTags m n) where +instance (KnownNat m, KnownNat n, m <= n) => Arbitrary (QueryAllTags m n) where arbitrary = QueryAllTags <$> arbitrary -queryAllTags :: LTE m n => MatchAll -> Maybe (QueryAllTags m n) +queryAllTags :: (KnownNat m, KnownNat n, m <= n) => MatchAll -> Maybe (QueryAllTags m n) queryAllTags = fmap QueryAllTags . Range.checked . matchAllSet -- | QueryAll ::= tag { "." tag } @@ -228,7 +229,7 @@ instance ToByteString (QueryAllTags m n) where . queryAllTagsRange -- | QueryAll ::= tag { "." tag } -instance LTE m n => FromByteString (QueryAllTags m n) where +instance (KnownNat m, KnownNat n, m <= n) => FromByteString (QueryAllTags m n) where parser = do bs <- C8.split '.' <$> parser ts <- mapM (either fail pure . runParser parser) bs diff --git a/libs/wire-api/src/Wire/API/RawJson.hs b/libs/wire-api/src/Wire/API/RawJson.hs index 295202c1ed..4039108673 100644 --- a/libs/wire-api/src/Wire/API/RawJson.hs +++ b/libs/wire-api/src/Wire/API/RawJson.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -17,13 +19,27 @@ module Wire.API.RawJson where +import Control.Lens +import qualified Data.Swagger as Swagger import Imports import Servant +import Test.QuickCheck +import Test.QuickCheck.Instances () -- | Wrap json content as plain 'LByteString' --- This type is intented to be used to receive json content as 'LByteString'. +-- This type is intented to be used to receive json content as 'LText'. -- Warning: There is no validation of the json content. It may be any string. newtype RawJson = RawJson {rawJsonBytes :: LByteString} + deriving (Eq, Show) + deriving newtype (Arbitrary) instance {-# OVERLAPPING #-} MimeUnrender JSON RawJson where mimeUnrender _ = pure . RawJson + +instance Swagger.ToSchema RawJson where + declareNamedSchema _ = + pure . Swagger.NamedSchema (Just "RawJson") $ + mempty + & Swagger.type_ ?~ Swagger.SwaggerObject + & Swagger.description + ?~ "Any JSON as plain string. The object structure is not specified in this schema." diff --git a/libs/wire-api/src/Wire/API/Routes/ClientAlgebra.hs b/libs/wire-api/src/Wire/API/Routes/ClientAlgebra.hs index e3ee1d1897..abc2b28e28 100644 --- a/libs/wire-api/src/Wire/API/Routes/ClientAlgebra.hs +++ b/libs/wire-api/src/Wire/API/Routes/ClientAlgebra.hs @@ -1,3 +1,6 @@ +-- Disabling for `(Monad m, AllMime cs, HasClient m (MultiVerb method cs as r)) =>` +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH diff --git a/libs/wire-api/src/Wire/API/Routes/Cookies.hs b/libs/wire-api/src/Wire/API/Routes/Cookies.hs index af51f950b6..3feadafb10 100644 --- a/libs/wire-api/src/Wire/API/Routes/Cookies.hs +++ b/libs/wire-api/src/Wire/API/Routes/Cookies.hs @@ -17,6 +17,7 @@ module Wire.API.Routes.Cookies where +import Data.Kind import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.Map as M import Data.Metrics.Servant @@ -42,12 +43,12 @@ data (:::) a b -- @@ -- results in a cookie with name "foo" containing a 64-bit integer, and a -- cookie with name "bar" containing an arbitrary text value. -data Cookies (cs :: [*]) +data Cookies (cs :: [Type]) type CookieHeader cs = Header "Cookie" (CookieTuple cs) -- CookieTypes = map snd -type family CookieTypes (cs :: [*]) :: [*] +type family CookieTypes (cs :: [Type]) :: [Type] type instance CookieTypes '[] = '[] @@ -60,9 +61,9 @@ type CookieMap = Map ByteString (NonEmpty ByteString) instance HasSwagger api => HasSwagger (Cookies cs :> api) where toSwagger _ = toSwagger (Proxy @api) -class CookieArgs (cs :: [*]) where +class CookieArgs (cs :: [Type]) where -- example: AddArgs ["foo" :: Foo, "bar" :: Bar] a = Foo -> Bar -> a - type AddArgs cs a :: * + type AddArgs cs a :: Type uncurryArgs :: AddArgs cs a -> CookieTuple cs -> a mapArgs :: (a -> b) -> AddArgs cs a -> AddArgs cs b @@ -81,7 +82,7 @@ instance KnownSymbol label, FromHttpApiData x ) => - CookieArgs ((label ::: (x :: *)) ': cs) + CookieArgs ((label ::: (x :: Type)) ': cs) where type AddArgs ((label ::: x) ': cs) a = [Either Text x] -> AddArgs cs a uncurryArgs f (CookieTuple (I x :* xs)) = uncurryArgs @cs (f x) (CookieTuple xs) diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs index c42b16e029..0743610bb6 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -27,7 +27,6 @@ module Wire.API.Routes.Internal.Brig GetAccountConferenceCallingConfig, PutAccountConferenceCallingConfig, DeleteAccountConferenceCallingConfig, - SwaggerDocsAPI, swaggerDoc, module Wire.API.Routes.Internal.Brig.EJPD, NewKeyPackageRef (..), @@ -48,7 +47,6 @@ import Imports hiding (head) import Servant hiding (Handler, WithStatus, addHeader, respond) import Servant.Swagger (HasSwagger (toSwagger)) import Servant.Swagger.Internal.Orphans () -import Servant.Swagger.UI import Wire.API.Connection import Wire.API.Error import Wire.API.Error.Brig @@ -397,9 +395,7 @@ type AuthAPI = :> MultiVerb1 'GET '[JSON] (RespondEmpty 200 "OK") ) -type SwaggerDocsAPI = "api" :> "internal" :> SwaggerSchemaUI "swagger-ui" "swagger.json" - swaggerDoc :: Swagger swaggerDoc = toSwagger (Proxy @API) - & info . title .~ "Wire-Server API as Swagger 2.0 (internal end-points; incomplete) " + & info . title .~ "Wire-Server internal brig API" diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Cannon.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Cannon.hs index 672651ab07..ff0fe916a1 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Cannon.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Cannon.hs @@ -1,8 +1,11 @@ module Wire.API.Routes.Internal.Cannon where +import Control.Lens ((.~)) import Data.Id +import Data.Swagger (HasInfo (info), HasTitle (title), Swagger) import Imports import Servant +import Servant.Swagger (HasSwagger (toSwagger)) import Wire.API.Error import Wire.API.Error.Cannon import Wire.API.Internal.BulkPush @@ -55,3 +58,8 @@ type API = (Maybe ()) ) ) + +swaggerDoc :: Swagger +swaggerDoc = + toSwagger (Proxy @API) + & info . title .~ "Wire-Server internal cannon API" diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs index 5900b31d96..825623ac9c 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs @@ -17,8 +17,19 @@ module Wire.API.Routes.Internal.Cargohold where +import Control.Lens +import Data.Swagger +import Imports import Servant +import Servant.Swagger import Wire.API.Routes.MultiVerb type InternalAPI = - "i" :> "status" :> MultiVerb 'GET '() '[RespondEmpty 200 "OK"] () + "i" + :> "status" + :> MultiVerb 'GET '() '[RespondEmpty 200 "OK"] () + +swaggerDoc :: Swagger +swaggerDoc = + toSwagger (Proxy @InternalAPI) + & info . title .~ "Wire-Server internal cargohold API" diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs new file mode 100644 index 0000000000..f7e533d53b --- /dev/null +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -0,0 +1,416 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 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.Internal.Galley where + +import Control.Lens ((.~)) +import Data.Id as Id +import Data.Range +import Data.Swagger (Swagger, info, title) +import GHC.TypeLits (AppendSymbol) +import Imports hiding (head) +import Servant hiding (JSON, WithStatus) +import qualified Servant hiding (WithStatus) +import Servant.Swagger +import Wire.API.ApplyMods +import Wire.API.Conversation.Role +import Wire.API.Error +import Wire.API.Error.Galley +import Wire.API.Event.Conversation +import Wire.API.MakesFederatedCall +import Wire.API.Routes.Internal.Galley.ConversationsIntra +import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti +import Wire.API.Routes.Internal.Galley.TeamsIntra +import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Named +import Wire.API.Routes.Public +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 +import Wire.API.Team.SearchVisibility + +type LegalHoldFeatureStatusChangeErrors = + '( 'ActionDenied 'RemoveConversationMember, + '( AuthenticationError, + '( 'CannotEnableLegalHoldServiceLargeTeam, + '( 'LegalHoldNotEnabled, + '( 'LegalHoldDisableUnimplemented, + '( 'LegalHoldServiceNotRegistered, + '( 'UserLegalHoldIllegalOperation, + '( 'LegalHoldCouldNotBlockConnections, '()) + ) + ) + ) + ) + ) + ) + ) + +type LegalHoldFeaturesStatusChangeFederatedCalls = + '[ MakesFederatedCall 'Galley "on-conversation-updated", + MakesFederatedCall 'Galley "on-mls-message-sent", + MakesFederatedCall 'Galley "on-new-remote-conversation" + ] + +type IFeatureAPI = + -- SSOConfig + IFeatureStatusGet SSOConfig + :<|> IFeatureStatusPut '[] '() SSOConfig + :<|> IFeatureStatusPatch '[] '() SSOConfig + -- LegalholdConfig + :<|> IFeatureStatusGet LegalholdConfig + :<|> IFeatureStatusPut + LegalHoldFeaturesStatusChangeFederatedCalls + LegalHoldFeatureStatusChangeErrors + LegalholdConfig + :<|> IFeatureStatusPatch + LegalHoldFeaturesStatusChangeFederatedCalls + LegalHoldFeatureStatusChangeErrors + LegalholdConfig + -- SearchVisibilityAvailableConfig + :<|> IFeatureStatusGet SearchVisibilityAvailableConfig + :<|> IFeatureStatusPut '[] '() SearchVisibilityAvailableConfig + :<|> IFeatureStatusPatch '[] '() SearchVisibilityAvailableConfig + -- ValidateSAMLEmailsConfig + :<|> IFeatureStatusGet ValidateSAMLEmailsConfig + :<|> IFeatureStatusPut '[] '() ValidateSAMLEmailsConfig + :<|> IFeatureStatusPatch '[] '() ValidateSAMLEmailsConfig + -- DigitalSignaturesConfig + :<|> IFeatureStatusGet DigitalSignaturesConfig + :<|> IFeatureStatusPut '[] '() DigitalSignaturesConfig + :<|> IFeatureStatusPatch '[] '() DigitalSignaturesConfig + -- AppLockConfig + :<|> IFeatureStatusGet AppLockConfig + :<|> IFeatureStatusPut '[] '() AppLockConfig + :<|> IFeatureStatusPatch '[] '() AppLockConfig + -- FileSharingConfig + :<|> IFeatureStatusGet FileSharingConfig + :<|> IFeatureStatusPut '[] '() FileSharingConfig + :<|> IFeatureStatusLockStatusPut FileSharingConfig + :<|> IFeatureStatusPatch '[] '() FileSharingConfig + -- ConferenceCallingConfig + :<|> IFeatureStatusGet ConferenceCallingConfig + :<|> IFeatureStatusPut '[] '() ConferenceCallingConfig + :<|> IFeatureStatusPatch '[] '() ConferenceCallingConfig + -- SelfDeletingMessagesConfig + :<|> IFeatureStatusGet SelfDeletingMessagesConfig + :<|> IFeatureStatusPut '[] '() SelfDeletingMessagesConfig + :<|> IFeatureStatusLockStatusPut SelfDeletingMessagesConfig + :<|> IFeatureStatusPatch '[] '() SelfDeletingMessagesConfig + -- GuestLinksConfig + :<|> IFeatureStatusGet GuestLinksConfig + :<|> IFeatureStatusPut '[] '() GuestLinksConfig + :<|> IFeatureStatusLockStatusPut GuestLinksConfig + :<|> IFeatureStatusPatch '[] '() GuestLinksConfig + -- SndFactorPasswordChallengeConfig + :<|> IFeatureStatusGet SndFactorPasswordChallengeConfig + :<|> IFeatureStatusPut '[] '() SndFactorPasswordChallengeConfig + :<|> IFeatureStatusLockStatusPut SndFactorPasswordChallengeConfig + :<|> IFeatureStatusPatch '[] '() SndFactorPasswordChallengeConfig + -- SearchVisibilityInboundConfig + :<|> IFeatureStatusGet SearchVisibilityInboundConfig + :<|> IFeatureStatusPut '[] '() SearchVisibilityInboundConfig + :<|> IFeatureStatusPatch '[] '() SearchVisibilityInboundConfig + :<|> IFeatureNoConfigMultiGet SearchVisibilityInboundConfig + -- ClassifiedDomainsConfig + :<|> IFeatureStatusGet ClassifiedDomainsConfig + -- MLSConfig + :<|> IFeatureStatusGet MLSConfig + :<|> IFeatureStatusPut '[] '() MLSConfig + :<|> IFeatureStatusPatch '[] '() MLSConfig + -- ExposeInvitationURLsToTeamAdminConfig + :<|> IFeatureStatusGet ExposeInvitationURLsToTeamAdminConfig + :<|> IFeatureStatusPut '[] '() ExposeInvitationURLsToTeamAdminConfig + :<|> IFeatureStatusPatch '[] '() ExposeInvitationURLsToTeamAdminConfig + -- SearchVisibilityInboundConfig + :<|> IFeatureStatusGet SearchVisibilityInboundConfig + :<|> IFeatureStatusPut '[] '() SearchVisibilityInboundConfig + :<|> IFeatureStatusPatch '[] '() SearchVisibilityInboundConfig + -- OutlookCalIntegrationConfig + :<|> IFeatureStatusGet OutlookCalIntegrationConfig + :<|> IFeatureStatusPut '[] '() OutlookCalIntegrationConfig + :<|> IFeatureStatusPatch '[] '() OutlookCalIntegrationConfig + :<|> IFeatureStatusLockStatusPut OutlookCalIntegrationConfig + -- MlsE2EIdConfig + :<|> IFeatureStatusGet MlsE2EIdConfig + :<|> IFeatureStatusPut '[] '() MlsE2EIdConfig + :<|> IFeatureStatusPatch '[] '() MlsE2EIdConfig + :<|> IFeatureStatusLockStatusPut MlsE2EIdConfig + -- all feature configs + :<|> Named + "feature-configs-internal" + ( Summary "Get all feature configs (for user/team; if n/a fall back to site config)." + :> "feature-configs" + :> CanThrow OperationDenied + :> CanThrow 'NotATeamMember + :> CanThrow 'TeamNotFound + :> QueryParam' + [ Optional, + Strict, + Description "Optional user id" + ] + "user_id" + UserId + :> Get '[Servant.JSON] AllFeatureConfigs + ) + +type InternalAPI = "i" :> InternalAPIBase + +type InternalAPIBase = + Named + "status" + ( "status" :> MultiVerb 'GET '[Servant.JSON] '[RespondEmpty 200 "OK"] () + ) + -- This endpoint can lead to the following events being sent: + -- - MemberLeave event to members for all conversations the user was in + :<|> Named + "delete-user" + ( Summary + "Remove a user from their teams and conversations and erase their clients" + :> MakesFederatedCall 'Galley "on-conversation-updated" + :> MakesFederatedCall 'Galley "on-user-deleted-conversations" + :> MakesFederatedCall 'Galley "on-mls-message-sent" + :> ZLocalUser + :> ZOptConn + :> "user" + :> MultiVerb 'DELETE '[Servant.JSON] '[RespondEmpty 200 "Remove a user from Galley"] () + ) + -- This endpoint can lead to the following events being sent: + -- - ConvCreate event to self, if conversation did not exist before + -- - ConvConnect event to self, if other didn't join the connect conversation before + :<|> Named + "connect" + ( Summary "Create a connect conversation (deprecated)" + :> MakesFederatedCall 'Galley "on-conversation-created" + :> CanThrow 'ConvNotFound + :> CanThrow 'InvalidOperation + :> CanThrow 'NotConnected + :> ZLocalUser + :> ZOptConn + :> "conversations" + :> "connect" + :> ReqBody '[Servant.JSON] Connect + :> ConversationVerb + ) + :<|> Named + "guard-legalhold-policy-conflicts" + ( "guard-legalhold-policy-conflicts" + :> CanThrow 'MissingLegalholdConsent + :> ReqBody '[Servant.JSON] GuardLegalholdPolicyConflicts + :> MultiVerb1 'PUT '[Servant.JSON] (RespondEmpty 200 "Guard Legalhold Policy") + ) + :<|> ILegalholdWhitelistedTeamsAPI + :<|> ITeamsAPI + :<|> Named + "upsert-one2one" + ( Summary "Create or Update a connect or one2one conversation." + :> "conversations" + :> "one2one" + :> "upsert" + :> ReqBody '[Servant.JSON] UpsertOne2OneConversationRequest + :> Post '[Servant.JSON] UpsertOne2OneConversationResponse + ) + :<|> IFeatureAPI + +type ILegalholdWhitelistedTeamsAPI = + "legalhold" + :> "whitelisted-teams" + :> Capture "tid" TeamId + :> ILegalholdWhitelistedTeamsAPIBase + +type ILegalholdWhitelistedTeamsAPIBase = + Named + "set-team-legalhold-whitelisted" + (MultiVerb1 'PUT '[Servant.JSON] (RespondEmpty 200 "Team Legalhold Whitelisted")) + :<|> Named + "unset-team-legalhold-whitelisted" + (MultiVerb1 'DELETE '[Servant.JSON] (RespondEmpty 204 "Team Legalhold un-Whitelisted")) + :<|> Named + "get-team-legalhold-whitelisted" + ( MultiVerb + 'GET + '[Servant.JSON] + '[ RespondEmpty 404 "Team not Legalhold Whitelisted", + RespondEmpty 200 "Team Legalhold Whitelisted" + ] + Bool + ) + +type ITeamsAPI = "teams" :> Capture "tid" TeamId :> ITeamsAPIBase + +type ITeamsAPIBase = + Named "get-team-internal" (CanThrow 'TeamNotFound :> Get '[Servant.JSON] TeamData) + :<|> Named + "create-binding-team" + ( ZUser + :> ReqBody '[Servant.JSON] BindingNewTeam + :> MultiVerb1 + 'PUT + '[Servant.JSON] + ( WithHeaders + '[Header "Location" TeamId] + TeamId + (RespondEmpty 201 "OK") + ) + ) + :<|> Named + "delete-binding-team" + ( CanThrow 'NoBindingTeam + :> CanThrow 'NotAOneMemberTeam + :> CanThrow 'DeleteQueueFull + :> CanThrow 'TeamNotFound + :> QueryFlag "force" + :> MultiVerb1 'DELETE '[Servant.JSON] (RespondEmpty 202 "OK") + ) + :<|> Named "get-team-name" ("name" :> CanThrow 'TeamNotFound :> Get '[Servant.JSON] TeamName) + :<|> Named + "update-team-status" + ( "status" + :> CanThrow 'TeamNotFound + :> CanThrow 'InvalidTeamStatusUpdate + :> ReqBody '[Servant.JSON] TeamStatusUpdate + :> MultiVerb1 'PUT '[Servant.JSON] (RespondEmpty 200 "OK") + ) + :<|> "members" + :> ( Named + "unchecked-add-team-member" + ( CanThrow 'TooManyTeamMembers + :> CanThrow 'TooManyTeamMembersOnTeamWithLegalhold + :> ReqBody '[Servant.JSON] NewTeamMember + :> MultiVerb1 'POST '[Servant.JSON] (RespondEmpty 200 "OK") + ) + :<|> Named + "unchecked-get-team-members" + ( QueryParam' '[Strict] "maxResults" (Range 1 HardTruncationLimit Int32) + :> Get '[Servant.JSON] TeamMemberList + ) + :<|> Named + "unchecked-get-team-member" + ( Capture "uid" UserId + :> CanThrow 'TeamMemberNotFound + :> Get '[Servant.JSON] TeamMember + ) + :<|> Named + "can-user-join-team" + ( "check" + :> 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" + ( "is-team-owner" + :> Capture "uid" UserId + :> CanThrow 'AccessDenied + :> CanThrow 'TeamMemberNotFound + :> CanThrow 'NotATeamMember + :> MultiVerb1 'GET '[Servant.JSON] (RespondEmpty 200 "User is team owner") + ) + :<|> "search-visibility" + :> ( Named "get-search-visibility-internal" (Get '[Servant.JSON] TeamSearchVisibilityView) + :<|> Named + "set-search-visibility-internal" + ( CanThrow 'TeamSearchVisibilityNotEnabled + :> CanThrow OperationDenied + :> CanThrow 'NotATeamMember + :> CanThrow 'TeamNotFound + :> ReqBody '[Servant.JSON] TeamSearchVisibilityView + :> MultiVerb1 'PUT '[Servant.JSON] (RespondEmpty 204 "OK") + ) + ) + +type IFeatureStatusGet f = Named '("iget", f) (FeatureStatusBaseGet f) + +type IFeatureStatusPut calls errs f = Named '("iput", f) (ApplyMods calls (FeatureStatusBasePutInternal errs f)) + +type IFeatureStatusPatch calls errs f = Named '("ipatch", f) (ApplyMods calls (FeatureStatusBasePatchInternal errs f)) + +type FeatureStatusBasePutInternal errs featureConfig = + FeatureStatusBaseInternal + (AppendSymbol "Put config for " (FeatureSymbol featureConfig)) + errs + featureConfig + ( ReqBody '[Servant.JSON] (WithStatusNoLock featureConfig) + :> Put '[Servant.JSON] (WithStatus featureConfig) + ) + +type FeatureStatusBasePatchInternal errs featureConfig = + FeatureStatusBaseInternal + (AppendSymbol "Patch config for " (FeatureSymbol featureConfig)) + errs + featureConfig + ( ReqBody '[Servant.JSON] (WithStatusPatch featureConfig) + :> Patch '[Servant.JSON] (WithStatus featureConfig) + ) + +type FeatureStatusBaseInternal desc errs featureConfig a = + Summary desc + :> CanThrow OperationDenied + :> CanThrow 'NotATeamMember + :> CanThrow 'TeamNotFound + :> CanThrow TeamFeatureError + :> CanThrowMany errs + :> "teams" + :> Capture "tid" TeamId + :> "features" + :> FeatureSymbol featureConfig + :> a + +type IFeatureStatusLockStatusPut featureName = + Named + '("ilock", featureName) + ( Summary (AppendSymbol "(Un-)lock " (FeatureSymbol featureName)) + :> CanThrow 'NotATeamMember + :> CanThrow 'TeamNotFound + :> "teams" + :> Capture "tid" TeamId + :> "features" + :> FeatureSymbol featureName + :> Capture "lockStatus" LockStatus + :> Put '[Servant.JSON] LockStatusResponse + ) + +type FeatureNoConfigMultiGetBase featureName = + Summary + (AppendSymbol "Get team feature status in bulk for feature " (FeatureSymbol featureName)) + :> "features-multi-teams" + :> FeatureSymbol featureName + :> ReqBody '[Servant.JSON] TeamFeatureNoConfigMultiRequest + :> Post '[Servant.JSON] (TeamFeatureNoConfigMultiResponse featureName) + +type IFeatureNoConfigMultiGet f = + Named + '("igetmulti", f) + (FeatureNoConfigMultiGetBase f) + +swaggerDoc :: Swagger +swaggerDoc = + toSwagger (Proxy @InternalAPI) + & info . title .~ "Wire-Server internal galley API" diff --git a/libs/galley-types/src/Galley/Types/Conversations/Intra.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley/ConversationsIntra.hs similarity index 89% rename from libs/galley-types/src/Galley/Types/Conversations/Intra.hs rename to libs/wire-api/src/Wire/API/Routes/Internal/Galley/ConversationsIntra.hs index 26c941bec3..a296747691 100644 --- a/libs/galley-types/src/Galley/Types/Conversations/Intra.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley/ConversationsIntra.hs @@ -1,6 +1,6 @@ -- This file is part of the Wire Server implementation. -- --- Copyright (C) 2022 Wire Swiss GmbH +-- Copyright (C) 2023 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 @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Galley.Types.Conversations.Intra +module Wire.API.Routes.Internal.Galley.ConversationsIntra ( DesiredMembership (..), Actor (..), UpsertOne2OneConversationRequest (..), @@ -28,6 +28,7 @@ import Data.Aeson.Types (FromJSON, ToJSON) import Data.Id (ConvId, UserId) import Data.Qualified import Data.Schema +import qualified Data.Swagger as Swagger import Imports data DesiredMembership = Included | Excluded @@ -62,7 +63,7 @@ data UpsertOne2OneConversationRequest = UpsertOne2OneConversationRequest uooConvId :: Maybe (Qualified ConvId) } deriving (Show, Generic) - deriving (FromJSON, ToJSON) via Schema UpsertOne2OneConversationRequest + deriving (FromJSON, ToJSON, Swagger.ToSchema) via Schema UpsertOne2OneConversationRequest instance ToSchema UpsertOne2OneConversationRequest where schema = @@ -78,7 +79,7 @@ newtype UpsertOne2OneConversationResponse = UpsertOne2OneConversationResponse { uuorConvId :: Qualified ConvId } deriving (Show, Generic) - deriving (FromJSON, ToJSON) via Schema UpsertOne2OneConversationResponse + deriving (FromJSON, ToJSON, Swagger.ToSchema) via Schema UpsertOne2OneConversationResponse instance ToSchema UpsertOne2OneConversationResponse where schema = diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamFeatureNoConfigMulti.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamFeatureNoConfigMulti.hs index e3b704438e..40c9a1a094 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamFeatureNoConfigMulti.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamFeatureNoConfigMulti.hs @@ -33,7 +33,7 @@ newtype TeamFeatureNoConfigMultiRequest = TeamFeatureNoConfigMultiRequest { teams :: [TeamId] } deriving (Show, Eq) - deriving (A.ToJSON, A.FromJSON) via (Schema TeamFeatureNoConfigMultiRequest) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema TeamFeatureNoConfigMultiRequest instance ToSchema TeamFeatureNoConfigMultiRequest where schema = @@ -45,7 +45,7 @@ newtype TeamFeatureNoConfigMultiResponse cfg = TeamFeatureNoConfigMultiResponse { teamsStatuses :: [TeamStatus cfg] } deriving (Show, Eq) - deriving (A.ToJSON, A.FromJSON) via (Schema (TeamFeatureNoConfigMultiResponse cfg)) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema (TeamFeatureNoConfigMultiResponse cfg) instance ToSchema (TeamFeatureNoConfigMultiResponse cfg) where schema = @@ -58,7 +58,7 @@ data TeamStatus cfg = TeamStatus status :: Public.FeatureStatus } deriving (Show, Eq) - deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema (TeamStatus cfg)) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema (TeamStatus cfg) instance ToSchema (TeamStatus cfg) where schema = diff --git a/libs/galley-types/src/Galley/Types/Teams/Intra.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamsIntra.hs similarity index 65% rename from libs/galley-types/src/Galley/Types/Teams/Intra.hs rename to libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamsIntra.hs index 75369bc73a..3b64ef876c 100644 --- a/libs/galley-types/src/Galley/Types/Teams/Intra.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamsIntra.hs @@ -1,11 +1,3 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE StandaloneDeriving #-} -{-# LANGUAGE TemplateHaskell #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -22,8 +14,11 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE OverloadedStrings #-} -module Galley.Types.Teams.Intra +module Wire.API.Routes.Internal.Galley.TeamsIntra ( TeamStatus (..), TeamData (..), TeamStatusUpdate (..), @@ -32,12 +27,12 @@ module Galley.Types.Teams.Intra ) where +import Control.Lens ((?~)) import Data.Aeson -import Data.Aeson.TH import qualified Data.Currency as Currency import Data.Json.Util import qualified Data.Schema as S -import qualified Data.Swagger as Swagger hiding (schema) +import qualified Data.Swagger as Swagger import Data.Time (UTCTime) import Imports import Test.QuickCheck.Arbitrary (Arbitrary) @@ -53,6 +48,7 @@ data TeamStatus | Suspended | PendingActive deriving (Eq, Show, Generic) + deriving (Arbitrary) via GenericUniform TeamStatus deriving (ToJSON, FromJSON, Swagger.ToSchema) via S.Schema TeamStatus instance S.ToSchema TeamStatus where @@ -72,6 +68,7 @@ data TeamData = TeamData tdStatusTime :: !(Maybe UTCTime) -- This needs to be a Maybe due to backwards compatibility } deriving (Eq, Show, Generic) + deriving (Arbitrary) via GenericUniform TeamData deriving (ToJSON, FromJSON, Swagger.ToSchema) via S.Schema TeamData instance S.ToSchema TeamData where @@ -88,25 +85,35 @@ data TeamStatusUpdate = TeamStatusUpdate -- TODO: Remove Currency selection once billing supports currency changes after team creation } deriving (Eq, Show, Generic) + deriving (Arbitrary) via GenericUniform TeamStatusUpdate + deriving (ToJSON, FromJSON, Swagger.ToSchema) via S.Schema TeamStatusUpdate -instance FromJSON TeamStatusUpdate where - parseJSON = withObject "team-status-update" $ \o -> - TeamStatusUpdate - <$> o .: "status" - <*> o .:? "currency" - -instance ToJSON TeamStatusUpdate where - toJSON s = - object - [ "status" .= tuStatus s, - "currency" .= tuCurrency s - ] +instance S.ToSchema TeamStatusUpdate where + schema = + S.object "TeamStatusUpdate" $ + TeamStatusUpdate + <$> tuStatus S..= S.field "status" S.schema + <*> tuCurrency S..= S.maybe_ (S.optField "currency" currencyAlphaSchema) + where + currencyAlphaSchema :: S.ValueSchema S.NamedSwaggerDoc Currency.Alpha + currencyAlphaSchema = S.mkSchema docs parseJSON (pure . toJSON) + where + docs = + S.swaggerDoc @Text + & Swagger.schema . Swagger.description ?~ "ISO 4217 alphabetic codes" + & Swagger.schema . Swagger.example ?~ "EUR" newtype TeamName = TeamName {tnName :: Text} deriving (Eq, Show, Generic) + deriving (Arbitrary) via GenericUniform TeamName + deriving (ToJSON, FromJSON, Swagger.ToSchema) via S.Schema TeamName -deriveJSON toJSONFieldName ''TeamName +instance S.ToSchema TeamName where + schema = + S.object "TeamName" $ + TeamName + <$> tnName S..= S.field "name" S.schema data GuardLegalholdPolicyConflicts = GuardLegalholdPolicyConflicts { glhProtectee :: LegalholdProtectee, @@ -114,7 +121,11 @@ data GuardLegalholdPolicyConflicts = GuardLegalholdPolicyConflicts } deriving (Show, Eq, Generic) deriving (Arbitrary) via (GenericUniform GuardLegalholdPolicyConflicts) + deriving (ToJSON, FromJSON, Swagger.ToSchema) via S.Schema GuardLegalholdPolicyConflicts -instance ToJSON GuardLegalholdPolicyConflicts - -instance FromJSON GuardLegalholdPolicyConflicts +instance S.ToSchema GuardLegalholdPolicyConflicts where + schema = + S.object "GuardLegalholdPolicyConflicts" $ + GuardLegalholdPolicyConflicts + <$> glhProtectee S..= S.field "glhProtectee" S.schema + <*> glhUserClients S..= S.field "glhUserClients" S.schema diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs b/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs index 530449cbfd..69d114dca8 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs @@ -17,19 +17,28 @@ module Wire.API.Routes.Internal.LegalHold where +import Control.Lens import Data.Id +import Data.Proxy +import Data.Swagger +import Imports import Servant.API hiding (Header, WithStatus) +import Servant.Swagger import Wire.API.Team.Feature type InternalLegalHoldAPI = "i" :> "teams" - :> Capture "tid" TeamId - :> "legalhold" - :> Get '[JSON] (WithStatus LegalholdConfig) - :<|> "i" - :> "teams" - :> Capture "tid" TeamId - :> "legalhold" - :> ReqBody '[JSON] (WithStatusNoLock LegalholdConfig) - :> Put '[] NoContent + :> ( Capture "tid" TeamId + :> "legalhold" + :> Get '[JSON] (WithStatus LegalholdConfig) + :<|> Capture "tid" TeamId + :> "legalhold" + :> ReqBody '[JSON] (WithStatusNoLock LegalholdConfig) + :> Put '[] NoContent + ) + +swaggerDoc :: Swagger +swaggerDoc = + toSwagger (Proxy @InternalLegalHoldAPI) + & info . title .~ "Wire-Server internal legalhold API" diff --git a/services/brig/schema/src/V28.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs similarity index 51% rename from services/brig/schema/src/V28.hs rename to libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs index aa4248485c..63f2358f5e 100644 --- a/services/brig/schema/src/V28.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs @@ -1,6 +1,6 @@ -- This file is part of the Wire Server implementation. -- --- Copyright (C) 2022 Wire Swiss GmbH +-- Copyright (C) 2023 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 @@ -15,16 +15,26 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V28 - ( migration, - ) -where +module Wire.API.Routes.Internal.Spar where -import Cassandra.Schema +import Control.Lens +import Data.Id +import Data.Swagger import Imports -import Text.RawString.QQ +import Servant +import Servant.Swagger +import Wire.API.User +import Wire.API.User.Saml -migration :: Migration -migration = Migration 28 "Add additional client properties" $ do - schema' [r| alter columnfamily clients add class int; |] - schema' [r| alter columnfamily clients add cookie text; |] +type InternalAPI = + "i" + :> ( "status" :> Get '[JSON] NoContent + :<|> "teams" :> Capture "team" TeamId :> DeleteNoContent + :<|> "sso" :> "settings" :> ReqBody '[JSON] SsoSettings :> Put '[JSON] NoContent + :<|> "scim" :> "userinfos" :> ReqBody '[JSON] UserSet :> Post '[JSON] ScimUserInfos + ) + +swaggerDoc :: Swagger +swaggerDoc = + toSwagger (Proxy @InternalAPI) + & info . title .~ "Wire-Server internal spar API" diff --git a/libs/wire-api/src/Wire/API/Routes/MultiTablePaging.hs b/libs/wire-api/src/Wire/API/Routes/MultiTablePaging.hs index 4a8f737967..f0b697e059 100644 --- a/libs/wire-api/src/Wire/API/Routes/MultiTablePaging.hs +++ b/libs/wire-api/src/Wire/API/Routes/MultiTablePaging.hs @@ -62,7 +62,7 @@ data GetMultiTablePageRequest (name :: Symbol) (tables :: Type) (max :: Nat) (de -- 24 | deriving ToJSON via Schema (GetMultiTablePageRequest name tables max def) -- | ^^^^^^ -type RequestSchemaConstraint name tables max def = (KnownNat max, KnownNat def, Within Int32 1 max, LTE 1 def, LTE def max, PagingTable tables, KnownSymbol name) +type RequestSchemaConstraint name tables max def = (KnownNat max, KnownNat def, Within Int32 1 max, 1 <= def, def <= max, PagingTable tables, KnownSymbol name) deriving via Schema (GetMultiTablePageRequest name tables max def) diff --git a/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs b/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs index e8d79bee60..5b2c913f8e 100644 --- a/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs +++ b/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs @@ -58,6 +58,7 @@ import Data.Containers.ListUtils import Data.Either.Combinators (leftToMaybe) import Data.HashMap.Strict.InsOrd (InsOrdHashMap) import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap +import Data.Kind import Data.Metrics.Servant import Data.Proxy import Data.SOP @@ -94,13 +95,13 @@ type Declare = S.Declare (S.Definitions S.Schema) -- Includes status code, description, and return type. The content type of the -- response is determined dynamically using the accept header and the list of -- supported content types specified in the containing 'MultiVerb' type. -data Respond (s :: Nat) (desc :: Symbol) (a :: *) +data Respond (s :: Nat) (desc :: Symbol) (a :: Type) -- | A type to describe a 'MultiVerb' response with a fixed content type. -- -- Similar to 'Respond', but hardcodes the content type to be used for -- generating the response. -data RespondAs ct (s :: Nat) (desc :: Symbol) (a :: *) +data RespondAs ct (s :: Nat) (desc :: Symbol) (a :: Type) -- | A type to describe a 'MultiVerb' response with an empty body. -- @@ -111,7 +112,7 @@ type RespondEmpty s desc = RespondAs '() s desc () -- -- Includes status code, description, framing strategy and content type. Note -- that the handler return type is hardcoded to be 'SourceIO ByteString'. -data RespondStreaming (s :: Nat) (desc :: Symbol) (framing :: *) (ct :: *) +data RespondStreaming (s :: Nat) (desc :: Symbol) (framing :: Type) (ct :: Type) -- | The result of parsing a response as a union alternative of type 'a'. -- @@ -151,11 +152,11 @@ instance MonadPlus UnrenderResult where class IsSwaggerResponse a where responseSwagger :: Declare S.Response -type family ResponseType a :: * +type family ResponseType a :: Type class IsWaiBody (ResponseBody a) => IsResponse cs a where type ResponseStatus a :: Nat - type ResponseBody a :: * + type ResponseBody a :: Type responseRender :: AcceptHeader -> ResponseType a -> Maybe (ResponseF (ResponseBody a)) responseUnrender :: M.MediaType -> ResponseF (ResponseBody a) -> UnrenderResult (ResponseType a) @@ -211,7 +212,7 @@ instance MimeRender ct a, MimeUnrender ct a ) => - IsResponse cs (RespondAs (ct :: *) s desc a) + IsResponse cs (RespondAs (ct :: Type) s desc a) where type ResponseStatus (RespondAs ct s desc a) = s type ResponseBody (RespondAs ct s desc a) = LByteString @@ -248,7 +249,7 @@ instance KnownStatus s => IsResponse cs (RespondAs '() s desc ()) where instance (KnownSymbol desc, S.ToSchema a) => - IsSwaggerResponse (RespondAs (ct :: *) s desc a) + IsSwaggerResponse (RespondAs (ct :: Type) s desc a) where responseSwagger = simpleResponseSwagger @a @desc @@ -282,7 +283,7 @@ instance guard (responseStatusCode resp == statusVal (Proxy @s)) pure $ responseBody resp -instance (KnownStatus s, KnownSymbol desc) => IsSwaggerResponse (RespondStreaming s desc framing ct) where +instance KnownSymbol desc => IsSwaggerResponse (RespondStreaming s desc framing ct) where responseSwagger = pure $ mempty @@ -294,7 +295,7 @@ instance (KnownStatus s, KnownSymbol desc) => IsSwaggerResponse (RespondStreamin -- * @hs@: type-level list of headers -- * @a@: return type (with headers) -- * @r@: underlying response (without headers) -data WithHeaders (hs :: [*]) (a :: *) (r :: *) +data WithHeaders (hs :: [Type]) (a :: Type) (r :: Type) -- | This is used to convert a response containing headers to a custom type -- including the information in the headers. @@ -312,7 +313,7 @@ instance AsHeaders '[h] a (a, h) where toHeaders (t, cc) = (I cc :* Nil, t) fromHeaders (I cc :* Nil, t) = (t, cc) -data DescHeader (name :: Symbol) (desc :: Symbol) (a :: *) +data DescHeader (name :: Symbol) (desc :: Symbol) (a :: Type) -- | A wrapper to turn a response header into an optional one. data OptHeader h @@ -335,7 +336,6 @@ headerName = instance ( KnownSymbol name, ServantHeader h name x, - ToHttpApiData x, FromHttpApiData x, ServantHeaders hs xs ) => @@ -425,7 +425,7 @@ instance class IsSwaggerResponseList as where responseListSwagger :: Declare (InsOrdHashMap S.HttpStatusCode S.Response) -type family ResponseTypes (as :: [*]) where +type family ResponseTypes (as :: [Type]) where ResponseTypes '[] = '[] ResponseTypes (a ': as) = ResponseType a ': ResponseTypes as @@ -501,7 +501,7 @@ combineSwaggerSchema s1 s2 -- instance. -- * Headers can be attached to individual responses, also without affecting -- the handler return type. -data MultiVerb (method :: StdMethod) cs (as :: [*]) (r :: *) +data MultiVerb (method :: StdMethod) cs (as :: [Type]) (r :: Type) -- | A 'MultiVerb' endpoint with a single response. type MultiVerb1 m cs a = MultiVerb m cs '[a] (ResponseType a) @@ -512,7 +512,7 @@ type MultiVerb1 m cs a = MultiVerb m cs '[a] (ResponseType a) -- Any glue code necessary to convert application types to and from the -- canonical 'Union' type corresponding to a 'MultiVerb' endpoint should be -- packaged into an 'AsUnion' instance. -class AsUnion (as :: [*]) (r :: *) where +class AsUnion (as :: [Type]) (r :: Type) where toUnion :: r -> Union (ResponseTypes as) fromUnion :: Union (ResponseTypes as) -> r @@ -622,7 +622,7 @@ instance AsConstructor '[a] (Respond code desc a) where toConstructor x = I x :* Nil fromConstructor = unI . hd -instance AsConstructor '[a] (RespondAs (ct :: *) code desc a) where +instance AsConstructor '[a] (RespondAs (ct :: Type) code desc a) where toConstructor x = I x :* Nil fromConstructor = unI . hd @@ -718,7 +718,7 @@ instance instance (SwaggerMethod method, IsSwaggerResponseList as, AllMime cs) => - S.HasSwagger (MultiVerb method (cs :: [*]) as r) + S.HasSwagger (MultiVerb method (cs :: [Type]) as r) where toSwagger _ = mempty diff --git a/libs/wire-api/src/Wire/API/Routes/Named.hs b/libs/wire-api/src/Wire/API/Routes/Named.hs index df29b1a53b..0a1e09b149 100644 --- a/libs/wire-api/src/Wire/API/Routes/Named.hs +++ b/libs/wire-api/src/Wire/API/Routes/Named.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -17,6 +19,7 @@ module Wire.API.Routes.Named where +import Data.Kind import Data.Metrics.Servant import Data.Proxy import GHC.TypeLits @@ -51,7 +54,7 @@ instance HasClient m api => HasClient m (Named n api) where clientWithRoute pm _ req = clientWithRoute pm (Proxy @api) req hoistClientMonad pm _ f = hoistClientMonad pm (Proxy @api) f -type family FindName n (api :: *) :: (n, *) where +type family FindName n (api :: Type) :: (n, Type) where FindName n (Named name api) = '(name, api) FindName n (x :> api) = AddPrefix x (FindName n api) FindName n api = '(TypeError ('Text "Named combinator not found"), api) @@ -89,7 +92,7 @@ type family FMap (f :: a -> b) (m :: Maybe a) :: Maybe b where FMap _ 'Nothing = 'Nothing FMap f ('Just a) = 'Just (f a) -type family LookupEndpoint api name :: Maybe (*) where +type family LookupEndpoint api name :: Maybe Type where LookupEndpoint (Named name endpoint) name = 'Just endpoint LookupEndpoint (api1 :<|> api2) name = MappendMaybe diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index deff0d727c..bd2222e736 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -30,9 +30,6 @@ module Wire.API.Routes.Public ZBot, ZConversation, ZProvider, - - -- * Swagger combinators - OmitDocs, ) where @@ -40,6 +37,7 @@ import Control.Lens ((<>~)) import Data.Domain import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap import Data.Id as Id +import Data.Kind import Data.Metrics.Servant import Data.Qualified import Data.Swagger @@ -87,8 +85,8 @@ class IsZType (ztype :: ZType) ctx where type ZHeader ztype :: Symbol - type ZParam ztype :: * - type ZQualifiedParam ztype :: * + type ZParam ztype :: Type + type ZQualifiedParam ztype :: Type qualifyZParam :: Context ctx -> ZParam ztype -> ZQualifiedParam ztype @@ -154,7 +152,7 @@ instance IsZType 'ZAuthProvider ctx where instance HasTokenType 'ZAuthProvider where tokenType = Just "provider" -data ZAuthServant (ztype :: ZType) (opts :: [*]) +data ZAuthServant (ztype :: ZType) (opts :: [Type]) type InternalAuthDefOpts = '[Servant.Required, Servant.Strict] @@ -253,25 +251,3 @@ instance RoutesToPaths api => RoutesToPaths (ZAuthServant ztype opts :> api) whe -- FUTUREWORK: Make a PR to the servant-swagger package with this instance instance ToSchema a => ToSchema (Headers ls a) where declareNamedSchema _ = declareNamedSchema (Proxy @a) - --- | A type-level tag that lets us omit any branch from Swagger docs. --- --- Those are likely to be: --- --- * Endpoints for which we can't generate Swagger docs. --- * The endpoint that serves Swagger docs. --- * Internal endpoints. -data OmitDocs - -instance HasSwagger (OmitDocs :> a) where - toSwagger _ = mempty - -instance HasServer api ctx => HasServer (OmitDocs :> api) ctx where - type ServerT (OmitDocs :> api) m = ServerT api m - - route _ = route (Proxy :: Proxy api) - hoistServerWithContext _ pc nt s = - hoistServerWithContext (Proxy :: Proxy api) pc nt s - -instance RoutesToPaths api => RoutesToPaths (OmitDocs :> api) where - getRoutes = getRoutes @api 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 430b20c002..365c82621a 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -1486,11 +1486,19 @@ type TeamsAPI = type SystemSettingsAPI = Named - "get-system-settings" + "get-system-settings-unauthorized" ( Summary "Returns a curated set of system configuration settings." :> From 'V3 :> "system" :> "settings" :> "unauthorized" - :> Get '[JSON] SystemSettings + :> Get '[JSON] SystemSettingsPublic ) + :<|> Named + "get-system-settings" + ( Summary "Returns a curated set of system configuration settings for authorized users." + :> ZUser + :> "system" + :> "settings" + :> Get '[JSON] SystemSettings + ) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs b/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs index f31683711f..b4ea06f2bb 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs @@ -18,6 +18,7 @@ module Wire.API.Routes.Public.Cargohold where import Data.Id +import Data.Kind import Data.Metrics.Servant import Data.Qualified import Data.SOP @@ -40,7 +41,7 @@ import Wire.API.Routes.Version data PrincipalTag = UserPrincipalTag | BotPrincipalTag | ProviderPrincipalTag deriving (Eq, Show) -type family PrincipalId (tag :: PrincipalTag) = (id :: *) | id -> tag where +type family PrincipalId (tag :: PrincipalTag) = (id :: Type) | id -> tag where PrincipalId 'UserPrincipalTag = Local UserId PrincipalId 'BotPrincipalTag = BotId PrincipalId 'ProviderPrincipalTag = ProviderId 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 a1d786c15a..2be633e5e7 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs @@ -35,6 +35,7 @@ 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 +import Wire.API.Routes.Public.Galley.TeamNotification (TeamNotificationAPI) type ServantAPI = ConversationAPI @@ -47,6 +48,7 @@ type ServantAPI = :<|> CustomBackendAPI :<|> LegalHoldAPI :<|> TeamMemberAPI + :<|> TeamNotificationAPI swaggerDoc :: Swagger.Swagger swaggerDoc = toSwagger (Proxy @ServantAPI) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index 65dc97b08b..908c5c88eb 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -161,6 +161,7 @@ type ConversationAPI = :<|> Named "get-group-info" ( Summary "Get MLS group information" + :> From 'V4 :> MakesFederatedCall 'Galley "query-group-info" :> CanThrow 'ConvNotFound :> CanThrow 'MLSMissingGroupInfo @@ -331,6 +332,7 @@ type ConversationAPI = :> MakesFederatedCall 'Galley "on-conversation-created" :> Until 'V3 :> CanThrow 'ConvAccessDenied + :> CanThrow 'MLSMissingSenderClient :> CanThrow 'MLSNonEmptyMemberList :> CanThrow 'MLSNotEnabled :> CanThrow 'NotConnected @@ -339,6 +341,7 @@ type ConversationAPI = :> CanThrow 'MissingLegalholdConsent :> Description "This returns 201 when a new conversation is created, and 200 when the conversation already existed" :> ZLocalUser + :> ZOptClient :> ZConn :> "conversations" :> VersionedReqBody 'V2 '[Servant.JSON] NewConv @@ -350,6 +353,7 @@ type ConversationAPI = :> MakesFederatedCall 'Galley "on-conversation-created" :> From 'V3 :> CanThrow 'ConvAccessDenied + :> CanThrow 'MLSMissingSenderClient :> CanThrow 'MLSNonEmptyMemberList :> CanThrow 'MLSNotEnabled :> CanThrow 'NotConnected @@ -358,6 +362,7 @@ type ConversationAPI = :> CanThrow 'MissingLegalholdConsent :> Description "This returns 201 when a new conversation is created, and 200 when the conversation already existed" :> ZLocalUser + :> ZOptClient :> ZConn :> "conversations" :> ReqBody '[Servant.JSON] NewConv @@ -384,6 +389,7 @@ type ConversationAPI = :<|> Named "get-mls-self-conversation" ( Summary "Get the user's MLS self-conversation" + :> From 'V4 :> ZLocalUser :> "conversations" :> "mls-self" @@ -645,6 +651,8 @@ type ConversationAPI = "member-typing-unqualified" ( Summary "Sending typing notifications" :> Until 'V3 + :> MakesFederatedCall 'Galley "update-typing-indicator" + :> MakesFederatedCall 'Galley "on-typing-indicator-updated" :> CanThrow 'ConvNotFound :> ZLocalUser :> ZConn @@ -657,6 +665,7 @@ type ConversationAPI = :<|> Named "member-typing-qualified" ( Summary "Sending typing notifications" + :> MakesFederatedCall 'Galley "update-typing-indicator" :> MakesFederatedCall 'Galley "on-typing-indicator-updated" :> CanThrow 'ConvNotFound :> ZLocalUser 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 index 853884e30f..370a2a3d0f 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs @@ -85,6 +85,10 @@ type FeatureAPI = :<|> FeatureStatusPut '[] '() ExposeInvitationURLsToTeamAdminConfig :<|> FeatureStatusGet SearchVisibilityInboundConfig :<|> FeatureStatusPut '[] '() SearchVisibilityInboundConfig + :<|> FeatureStatusGet OutlookCalIntegrationConfig + :<|> FeatureStatusPut '[] '() OutlookCalIntegrationConfig + :<|> FeatureStatusGet MlsE2EIdConfig + :<|> FeatureStatusPut '[] '() MlsE2EIdConfig :<|> 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 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 index 2d6a25e5b0..f79247fa50 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs @@ -38,6 +38,7 @@ type MLSMessagingAPI = Named "mls-welcome-message" ( Summary "Post an MLS welcome message" + :> Until 'V3 :> MakesFederatedCall 'Galley "mls-welcome" :> CanThrow 'MLSKeyPackageRefNotFound :> CanThrow 'MLSNotEnabled @@ -126,7 +127,7 @@ type MLSMessagingAPI = :> MakesFederatedCall 'Galley "on-conversation-updated" :> MakesFederatedCall 'Galley "on-new-remote-conversation" :> MakesFederatedCall 'Brig "get-mls-clients" - :> From 'V3 + :> From 'V4 :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvMemberNotFound :> CanThrow 'ConvNotFound @@ -157,6 +158,7 @@ type MLSMessagingAPI = :<|> Named "mls-public-keys" ( Summary "Get public keys used by the backend to sign external proposals" + :> From 'V4 :> CanThrow 'MLSNotEnabled :> "public-keys" :> ZLocalUser diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamNotification.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamNotification.hs new file mode 100644 index 0000000000..c5e7f7eeb5 --- /dev/null +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamNotification.hs @@ -0,0 +1,60 @@ +module Wire.API.Routes.Public.Galley.TeamNotification where + +import Data.Range +import Imports +import Servant +import Wire.API.Error +import Wire.API.Error.Galley +import Wire.API.Notification +import Wire.API.Routes.Named +import Wire.API.Routes.Public + +type TeamNotificationAPI = + Named + "get-team-notifications" + ( Summary "Read recently added team members from team queue" + :> Description GetTeamNotificationsDescription + :> "teams" + :> "notifications" + :> ZUser + :> CanThrow 'TeamNotFound + :> CanThrow 'InvalidTeamNotificationId + :> QueryParam' + [ Optional, + Strict, + Description "Notification id to start with in the response (UUIDv1)" + ] + "since" + NotificationId + :> QueryParam' + [ Optional, + Strict, + Description "Maximum number of events to return (1..10000; default: 1000)" + ] + "size" + (Range 1 10000 Int32) + :> Get '[Servant.JSON] QueuedNotificationList + ) + +type GetTeamNotificationsDescription = + "This is a work-around for scalability issues with gundeck user event fan-out. \ + \It does not track all team-wide events, but only `member-join`.\ + \\n\ + \Note that `/teams/notifications` behaves differently from `/notifications`:\ + \\n\ + \- If there is a gap between the notification id requested with `since` and the \ + \available data, team queues respond with 200 and the data that could be found. \ + \They do NOT respond with status 404, but valid data in the body.\ + \\n\ + \- The notification with the id given via `since` is included in the \ + \response if it exists. You should remove this and only use it to decide whether \ + \there was a gap between your last request and this one.\ + \\n\ + \- If the notification id does *not* exist, you get the more recent events from the queue \ + \(instead of all of them). This can be done because a notification id is a UUIDv1, which \ + \is essentially a time stamp.\ + \\n\ + \- There is no corresponding `/last` end-point to get only the most recent event. \ + \That end-point was only useful to avoid having to pull the entire queue. In team \ + \queues, if you have never requested the queue before and \ + \have no prior notification id, just pull with timestamp 'now'." diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs index 26bd5205d0..4af44325db 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs @@ -25,7 +25,6 @@ import Imports import qualified SAML2.WebSSO as SAML import Servant import Servant.API.Extended -import Servant.API.Generic (ToServantApi, (:-)) import Servant.Multipart import Servant.Swagger (toSwagger) import qualified URI.ByteString as URI @@ -34,8 +33,9 @@ import Web.Scim.Class.Auth as Scim.Auth import Web.Scim.Class.User as Scim.User import Wire.API.Error import Wire.API.Error.Brig +import Wire.API.Routes.Internal.Spar import Wire.API.Routes.Public -import Wire.API.User (ScimUserInfos, UserSet) +import Wire.API.SwaggerServant import Wire.API.User.IdentityProvider import Wire.API.User.Saml import Wire.API.User.Scim @@ -49,10 +49,15 @@ type API = "sso" :> APISSO :<|> "identity-providers" :> APIIDP :<|> "scim" :> APIScim - :<|> OmitDocs :> "i" :> APIINTERNAL + :<|> OmitDocs :> InternalAPI + +type DeprecateSSOAPIV1 = + Description + "DEPRECATED! use /sso/metadata/:tid instead! \ + \Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams" type APISSO = - "metadata" :> SAML.APIMeta + DeprecateSSOAPIV1 :> "metadata" :> SAML.APIMeta :<|> "metadata" :> Capture "team" TeamId :> SAML.APIMeta :<|> "initiate-login" :> APIAuthReqPrecheck :<|> "initiate-login" :> APIAuthReq @@ -76,7 +81,8 @@ type APIAuthReq = :> Get '[SAML.HTML] (SAML.FormRedirect SAML.AuthnRequest) type APIAuthRespLegacy = - "finalize-login" + DeprecateSSOAPIV1 + :> "finalize-login" -- (SAML.APIAuthResp from here on, except for response) :> MultipartForm Mem SAML.AuthnResponseBody :> Post '[PlainText] Void @@ -106,7 +112,7 @@ type IdpGetAll = Get '[JSON] IdPList type IdpCreate = ReqBodyCustomError '[RawXML, JSON] "wai-error" IdPMetadataInfo :> QueryParam' '[Optional, Strict] "replaces" SAML.IdPId - :> QueryParam' '[Optional, Strict] "api_version" WireIdPAPIVersion + :> QueryParam' '[Optional, Strict] "api_version" WireIdPAPIVersion -- see also: 'DeprecateSSOAPIV1' -- FUTUREWORK: The handle is restricted to 32 characters. Can we find a more reasonable upper bound and create a type for it? Also see `IdpUpdate`. :> QueryParam' '[Optional, Strict] "handle" (Range 1 32 Text) :> PostCreated '[JSON] IdP @@ -126,12 +132,6 @@ type IdpDelete = type SsoSettingsGet = Get '[JSON] SsoSettings -type APIINTERNAL = - "status" :> Get '[JSON] NoContent - :<|> "teams" :> Capture "team" TeamId :> DeleteNoContent - :<|> "sso" :> "settings" :> ReqBody '[JSON] SsoSettings :> Put '[JSON] NoContent - :<|> "scim" :> "userinfos" :> ReqBody '[JSON] UserSet :> Post '[JSON] ScimUserInfos - sparSPIssuer :: (Functor m, SAML.HasConfig m) => Maybe TeamId -> m SAML.Issuer sparSPIssuer Nothing = SAML.Issuer <$> SAML.getSsoURI (Proxy @APISSO) (Proxy @APIAuthRespLegacy) diff --git a/libs/wire-api/src/Wire/API/Routes/QualifiedCapture.hs b/libs/wire-api/src/Wire/API/Routes/QualifiedCapture.hs index 6356821b5d..4fd030267d 100644 --- a/libs/wire-api/src/Wire/API/Routes/QualifiedCapture.hs +++ b/libs/wire-api/src/Wire/API/Routes/QualifiedCapture.hs @@ -22,6 +22,7 @@ module Wire.API.Routes.QualifiedCapture where import Data.Domain +import Data.Kind import Data.Metrics.Servant import Data.Qualified import Data.Swagger @@ -35,7 +36,7 @@ import Servant.Server.Internal.ErrorFormatter import Servant.Swagger -- | Capture a value qualified by a domain, with modifiers. -data QualifiedCapture' (mods :: [*]) (capture :: Symbol) (a :: *) +data QualifiedCapture' (mods :: [Type]) (capture :: Symbol) (a :: Type) -- | Capture a value qualified by a domain. -- @@ -63,6 +64,7 @@ instance instance ( KnownSymbol capture, + Typeable a, FromHttpApiData a, HasServer api context, SBoolI (FoldLenient mods), diff --git a/libs/wire-api/src/Wire/API/Routes/Version.hs b/libs/wire-api/src/Wire/API/Routes/Version.hs index 68d46bf8ee..0c4d4f2d4d 100644 --- a/libs/wire-api/src/Wire/API/Routes/Version.hs +++ b/libs/wire-api/src/Wire/API/Routes/Version.hs @@ -32,6 +32,7 @@ module Wire.API.Routes.Version developmentVersions, readVersionNumber, mkVersion, + toPathComponent, -- * Servant combinators Until, @@ -43,6 +44,7 @@ import Control.Lens ((?~)) import Data.Aeson (FromJSON, ToJSON (..)) import qualified Data.Aeson as Aeson import Data.Bifunctor +import Data.ByteString.Conversion (ToByteString (builder)) import qualified Data.ByteString.Lazy as LBS import Data.Domain import Data.Schema @@ -57,7 +59,7 @@ import Wire.API.Routes.Named import Wire.API.VersionInfo -- | Version of the public API. -data Version = V0 | V1 | V2 | V3 +data Version = V0 | V1 | V2 | V3 | V4 deriving stock (Eq, Ord, Bounded, Enum, Show) deriving (FromJSON, ToJSON) via (Schema Version) @@ -67,7 +69,8 @@ instance ToSchema Version where [ element 0 V0, element 1 V1, element 2 V2, - element 3 V3 + element 3 V3, + element 4 V4 ] mkVersion :: Integer -> Maybe Version @@ -83,11 +86,21 @@ instance ToHttpApiData Version where toHeader = LBS.toStrict . Aeson.encode toUrlPiece = Text.decodeUtf8 . toHeader +instance ToByteString Version where + builder = toEncodedUrlPiece + +-- | `Version` as it appears in an URL path +-- +-- >>> toPathComponent V1 +-- "v1" +toPathComponent :: Version -> ByteString +toPathComponent v = "v" <> toHeader v + supportedVersions :: [Version] supportedVersions = [minBound .. maxBound] developmentVersions :: [Version] -developmentVersions = [V3] +developmentVersions = [V4] -- | Information related to the public API version. -- diff --git a/libs/wire-api/src/Wire/API/Routes/Versioned.hs b/libs/wire-api/src/Wire/API/Routes/Versioned.hs index bb9dcf766a..44dac445e8 100644 --- a/libs/wire-api/src/Wire/API/Routes/Versioned.hs +++ b/libs/wire-api/src/Wire/API/Routes/Versioned.hs @@ -18,6 +18,7 @@ module Wire.API.Routes.Versioned where import Data.Aeson (FromJSON, ToJSON) +import Data.Kind import Data.Metrics.Servant import Data.Schema import Data.Singletons @@ -34,7 +35,7 @@ import Wire.API.Routes.Version -------------------------------------- -- Versioned requests -data VersionedReqBody' v (mods :: [*]) (ct :: [*]) (a :: *) +data VersionedReqBody' v (mods :: [Type]) (ct :: [Type]) (a :: Type) type VersionedReqBody v = VersionedReqBody' v '[Required, Strict] @@ -57,8 +58,7 @@ instance route _p ctx d = route (Proxy :: Proxy (ReqBody cts (Versioned v a) :> api)) ctx (fmap (. unVersioned) d) instance - ( HasSwagger (ReqBody' '[Required, Strict] cts a :> api), - S.ToSchema (Versioned v a), + ( S.ToSchema (Versioned v a), HasSwagger api, AllAccept cts ) => @@ -69,7 +69,7 @@ instance -------------------------------------------------------------------------------- -- Versioned responses -data VersionedRespond v (s :: Nat) (desc :: Symbol) (a :: *) +data VersionedRespond v (s :: Nat) (desc :: Symbol) (a :: Type) type instance ResponseType (VersionedRespond v s desc a) = a diff --git a/libs/wire-api/src/Wire/API/Swagger.hs b/libs/wire-api/src/Wire/API/Swagger.hs deleted file mode 100644 index 3b757e1548..0000000000 --- a/libs/wire-api/src/Wire/API/Swagger.hs +++ /dev/null @@ -1,109 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Wire.API.Swagger where - -import Data.Swagger.Build.Api (Model) -import qualified Wire.API.Connection as Connection -import qualified Wire.API.Conversation as Conversation -import qualified Wire.API.Conversation.Code as Conversation.Code -import qualified Wire.API.Conversation.Member as Conversation.Member -import qualified Wire.API.Conversation.Role as Conversation.Role -import qualified Wire.API.Conversation.Typing as Conversation.Typing -import qualified Wire.API.Event.Team as Event.Team -import qualified Wire.API.Message as Message -import qualified Wire.API.Notification as Notification -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.Permission as Team.Permission -import qualified Wire.API.User as User -import qualified Wire.API.User.Client as User.Client -import qualified Wire.API.User.Client.Prekey as User.Client.Prekey -import qualified Wire.API.User.Handle as User.Handle -import qualified Wire.API.User.Profile as User.Profile -import qualified Wire.API.User.RichInfo as User.RichInfo -import qualified Wire.API.User.Search as User.Search - -models :: [Model] -models = - [ Connection.modelConnectionList, - Connection.modelConnection, - Connection.modelConnectionUpdate, - Conversation.modelConversation, - Conversation.modelConversations, - Conversation.modelConversationIds, - Conversation.modelInvite, - Conversation.modelNewConversation, - Conversation.modelTeamInfo, - Conversation.modelConversationUpdateName, - Conversation.modelConversationAccessData, - Conversation.modelConversationReceiptModeUpdate, - Conversation.modelConversationMessageTimerUpdate, - Conversation.Code.modelConversationCode, - Conversation.Member.modelConversationMembers, - Conversation.Member.modelOtherMember, - Conversation.Member.modelMember, - Conversation.Member.modelMemberUpdate, - Conversation.Member.modelOtherMemberUpdate, - Conversation.Role.modelConversationRole, - Conversation.Role.modelConversationRolesList, - Conversation.Typing.modelTyping, - Event.Team.modelEvent, - Event.Team.modelMemberEvent, - Event.Team.modelMemberData, - Event.Team.modelConvEvent, - Event.Team.modelConversationData, - Event.Team.modelUpdateEvent, - Message.modelNewOtrMessage, - Message.modelOtrRecipients, - Message.modelClientMismatch, - Notification.modelEvent, - Notification.modelNotification, - Notification.modelNotificationList, - Properties.modelPropertyValue, - Properties.modelPropertyDictionary, - Provider.Service.modelServiceRef, - Team.modelTeam, - Team.modelTeamList, - Team.modelTeamDelete, - Team.Conversation.modelTeamConversation, - Team.Conversation.modelTeamConversationList, - Team.Permission.modelPermissions, - User.modelUserIdList, - User.modelUser, - User.modelEmailUpdate, - User.modelDelete, - User.Client.modelOtrClientMap, - User.Client.modelUserClients, - User.Client.modelNewClient, - User.Client.modelUpdateClient, - User.Client.modelDeleteClient, - User.Client.modelSigkeys, - User.Client.modelLocation, -- re-export from types-common - User.Client.Prekey.modelPrekey, - User.Handle.modelUserHandleInfo, - User.Handle.modelCheckHandles, - User.Profile.modelAsset, - User.RichInfo.modelRichInfo, - User.RichInfo.modelRichField, - User.Search.modelSearchResult User.Search.modelSearchContact, - User.Search.modelSearchResult User.Search.modelTeamContact, - User.Search.modelSearchContact, - User.Search.modelTeamContact - ] diff --git a/libs/wire-api/src/Wire/API/SwaggerServant.hs b/libs/wire-api/src/Wire/API/SwaggerServant.hs new file mode 100644 index 0000000000..89973fb59a --- /dev/null +++ b/libs/wire-api/src/Wire/API/SwaggerServant.hs @@ -0,0 +1,48 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 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 . + +-- | Servant combinators related to Swagger docs +module Wire.API.SwaggerServant + ( OmitDocs, + ) +where + +import Data.Metrics.Servant +import Data.Proxy +import Imports hiding (head) +import Servant +import Servant.Swagger (HasSwagger (toSwagger)) + +-- | A type-level tag that lets us omit any branch from Swagger docs. +-- +-- FUTUREWORK(fisx): this is currently only used for the spar internal api +-- and spar scim, and we should probably eliminate those uses and this combinator. +-- it's only justification is laziness. +data OmitDocs + +instance HasSwagger (OmitDocs :> a) where + toSwagger _ = mempty + +instance HasServer api ctx => HasServer (OmitDocs :> api) ctx where + type ServerT (OmitDocs :> api) m = ServerT api m + + route _ = route (Proxy :: Proxy api) + hoistServerWithContext _ pc nt s = + hoistServerWithContext (Proxy :: Proxy api) pc nt s + +instance RoutesToPaths api => RoutesToPaths (OmitDocs :> api) where + getRoutes = getRoutes @api diff --git a/libs/wire-api/src/Wire/API/SystemSettings.hs b/libs/wire-api/src/Wire/API/SystemSettings.hs index 179470f23f..dbd973a80a 100644 --- a/libs/wire-api/src/Wire/API/SystemSettings.hs +++ b/libs/wire-api/src/Wire/API/SystemSettings.hs @@ -1,10 +1,27 @@ +-- 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.SystemSettings where import Control.Lens hiding ((.=)) import qualified Data.Aeson as A import Data.Schema as Schema import qualified Data.Swagger as S -import Imports hiding (head) +import Imports import Servant.Swagger.Internal.Orphans () import Test.QuickCheck import Wire.Arbitrary @@ -13,19 +30,49 @@ import Wire.Arbitrary -- -- Used to expose settings via the @/system/settings/unauthorized@ endpoint. -- ALWAYS CHECK WITH SECURITY IF YOU WANT TO ADD SETTINGS HERE. +data SystemSettingsPublic = SystemSettingsPublic + { sspSetRestrictUserCreation :: !Bool + } + deriving (Eq, Show, Generic) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema SystemSettingsPublic + deriving (Arbitrary) via (GenericUniform SystemSettingsPublic) + +instance ToSchema SystemSettingsPublic where + schema = + object "SystemSettingsPublic" $ settingsPublicObjectSchema + +settingsPublicObjectSchema :: ObjectSchema SwaggerDoc SystemSettingsPublic +settingsPublicObjectSchema = + SystemSettingsPublic + <$> sspSetRestrictUserCreation .= fieldWithDocModifier "setRestrictUserCreation" (description ?~ "Do not allow certain user creation flows") schema + +data SystemSettingsInternal = SystemSettingsInternal + { ssiSetEnableMls :: !Bool + } + deriving (Eq, Show, Generic) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema SystemSettingsInternal + deriving (Arbitrary) via (GenericUniform SystemSettingsInternal) + +instance ToSchema SystemSettingsInternal where + schema = + object "SystemSettingsInternal" $ settingsInternalObjectSchema + +settingsInternalObjectSchema :: ObjectSchema SwaggerDoc SystemSettingsInternal +settingsInternalObjectSchema = + SystemSettingsInternal + <$> ssiSetEnableMls .= fieldWithDocModifier "setEnableMls" (description ?~ "Whether MLS is enabled or not") schema + data SystemSettings = SystemSettings - { systemSettingsSetRestrictUserCreation :: !Bool + { ssPublic :: !SystemSettingsPublic, + ssInternal :: !SystemSettingsInternal } deriving (Eq, Show, Generic) - deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema.Schema SystemSettings + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema SystemSettings deriving (Arbitrary) via (GenericUniform SystemSettings) -instance Schema.ToSchema SystemSettings where +instance ToSchema SystemSettings where schema = - Schema.object "SystemSettings" $ + object "SystemSettings" $ SystemSettings - <$> systemSettingsSetRestrictUserCreation - Schema..= Schema.fieldWithDocModifier - "setRestrictUserCreation" - (description ?~ "Do not allow certain user creation flows") - Schema.schema + <$> ssPublic .= settingsPublicObjectSchema + <*> ssInternal .= settingsInternalObjectSchema diff --git a/libs/wire-api/src/Wire/API/Team.hs b/libs/wire-api/src/Wire/API/Team.hs index de679ad470..adae16e742 100644 --- a/libs/wire-api/src/Wire/API/Team.hs +++ b/libs/wire-api/src/Wire/API/Team.hs @@ -64,12 +64,6 @@ module Wire.API.Team newTeamDeleteData, tdAuthPassword, tdVerificationCode, - - -- * Swagger - modelTeam, - modelTeamList, - modelUpdateData, - modelTeamDelete, ) where @@ -85,7 +79,6 @@ import Data.Misc (PlainTextPassword (..)) import Data.Range import Data.Schema import qualified Data.Swagger as S -import qualified Data.Swagger.Build.Api as Doc import qualified Data.Text.Encoding as T import Imports import Test.QuickCheck.Gen (suchThat) @@ -112,26 +105,6 @@ data Team = Team newTeam :: TeamId -> UserId -> Text -> Icon -> TeamBinding -> Team newTeam tid uid nme ico tb = Team tid uid nme ico Nothing tb DefaultIcon -modelTeam :: Doc.Model -modelTeam = Doc.defineModel "Team" $ do - Doc.description "Team information" - Doc.property "id" Doc.bytes' $ - Doc.description "team ID" - Doc.property "creator" Doc.bytes' $ - Doc.description "team creator's user ID" - Doc.property "name" Doc.string' $ - Doc.description "team name" - Doc.property "icon" Doc.string' $ - Doc.description "team icon (asset ID)" - Doc.property "icon_key" Doc.string' $ do - Doc.description "team icon asset key" - Doc.optional - Doc.property "binding" Doc.bool' $ - Doc.description "user binding team" - Doc.property "splash_screen" Doc.string' $ do - Doc.description "new splash screen asset key" - Doc.optional - instance ToSchema Team where schema = object "Team" $ @@ -144,6 +117,22 @@ instance ToSchema Team where <*> _teamBinding .= (fromMaybe Binding <$> optField "binding" schema) <*> _teamSplashScreen .= (fromMaybe DefaultIcon <$> optField "splash_screen" schema) +-- | How a team "binds" its members (users) +-- +-- A `Binding` team is the normal team which we see in the UI. A user is +-- on-boarded as part of the team. If the team gets deleted/suspended the user +-- gets deleted/suspended. +-- +-- A `NonBinding` team is a concept only in the backend. It is a team someone +-- can create and someone who has an account on Wire can join that team. This +-- way, in theory, one person can join many teams. This concept never made it as +-- a concept of product, but got used a lot of writing integration tests. Newer +-- features don't really work well with this and sometimes we have to rewrite +-- parts of the tests to use `Binding` teams. +-- +-- Please try to not use `NonBinding` teams in tests anymore. In future, we +-- would like it to be deleted, but it is hard to delete because it requires a +-- bunch of tests to be rewritten. data TeamBinding = Binding | NonBinding @@ -170,14 +159,6 @@ data TeamList = TeamList newTeamList :: [Team] -> Bool -> TeamList newTeamList = TeamList -modelTeamList :: Doc.Model -modelTeamList = Doc.defineModel "TeamList" $ do - Doc.description "list of teams" - Doc.property "teams" (Doc.unique $ Doc.array (Doc.ref modelTeam)) $ - Doc.description "the Doc.array of teams" - Doc.property "has_more" Doc.bool' $ - Doc.description "if more teams are available" - instance ToSchema TeamList where schema = object "TeamList" $ @@ -285,22 +266,6 @@ instance Arbitrary TeamUpdateData where valid (TeamUpdateData Nothing Nothing Nothing Nothing) = False valid _ = True -modelUpdateData :: Doc.Model -modelUpdateData = Doc.defineModel "TeamUpdateData" $ do - Doc.description "team update data" - Doc.property "name" Doc.string' $ do - Doc.description "new team name" - Doc.optional - Doc.property "icon" Doc.string' $ do - Doc.description "new icon asset id" - Doc.optional - Doc.property "icon_key" Doc.string' $ do - Doc.description "new icon asset key" - Doc.optional - Doc.property "splash_screen" Doc.string' $ do - Doc.description "new splash screen asset key" - Doc.optional - newTeamUpdateData :: TeamUpdateData newTeamUpdateData = TeamUpdateData Nothing Nothing Nothing Nothing @@ -340,15 +305,6 @@ newTeamDeleteData = flip TeamDeleteData Nothing newTeamDeleteDataWithCode :: Maybe PlainTextPassword -> Maybe Code.Value -> TeamDeleteData newTeamDeleteDataWithCode = TeamDeleteData --- FUTUREWORK: fix name of model? (upper case) -modelTeamDelete :: Doc.Model -modelTeamDelete = Doc.defineModel "teamDeleteData" $ do - Doc.description "Data for a team deletion request in case of binding teams." - Doc.property "password" Doc.string' $ - Doc.description "The account password to authorise the deletion." - Doc.property "verification_code" Doc.string' $ - Doc.description "The verification code to authorise the deletion." - instance ToSchema TeamDeleteData where schema = object "TeamDeleteData" $ diff --git a/libs/wire-api/src/Wire/API/Team/Conversation.hs b/libs/wire-api/src/Wire/API/Team/Conversation.hs index 207ba65422..5d1792bac5 100644 --- a/libs/wire-api/src/Wire/API/Team/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Team/Conversation.hs @@ -29,10 +29,6 @@ module Wire.API.Team.Conversation TeamConversationList, newTeamConversationList, teamConversations, - - -- * Swagger - modelTeamConversation, - modelTeamConversationList, ) where @@ -41,7 +37,6 @@ import qualified Data.Aeson as A import Data.Id (ConvId) import Data.Schema import qualified Data.Swagger as S -import qualified Data.Swagger.Build.Api as Doc import Imports import Wire.Arbitrary (Arbitrary, GenericUniform (..)) @@ -79,14 +74,6 @@ instance ToSchema TeamConversation where newTeamConversation :: ConvId -> TeamConversation newTeamConversation = TeamConversation -modelTeamConversation :: Doc.Model -modelTeamConversation = Doc.defineModel "TeamConversation" $ do - Doc.description "team conversation data" - Doc.property "conversation" Doc.bytes' $ - Doc.description "conversation ID" - Doc.property "managed" Doc.bytes' $ - Doc.description managedDesc - -------------------------------------------------------------------------------- -- TeamConversationList @@ -109,11 +96,5 @@ instance ToSchema TeamConversationList where newTeamConversationList :: [TeamConversation] -> TeamConversationList newTeamConversationList = TeamConversationList -modelTeamConversationList :: Doc.Model -modelTeamConversationList = Doc.defineModel "TeamConversationListList" $ do - Doc.description "list of team conversations" - Doc.property "conversations" (Doc.unique $ Doc.array (Doc.ref modelTeamConversation)) $ - Doc.description "the array of team conversations" - makeLenses ''TeamConversation makeLenses ''TeamConversationList diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index e9bbd67aab..5aa29a23a9 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -35,6 +35,8 @@ module Wire.API.Team.Feature setStatus, setLockStatus, setConfig, + setConfig', + setTTL, setWsTTL, WithStatusPatch, wsPatch, @@ -75,8 +77,9 @@ module Wire.API.Team.Feature AppLockConfig (..), FileSharingConfig (..), MLSConfig (..), + OutlookCalIntegrationConfig (..), + MlsE2EIdConfig (..), AllFeatureConfigs (..), - typeFeatureTTL, unImplicitLockStatus, ImplicitLockStatus (..), ) @@ -92,19 +95,22 @@ import qualified Data.ByteString.UTF8 as UTF8 import Data.Domain (Domain) import Data.Either.Extra (maybeToEither) import Data.Id +import Data.Kind import Data.Proxy import Data.Schema import Data.Scientific (toBoundedInteger) 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 qualified Data.Text.Encoding as T import qualified Data.Text.Lazy as TL +import Data.Time (UTCTime) +import Data.Time.Clock.POSIX (posixSecondsToUTCTime, utcTimeToPOSIXSeconds) import Deriving.Aeson import GHC.TypeLits import Imports import Servant (FromHttpApiData (..), ToHttpApiData (..)) +import Test.QuickCheck (oneof) import Test.QuickCheck.Arbitrary (arbitrary) import Test.QuickCheck.Gen (suchThat) import Wire.API.Conversation.Protocol (ProtocolTag (ProtocolProteusTag)) @@ -119,11 +125,11 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -- 1. Add a data type for your feature's "config" part, naming convention: -- **Config**. If your feature doesn't have a config besides -- being enabled/disabled, locked/unlocked, then the config should be a unit --- type, e.g. **data MyFeatureConfig = MyFeatureConfig**. Implement type clases +-- type, e.g. **data MyFeatureConfig = MyFeatureConfig**. Implement type classes -- 'ToSchema', 'IsFeatureConfig' and 'Arbitrary'. If your feature doesn't have a -- config implement 'FeatureTrivialConfig'. -- --- 2. Add the config to to 'AllFeatureConfigs'. Add your feature to 'allFeatureModels'. +-- 2. Add the config to to 'AllFeatureConfigs'. -- -- 3. If your feature is configurable on a per-team basis, add a schema -- migration in galley and add 'FeatureStatusCassandra' instance in @@ -134,16 +140,16 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -- -- 5. Implement 'GetFeatureConfig' and 'SetFeatureConfig' in -- Galley.API.Teams.Features which defines the main business logic for getting --- and setting (with side-effects). +-- and setting (with side-effects). Note that we don't have to check the lockstatus inside 'setConfigForTeam' +-- because the lockstatus is checked in 'setFeatureStatus' before which is the public API for setting the feature status. -- --- 6. Add public routes to Routes.Public.Galley: 'FeatureStatusGet', --- 'FeatureStatusPut' (optional) and by by user: 'FeatureConfigGet'. Then --- implement them in Galley.API.Public. +-- 6. Add public routes to Wire.API.Routes.Public.Galley.Feature: 'FeatureStatusGet', +-- 'FeatureStatusPut' (optional). Then implement them in Galley.API.Public.Feature. -- --- 7. Add internal routes in Galley.API.Internal +-- 7. Add internal routes in Wire.API.Routes.Internal.Galley -- -- 8. If the feature should be configurable via Stern add routes to Stern.API. --- Manually check that the swagger looks okay. +-- Manually check that the swagger looks okay and works. -- -- 9. If the feature is configured on a per-user level, see the -- 'ConferenceCallingConfig' as an example. @@ -151,6 +157,14 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -- https://github.com/wireapp/wire-server/pull/1818) -- -- 10. Extend the integration tests with cases +-- +-- 11. Edit/update the configurations: +-- - optionally add the config for local integration tests to 'galley.integration.yaml' +-- - add a config mapping to 'charts/galley/templates/configmap.yaml' +-- - add the defaults to 'charts/galley/values.yaml' +-- - optionally add config for CI to 'hack/helm_vars/wire-server/values.yaml' +-- +-- 12. Add a section to the documentation at an appropriate place (e.g. 'docs/src/developer/reference/config-options.md' or 'docs/src/understand/team-feature-settings.md') class IsFeatureConfig cfg where type FeatureSymbol cfg :: Symbol defFeatureStatus :: WithStatus cfg @@ -167,16 +181,16 @@ class FeatureTrivialConfig cfg where class HasDeprecatedFeatureName cfg where type DeprecatedFeatureName cfg :: Symbol -featureName :: forall cfg. (IsFeatureConfig cfg, KnownSymbol (FeatureSymbol cfg)) => Text +featureName :: forall cfg. KnownSymbol (FeatureSymbol cfg) => Text featureName = T.pack $ symbolVal (Proxy @(FeatureSymbol cfg)) -featureNameBS :: forall cfg. (IsFeatureConfig cfg, KnownSymbol (FeatureSymbol cfg)) => ByteString +featureNameBS :: forall cfg. KnownSymbol (FeatureSymbol cfg) => ByteString featureNameBS = UTF8.fromString $ symbolVal (Proxy @(FeatureSymbol cfg)) ---------------------------------------------------------------------- -- WithStatusBase -data WithStatusBase (m :: * -> *) (cfg :: *) = WithStatusBase +data WithStatusBase (m :: Type -> Type) (cfg :: Type) = WithStatusBase { wsbStatus :: m FeatureStatus, wsbLockStatus :: m LockStatus, wsbConfig :: m cfg, @@ -210,12 +224,18 @@ setLockStatus :: LockStatus -> WithStatus cfg -> WithStatus cfg setLockStatus ls (WithStatusBase s _ c ttl) = WithStatusBase s (Identity ls) c ttl setConfig :: cfg -> WithStatus cfg -> WithStatus cfg -setConfig c (WithStatusBase s ls _ ttl) = WithStatusBase s ls (Identity c) ttl +setConfig = setConfig' + +setConfig' :: forall (m :: Type -> Type) (cfg :: Type). Applicative m => cfg -> WithStatusBase m cfg -> WithStatusBase m cfg +setConfig' c (WithStatusBase s ls _ ttl) = WithStatusBase s ls (pure c) ttl + +setTTL :: forall (m :: Type -> Type) (cfg :: Type). Applicative m => FeatureTTL -> WithStatusBase m cfg -> WithStatusBase m cfg +setTTL ttl (WithStatusBase s ls c _) = WithStatusBase s ls c (pure ttl) setWsTTL :: FeatureTTL -> WithStatus cfg -> WithStatus cfg -setWsTTL ttl (WithStatusBase s ls c _) = WithStatusBase s ls c (Identity ttl) +setWsTTL = setTTL -type WithStatus (cfg :: *) = WithStatusBase Identity cfg +type WithStatus (cfg :: Type) = WithStatusBase Identity cfg deriving instance (Eq cfg) => Eq (WithStatus cfg) @@ -245,7 +265,7 @@ instance (Arbitrary cfg, IsFeatureConfig cfg) => Arbitrary (WithStatus cfg) wher ---------------------------------------------------------------------- -- WithStatusPatch -type WithStatusPatch (cfg :: *) = WithStatusBase Maybe cfg +type WithStatusPatch (cfg :: Type) = WithStatusBase Maybe cfg deriving instance (Eq cfg) => Eq (WithStatusPatch cfg) @@ -277,7 +297,7 @@ withStatus' = WithStatusBase -- | The ToJSON implementation of `WithStatusPatch` will encode the trivial config as `"config": {}` -- when the value is a `Just`, if it's `Nothing` it will be omitted, which is the important part. -instance (ToSchema cfg, IsFeatureConfig cfg) => ToSchema (WithStatusPatch cfg) where +instance ToSchema cfg => ToSchema (WithStatusPatch cfg) where schema = object name $ WithStatusBase @@ -303,7 +323,7 @@ instance (Arbitrary cfg, IsFeatureConfig cfg) => Arbitrary (WithStatusPatch cfg) -- if we switch to `unlocked`, we auto-enable the feature, and if we switch to locked, we -- auto-disable it. But we need to change the API to force clients to use `lockStatus` -- instead of `status`, current behavior is just wrong. -data WithStatusNoLock (cfg :: *) = WithStatusNoLock +data WithStatusNoLock (cfg :: Type) = WithStatusNoLock { wssStatus :: FeatureStatus, wssConfig :: cfg, wssTTL :: FeatureTTL @@ -439,10 +459,6 @@ instance Cass.Cql FeatureTTL where toCql FeatureTTLUnlimited = Cass.CqlInt 0 toCql (FeatureTTLSeconds d) = Cass.CqlInt . fromIntegral $ d -typeFeatureTTL :: Doc.DataType -typeFeatureTTL = - Doc.int64' - invalidTTLErrorString :: Text invalidTTLErrorString = "Invalid FeatureTTLSeconds: must be a positive integer or 'unlimited.'" @@ -464,6 +480,8 @@ instance ToSchema LockStatus where element "unlocked" LockStatusUnlocked ] +instance S.ToParamSchema LockStatus + instance ToByteString LockStatus where builder LockStatusLocked = "locked" builder LockStatusUnlocked = "unlocked" @@ -500,7 +518,7 @@ instance ToSchema LockStatusResponse where LockStatusResponse <$> _unlockStatus .= field "lockStatus" schema -newtype ImplicitLockStatus (cfg :: *) = ImplicitLockStatus {_unImplicitLockStatus :: WithStatus cfg} +newtype ImplicitLockStatus (cfg :: Type) = ImplicitLockStatus {_unImplicitLockStatus :: WithStatus cfg} deriving newtype (Eq, Show, Arbitrary) instance (IsFeatureConfig a, ToSchema a) => ToJSON (ImplicitLockStatus a) where @@ -684,6 +702,7 @@ instance FeatureTrivialConfig SndFactorPasswordChallengeConfig where data SearchVisibilityInboundConfig = SearchVisibilityInboundConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform SearchVisibilityInboundConfig) + deriving (S.ToSchema) via Schema SearchVisibilityInboundConfig instance IsFeatureConfig SearchVisibilityInboundConfig where type FeatureSymbol SearchVisibilityInboundConfig = "searchVisibilityInbound" @@ -851,6 +870,59 @@ instance ToSchema ExposeInvitationURLsToTeamAdminConfig where instance FeatureTrivialConfig ExposeInvitationURLsToTeamAdminConfig where trivialConfig = ExposeInvitationURLsToTeamAdminConfig +---------------------------------------------------------------------- +-- OutlookCalIntegrationConfig + +-- | This feature setting only applies to the Outlook Calendar extension for Wire. +-- As it is an external service, it should only be configured through this feature flag and otherwise ignored by the backend. +data OutlookCalIntegrationConfig = OutlookCalIntegrationConfig + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform OutlookCalIntegrationConfig) + +instance IsFeatureConfig OutlookCalIntegrationConfig where + type FeatureSymbol OutlookCalIntegrationConfig = "outlookCalIntegration" + defFeatureStatus = withStatus FeatureStatusDisabled LockStatusLocked OutlookCalIntegrationConfig FeatureTTLUnlimited + objectSchema = pure OutlookCalIntegrationConfig + +instance ToSchema OutlookCalIntegrationConfig where + schema = object "OutlookCalIntegrationConfig" objectSchema + +instance FeatureTrivialConfig OutlookCalIntegrationConfig where + trivialConfig = OutlookCalIntegrationConfig + +---------------------------------------------------------------------- +-- MlsE2EId + +data MlsE2EIdConfig = MlsE2EIdConfig {verificationExpiration :: Maybe UTCTime} + deriving stock (Eq, Show, Generic) + +instance Arbitrary MlsE2EIdConfig where + arbitrary = oneof [pure $ MlsE2EIdConfig Nothing, MlsE2EIdConfig . Just . posixSecondsToUTCTime . fromIntegral <$> (arbitrary @Word32)] + +instance ToSchema MlsE2EIdConfig where + schema :: ValueSchema NamedSwaggerDoc MlsE2EIdConfig + schema = + object "MlsE2EIdConfig" $ + MlsE2EIdConfig + <$> (fmap toSeconds . verificationExpiration) .= maybe_ (optFieldWithDocModifier "verificationExpiration" desc (fromSeconds <$> schema)) + where + fromSeconds :: Int -> UTCTime + fromSeconds = posixSecondsToUTCTime . fromIntegral + + toSeconds :: UTCTime -> Int + toSeconds = truncate . utcTimeToPOSIXSeconds + + desc :: NamedSwaggerDoc -> NamedSwaggerDoc + desc = + description + ?~ "Unix timestamp (number of seconds that have passed since 00:00:00 UTC on Thursday, 1 January 1970) after which the period for clients to verify their identity expires. \ + \When the timer goes off, they will be logged out and get the certificate automatically on their devices." + +instance IsFeatureConfig MlsE2EIdConfig where + type FeatureSymbol MlsE2EIdConfig = "mlsE2EId" + defFeatureStatus = withStatus FeatureStatusDisabled LockStatusUnlocked (MlsE2EIdConfig Nothing) FeatureTTLUnlimited + objectSchema = field "config" schema + ---------------------------------------------------------------------- -- FeatureStatus @@ -925,7 +997,9 @@ data AllFeatureConfigs = AllFeatureConfigs afcGuestLink :: WithStatus GuestLinksConfig, afcSndFactorPasswordChallenge :: WithStatus SndFactorPasswordChallengeConfig, afcMLS :: WithStatus MLSConfig, - afcExposeInvitationURLsToTeamAdmin :: WithStatus ExposeInvitationURLsToTeamAdminConfig + afcExposeInvitationURLsToTeamAdmin :: WithStatus ExposeInvitationURLsToTeamAdminConfig, + afcOutlookCalIntegration :: WithStatus OutlookCalIntegrationConfig, + afcMlsE2EId :: WithStatus MlsE2EIdConfig } deriving stock (Eq, Show) deriving (FromJSON, ToJSON, S.ToSchema) via (Schema AllFeatureConfigs) @@ -949,6 +1023,8 @@ instance ToSchema AllFeatureConfigs where <*> afcSndFactorPasswordChallenge .= featureField <*> afcMLS .= featureField <*> afcExposeInvitationURLsToTeamAdmin .= featureField + <*> afcOutlookCalIntegration .= featureField + <*> afcMlsE2EId .= featureField where featureField :: forall cfg. @@ -974,5 +1050,7 @@ instance Arbitrary AllFeatureConfigs where <*> arbitrary <*> arbitrary <*> arbitrary + <*> arbitrary + <*> arbitrary makeLenses ''ImplicitLockStatus diff --git a/libs/wire-api/src/Wire/API/Team/LegalHold.hs b/libs/wire-api/src/Wire/API/Team/LegalHold.hs index d4f93a235a..d6794ae92a 100644 --- a/libs/wire-api/src/Wire/API/Team/LegalHold.hs +++ b/libs/wire-api/src/Wire/API/Team/LegalHold.hs @@ -29,10 +29,13 @@ module Wire.API.Team.LegalHold ) where +import Control.Lens (at, (?~)) +import qualified Data.Aeson as A import qualified Data.Aeson.Types as A import Data.Id import Data.LegalHold import Data.Misc +import Data.Proxy import Data.Schema import qualified Data.Swagger as S hiding (info) import Deriving.Aeson @@ -226,3 +229,31 @@ instance ToJSON LegalholdProtectee -- {"tag":"UnprotectedBot"} -- {"tag":"LegalholdPlusFederationNotImplemented"} instance FromJSON LegalholdProtectee + +instance ToSchema LegalholdProtectee where + -- Generated mixed-sums are hard to cover: Just use their existing JSON + -- representation and add handwritten Swagger docs + schema = mkSchema docs A.parseJSON (pure . A.toJSON) + where + docs :: NamedSwaggerDoc + docs = + pure $ + S.NamedSchema (Just "LegalholdProtectee") $ + mempty + & S.type_ ?~ S.SwaggerObject + & S.properties . at "tag" + ?~ S.Inline + ( mempty + & S.type_ ?~ S.SwaggerString + & S.enum_ + ?~ [ A.toJSON ("ProtectedUser" :: String), + A.toJSON ("UnprotectedBot" :: String), + A.toJSON ("LegalholdPlusFederationNotImplemented" :: String) + ] + ) + & S.properties . at "contents" + ?~ S.Inline + ( S.toSchema (Proxy @UserId) + & S.description + ?~ "A UserId for ProtectedUser, otherwise empty / null." + ) diff --git a/libs/wire-api/src/Wire/API/Team/Member.hs b/libs/wire-api/src/Wire/API/Team/Member.hs index e573a0658e..2fc27f12d5 100644 --- a/libs/wire-api/src/Wire/API/Team/Member.hs +++ b/libs/wire-api/src/Wire/API/Team/Member.hs @@ -71,6 +71,7 @@ import Data.Aeson (FromJSON (..), ToJSON (..), Value (..)) import qualified Data.ByteString.Lazy as LBS import Data.Id (UserId) import Data.Json.Util +import Data.Kind import Data.LegalHold (UserLegalHoldStatus (..), defUserLegalHoldStatus) import Data.Misc (PlainTextPassword (..)) import Data.Proxy @@ -86,7 +87,7 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) data PermissionTag = Required | Optional -type family PermissionType (tag :: PermissionTag) = (t :: *) | t -> tag where +type family PermissionType (tag :: PermissionTag) = (t :: Type) | t -> tag where PermissionType 'Required = Permissions PermissionType 'Optional = Maybe Permissions diff --git a/libs/wire-api/src/Wire/API/Team/Permission.hs b/libs/wire-api/src/Wire/API/Team/Permission.hs index 4b5c780235..1964c4f644 100644 --- a/libs/wire-api/src/Wire/API/Team/Permission.hs +++ b/libs/wire-api/src/Wire/API/Team/Permission.hs @@ -40,9 +40,6 @@ module Wire.API.Team.Permission intToPerms, permToInt, intToPerm, - - -- * Swagger - modelPermissions, ) where @@ -53,9 +50,8 @@ import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Bits (testBit, (.|.)) import Data.Schema import qualified Data.Set as Set -import Data.Singletons.TH +import Data.Singletons.Base.TH import qualified Data.Swagger as S -import qualified Data.Swagger.Build.Api as Doc import Imports import Wire.API.Util.Aeson (CustomEncoded (..)) import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) @@ -83,17 +79,6 @@ instance ToSchema Permissions where Nothing -> fail "invalid permissions" Just ps -> pure ps -modelPermissions :: Doc.Model -modelPermissions = Doc.defineModel "Permissions" $ do - Doc.description - "Permissions constrain possible member actions.\ - \ The currently defined permissions can be found in: \ - \ https://github.com/wireapp/wire-server/blob/develop/libs/galley-types/src/Galley/Types/Teams.hs#L247" - Doc.property "self" (Doc.int64 $ Doc.min 0 . Doc.max 0x7FFFFFFFFFFFFFFF) $ - Doc.description "The permissions bitmask which applies to this user" - Doc.property "copy" (Doc.int64 $ Doc.min 0 . Doc.max 0x7FFFFFFFFFFFFFFF) $ - Doc.description "The permissions bitmask which this user can assign to others" - instance Arbitrary Permissions where arbitrary = maybe (error "instance Arbitrary Permissions") pure =<< do @@ -211,4 +196,5 @@ instance Cql.Cql Permissions where fromCql _ = Left "permissions: udt expected" $(genSingletons [''Perm]) + $(promoteShowInstances [''Perm]) diff --git a/libs/wire-api/src/Wire/API/Team/SearchVisibility.hs b/libs/wire-api/src/Wire/API/Team/SearchVisibility.hs index 175ec24b47..66f429ef51 100644 --- a/libs/wire-api/src/Wire/API/Team/SearchVisibility.hs +++ b/libs/wire-api/src/Wire/API/Team/SearchVisibility.hs @@ -20,19 +20,12 @@ module Wire.API.Team.SearchVisibility ( TeamSearchVisibility (..), TeamSearchVisibilityView (..), - - -- * Swagger - modelTeamSearchVisibility, - typeSearchVisibility, ) where import Control.Lens ((?~)) -import qualified Data.Aeson as A import Data.Schema -import Data.String.Conversions (cs) import qualified Data.Swagger as S -import qualified Data.Swagger.Build.Api as Doc import Deriving.Aeson import Imports import Wire.Arbitrary (Arbitrary, GenericUniform (..)) @@ -68,11 +61,6 @@ data TeamSearchVisibility deriving (Arbitrary) via (GenericUniform TeamSearchVisibility) deriving (ToJSON, FromJSON, S.ToSchema) via (Schema TeamSearchVisibility) -typeSearchVisibility :: Doc.DataType -typeSearchVisibility = - Doc.string . Doc.enum $ - cs . A.encode <$> [(minBound :: TeamSearchVisibility) ..] - instance ToSchema TeamSearchVisibility where schema = enum @Text @@ -90,12 +78,6 @@ newtype TeamSearchVisibilityView = TeamSearchVisibilityView TeamSearchVisibility deriving newtype (Arbitrary) deriving (ToJSON, FromJSON, S.ToSchema) via (Schema TeamSearchVisibilityView) -modelTeamSearchVisibility :: Doc.Model -modelTeamSearchVisibility = Doc.defineModel "TeamSearchVisibility" $ do - Doc.description "Search visibility value for the team" - Doc.property "search_visibility" typeSearchVisibility $ do - Doc.description "value of visibility" - instance ToSchema TeamSearchVisibilityView where schema = objectWithDocModifier "TeamSearchVisibilityView" (description ?~ "Search visibility value for the team") $ diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 08af13b115..77143b91ab 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -106,12 +106,6 @@ module Wire.API.User module Wire.API.User.Identity, module Wire.API.User.Profile, - -- * Swagger - modelDelete, - modelEmailUpdate, - modelUser, - modelUserIdList, - -- * 2nd factor auth VerificationAction (..), SendVerificationCode (..), @@ -143,14 +137,13 @@ import Data.SOP 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 T import Data.Text.Ascii import qualified Data.Text.Encoding as T import Data.UUID (UUID, nil) import qualified Data.UUID as UUID import Deriving.Swagger -import GHC.TypeLits (KnownNat, Nat) +import GHC.TypeLits import qualified Generics.SOP as GSOP import Imports import qualified SAML2.WebSSO as SAML @@ -162,7 +155,7 @@ import qualified Web.Cookie as Web import Wire.API.Error import Wire.API.Error.Brig import qualified Wire.API.Error.Brig as E -import Wire.API.Provider.Service (ServiceRef, modelServiceRef) +import Wire.API.Provider.Service (ServiceRef) import Wire.API.Routes.MultiVerb import Wire.API.Team (BindingNewTeam, bindingNewTeamObjectSchema) import Wire.API.Team.Role @@ -193,12 +186,6 @@ instance ToSchema UserIdList where <$> mUsers .= field "user_ids" (array schema) -modelUserIdList :: Doc.Model -modelUserIdList = Doc.defineModel "UserIdList" $ do - Doc.description "list of user ids" - Doc.property "user_ids" (Doc.unique $ Doc.array Doc.bytes') $ - Doc.description "the array of team conversations" - -------------------------------------------------------------------------------- -- QualifiedUserIdList @@ -226,14 +213,14 @@ newtype LimitedQualifiedUserIdList (max :: Nat) = LimitedQualifiedUserIdList deriving stock (Eq, Show, Generic) deriving (S.ToSchema) via CustomSwagger '[FieldLabelModifier CamelToSnake] (LimitedQualifiedUserIdList max) -instance (KnownNat max, LTE 1 max) => Arbitrary (LimitedQualifiedUserIdList max) where +instance (KnownNat max, 1 <= max) => Arbitrary (LimitedQualifiedUserIdList max) where arbitrary = LimitedQualifiedUserIdList <$> arbitrary -instance LTE 1 max => FromJSON (LimitedQualifiedUserIdList max) where +instance (KnownNat max, 1 <= max) => FromJSON (LimitedQualifiedUserIdList max) where parseJSON = A.withObject "LimitedQualifiedUserIdList" $ \o -> LimitedQualifiedUserIdList <$> o A..: "qualified_users" -instance LTE 1 max => ToJSON (LimitedQualifiedUserIdList max) where +instance 1 <= max => ToJSON (LimitedQualifiedUserIdList max) where toJSON e = A.object ["qualified_users" A..= qualifiedUsers e] -------------------------------------------------------------------------------- @@ -294,34 +281,6 @@ instance ToSchema UserProfile where <*> profileLegalholdStatus .= field "legalhold_status" schema -modelUser :: Doc.Model -modelUser = Doc.defineModel "User" $ do - Doc.description "User Profile" - Doc.property "id" Doc.bytes' $ - Doc.description "User ID" - Doc.property "name" Doc.string' $ - Doc.description "Name" - Doc.property "email" Doc.string' $ do - Doc.description "Email" - Doc.optional - Doc.property "assets" (Doc.array (Doc.ref modelAsset)) $ - Doc.description "Profile assets" - Doc.property "accent_id" Doc.int32' $ do - Doc.description "Accent colour ID" - Doc.optional - Doc.property "deleted" Doc.bool' $ do - Doc.description "Whether the account has been deleted." - Doc.optional - Doc.property "service" (Doc.ref modelServiceRef) $ do - Doc.description "The reference to the owning service, if the user is a 'bot'." - Doc.optional - Doc.property "handle" Doc.string' $ do - Doc.description "Unique user handle." - Doc.optional - Doc.property "team" Doc.string' $ do - Doc.description "Team ID" - Doc.optional - -------------------------------------------------------------------------------- -- SelfProfile @@ -560,7 +519,7 @@ instance Arbitrary NewUserPublic where arbitrary = arbitrary `QC.suchThatMap` (rightMay . validateNewUserPublic) data RegisterError - = RegisterErrorWhitelistError + = RegisterErrorAllowlistError | RegisterErrorInvalidInvitationCode | RegisterErrorMissingIdentity | RegisterErrorUserKeyExists @@ -578,7 +537,7 @@ data RegisterError instance GSOP.Generic RegisterError type RegisterErrorResponses = - '[ ErrorResponse 'WhitelistError, + '[ ErrorResponse 'AllowlistError, ErrorResponse 'InvalidInvitationCode, ErrorResponse 'MissingIdentity, ErrorResponse 'UserKeyExists, @@ -1239,12 +1198,6 @@ instance ToSchema EmailUpdate where <$> euEmail .= field "email" schema -modelEmailUpdate :: Doc.Model -modelEmailUpdate = Doc.defineModel "EmailUpdate" $ do - Doc.description "Email Update Data" - Doc.property "email" Doc.string' $ - Doc.description "Email" - instance ToJSON EmailUpdate where toJSON e = A.object ["email" A..= euEmail e] @@ -1389,13 +1342,6 @@ instance ToSchema DeleteUser where mkDeleteUser :: Maybe PlainTextPassword -> DeleteUser mkDeleteUser = DeleteUser -modelDelete :: Doc.Model -modelDelete = Doc.defineModel "Delete" $ do - Doc.description "Data for an account deletion request." - Doc.property "password" Doc.string' $ do - Doc.description "The account password to authorise the deletion." - Doc.optional - instance ToJSON DeleteUser where toJSON d = A.object $ diff --git a/libs/wire-api/src/Wire/API/User/Client.hs b/libs/wire-api/src/Wire/API/User/Client.hs index e8b8f0574c..230e926982 100644 --- a/libs/wire-api/src/Wire/API/User/Client.hs +++ b/libs/wire-api/src/Wire/API/User/Client.hs @@ -63,17 +63,6 @@ module Wire.API.User.Client longitude, Latitude (..), Longitude (..), - - -- * Swagger - modelOtrClientMap, - modelUserClients, - modelNewClient, - modelUpdateClient, - modelClientCapabilityList, - typeClientCapability, - modelDeleteClient, - modelSigkeys, - modelLocation, -- re-export from types-common ) where @@ -91,14 +80,12 @@ import Data.Domain (Domain) import Data.Id import Data.Json.Util import qualified Data.Map.Strict as Map -import Data.Misc (Latitude (..), Location, Longitude (..), PlainTextPassword (..), latitude, location, longitude, modelLocation) +import Data.Misc (Latitude (..), Location, Longitude (..), PlainTextPassword (..), latitude, location, longitude) import Data.Qualified import Data.Schema -import qualified Data.Semigroup as Semigroup import qualified Data.Set as Set import Data.Swagger hiding (Schema, ToSchema, schema) import qualified Data.Swagger as Swagger -import qualified Data.Swagger.Build.Api as Doc import qualified Data.Text.Encoding as Text.E import Data.UUID (toASCIIBytes) import Deriving.Swagger @@ -162,13 +149,6 @@ instance ToSchema ClientCapability where enum @Text "ClientCapability" $ element "legalhold-implicit-consent" ClientSupportsLegalholdImplicitConsent -typeClientCapability :: Doc.DataType -typeClientCapability = - Doc.string $ - Doc.enum - [ "legalhold-implicit-consent" - ] - instance Cql.Cql ClientCapability where ctype = Cql.Tagged Cql.IntColumn @@ -203,12 +183,6 @@ capabilitiesFieldSchema = ?~ "Hints provided by the client for the backend so it can \ \behave in a backwards-compatible way." -modelClientCapabilityList :: Doc.Model -modelClientCapabilityList = Doc.defineModel "ClientCapabilityList" $ do - Doc.description "Hints provided by the client for the backend so it can behave in a backwards-compatible way." - Doc.property "capabilities" (Doc.array typeClientCapability) $ do - Doc.description "Array containing all capabilities supported by a client." - -------------------------------------------------------------------------------- -- UserClientMap @@ -219,13 +193,6 @@ newtype UserClientMap a = UserClientMap deriving newtype (Semigroup, Monoid) deriving (FromJSON, ToJSON, Swagger.ToSchema) via Schema (UserClientMap a) --- FUTUREWORK: Remove when 'NewOtrMessage' has ToSchema -modelOtrClientMap :: Doc.Model -modelOtrClientMap = Doc.defineModel "OtrClientMap" $ do - Doc.description "Map of client IDs to OTR content." - Doc.property "" Doc.bytes' $ - Doc.description "Mapping from client IDs to OTR content (Base64 in JSON)." - instance ToSchema a => ToSchema (UserClientMap a) where schema = userClientMapSchema schema @@ -316,7 +283,7 @@ newtype QualifiedUserClientPrekeyMap = QualifiedUserClientPrekeyMap deriving stock (Eq, Show) deriving newtype (Arbitrary) deriving (FromJSON, ToJSON, Swagger.ToSchema) via Schema QualifiedUserClientPrekeyMap - deriving (Semigroup, Monoid) via (QualifiedUserClientMap (Semigroup.Option (Semigroup.First Prekey))) + deriving (Semigroup, Monoid) via (QualifiedUserClientMap (Alt Maybe Prekey)) instance ToSchema QualifiedUserClientPrekeyMap where schema = @@ -409,14 +376,6 @@ instance ToSchema UserClients where ] ) --- FUTUREWORK: Remove when proto endpoint for sending messages is moved to --- servant -modelUserClients :: Doc.Model -modelUserClients = - Doc.defineModel "UserClients" $ - Doc.property "" (Doc.unique $ Doc.array Doc.bytes') $ - Doc.description "Map of user IDs to sets of client IDs ({ UserId: [ClientId] })." - instance Arbitrary UserClients where arbitrary = UserClients <$> mapOf' arbitrary (setOf' arbitrary) @@ -572,15 +531,6 @@ instance ToSchema ClientType where <> element "permanent" PermanentClientType <> element "legalhold" LegalHoldClientType -typeClientType :: Doc.DataType -typeClientType = - Doc.string $ - Doc.enum - [ "permanent", - "temporary", - "legalhold" - ] - data ClientClass = PhoneClient | TabletClient @@ -598,16 +548,6 @@ instance ToSchema ClientClass where <> element "desktop" DesktopClient <> element "legalhold" LegalHoldClient -typeClientClass :: Doc.DataType -typeClientClass = - Doc.string $ - Doc.enum - [ "phone", - "tablet", - "desktop", - "legalhold" - ] - -------------------------------------------------------------------------------- -- NewClient @@ -628,45 +568,6 @@ data NewClient = NewClient deriving (Arbitrary) via (GenericUniform NewClient) deriving (FromJSON, ToJSON, Swagger.ToSchema) via Schema NewClient -modelNewClient :: Doc.Model -modelNewClient = Doc.defineModel "NewClient" $ do - Doc.description "The registration data for a new client." - Doc.property "type" typeClientType $ - Doc.description - "The type of client to register. A user may have no more than \ - \7 (seven) permanent clients and 1 (one) temporary client. When the \ - \limit of permanent clients is reached, an error is returned. \ - \When a temporary client already exists, it is replaced." - Doc.property "password" Doc.string' $ do - Doc.description - "The password of the authenticated user for verification. \ - \Note: Required for registration of the 2nd, 3rd, ... client." - Doc.optional - Doc.property "prekeys" (Doc.array (Doc.ref modelPrekey)) $ - Doc.description "Prekeys for other clients to establish OTR sessions." - Doc.property "lastkey" (Doc.ref modelPrekey) $ - Doc.description - "The last resort prekey for other clients to establish OTR sessions. \ - \This key must have the ID 0xFFFF and is never deleted." - -- FUTUREWORK: sigkeys don't seem to be used anymore - Doc.property "sigkeys" (Doc.ref modelSigkeys) $ - Doc.description - "The signaling keys to use for encryption and signing of OTR native push \ - \notifications (APNS, GCM)." - Doc.property "label" Doc.string' $ do - Doc.description "An optional label to associate with the client." - Doc.optional - Doc.property "class" typeClientClass $ - Doc.description "The device class this client belongs to. Either 'phone', 'tablet', or 'desktop'." - Doc.property "cookie" Doc.string' $ - Doc.description "The cookie label, i.e. the label used when logging in." - Doc.property "model" Doc.string' $ do - Doc.description "Optional model information of this client" - Doc.optional - Doc.property "capabilities" typeClientCapability $ do - Doc.description "Hints for the backend so it can behave in a backwards-compatible way." - Doc.optional - instance ToSchema NewClient where schema = object "NewClient" $ @@ -796,28 +697,6 @@ instance ToSchema UpdateClient where <*> updateClientCapabilities .= maybe_ capabilitiesFieldSchema <*> updateClientMLSPublicKeys .= mlsPublicKeysFieldSchema -modelUpdateClient :: Doc.Model -modelUpdateClient = Doc.defineModel "UpdateClient" $ do - Doc.description "The new data for the registered client." - Doc.property "prekeys" (Doc.array (Doc.ref modelPrekey)) $ do - Doc.description "New prekeys for other clients to establish OTR sessions." - Doc.optional - Doc.property "lastkey" (Doc.ref modelPrekey) $ do - Doc.description "New last-resort prekey." - Doc.optional - -- FUTUREWORK: sigkeys don't seem to be used anymore, remove? - Doc.property "sigkeys" (Doc.ref modelSigkeys) $ do - Doc.description - "New signaling keys to use for encryption and signing of OTR native push \ - \notifications (APNS, GCM)." - Doc.optional - Doc.property "label" Doc.string' $ do - Doc.description "A new name for this client." - Doc.optional - Doc.property "capabilities" typeClientCapability $ do - Doc.description "Hints for the backend so it can behave in a backwards-compatible way." - Doc.optional - -------------------------------------------------------------------------------- -- RmClient @@ -840,23 +719,3 @@ instance ToSchema RmClient where \The password is not required for deleting temporary clients." ) (maybeWithDefault A.Null schema) - -modelDeleteClient :: Doc.Model -modelDeleteClient = Doc.defineModel "DeleteClient" $ do - Doc.description "Required information for client deletion." - Doc.property "password" Doc.string' $ do - Doc.description - "The password of the authenticated user for verification. \ - \The password is not required for deleting temporary clients." - Doc.optional - --------------------------------------------------------------------------------- --- other models - -modelSigkeys :: Doc.Model -modelSigkeys = Doc.defineModel "SignalingKeys" $ do - Doc.description "Signaling keys for encryption and signing of native push notifications (APNS, GCM)." - Doc.property "enckey" Doc.bytes' $ - Doc.description "The base64-encoded, 256 bit encryption key." - Doc.property "mackey" Doc.bytes' $ - Doc.description "The base64-encoded, 256 bit MAC key." diff --git a/libs/wire-api/src/Wire/API/User/Client/Prekey.hs b/libs/wire-api/src/Wire/API/User/Client/Prekey.hs index cf084c65d8..8da8d062a0 100644 --- a/libs/wire-api/src/Wire/API/User/Client/Prekey.hs +++ b/libs/wire-api/src/Wire/API/User/Client/Prekey.hs @@ -29,9 +29,6 @@ module Wire.API.User.Client.Prekey lastPrekeyId, PrekeyBundle (..), ClientPrekey (..), - - -- * Swagger - modelPrekey, ) where @@ -40,7 +37,6 @@ import Data.Hashable (hash) import Data.Id import Data.Schema import qualified Data.Swagger as S -import qualified Data.Swagger.Build.Api as Doc import Imports import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) @@ -59,15 +55,6 @@ data Prekey = Prekey deriving (Arbitrary) via (GenericUniform Prekey) deriving (FromJSON, ToJSON, S.ToSchema) via Schema Prekey --- FUTUREWORK: Remove when 'NewClient' has ToSchema -modelPrekey :: Doc.Model -modelPrekey = Doc.defineModel "Prekey" $ do - Doc.description "Prekey" - Doc.property "id" Doc.int32' $ - Doc.description "Prekey ID" - Doc.property "key" Doc.bytes' $ - Doc.description "Prekey data" - instance ToSchema Prekey where schema = object "Prekey" $ diff --git a/libs/wire-api/src/Wire/API/User/Handle.hs b/libs/wire-api/src/Wire/API/User/Handle.hs index 2fa2955d73..2ffc374a4c 100644 --- a/libs/wire-api/src/Wire/API/User/Handle.hs +++ b/libs/wire-api/src/Wire/API/User/Handle.hs @@ -21,10 +21,6 @@ module Wire.API.User.Handle ( UserHandleInfo (..), CheckHandles (..), - - -- * Swagger - modelUserHandleInfo, - modelCheckHandles, ) where @@ -36,7 +32,6 @@ import Data.Qualified (Qualified (..), deprecatedSchema) import Data.Range import Data.Schema import qualified Data.Swagger as S -import qualified Data.Swagger.Build.Api as Doc import Imports import Wire.Arbitrary (Arbitrary, GenericUniform (..)) @@ -48,12 +43,6 @@ newtype UserHandleInfo = UserHandleInfo {userHandleId :: Qualified UserId} deriving newtype (Arbitrary) deriving (FromJSON, ToJSON, S.ToSchema) via Schema UserHandleInfo -modelUserHandleInfo :: Doc.Model -modelUserHandleInfo = Doc.defineModel "UserHandleInfo" $ do - Doc.description "User handle info" - Doc.property "user" Doc.string' $ - Doc.description "ID of the user owning the handle" - instance ToSchema UserHandleInfo where schema = object "UserHandleInfo" $ @@ -76,15 +65,6 @@ data CheckHandles = CheckHandles deriving (Arbitrary) via (GenericUniform CheckHandles) deriving (S.ToSchema) via Schema CheckHandles -modelCheckHandles :: Doc.Model -modelCheckHandles = Doc.defineModel "CheckHandles" $ do - Doc.description "Check availability of user handles." - Doc.property "handles" (Doc.array Doc.string') $ - Doc.description "The prioritised list of handles to check (up to 50)" - Doc.property "return" Doc.int32' $ do - Doc.description "Desired number of free handles to return (1 - 10). Default 1." - Doc.optional - instance ToJSON CheckHandles where toJSON (CheckHandles l n) = A.object diff --git a/libs/wire-api/src/Wire/API/User/Profile.hs b/libs/wire-api/src/Wire/API/User/Profile.hs index 758444a891..864d068a36 100644 --- a/libs/wire-api/src/Wire/API/User/Profile.hs +++ b/libs/wire-api/src/Wire/API/User/Profile.hs @@ -46,10 +46,6 @@ module Wire.API.User.Profile -- * Deprecated Pict (..), noPict, - - -- * Swagger - modelAsset, - typeManagedBy, ) where @@ -65,7 +61,6 @@ import Data.LanguageCodes import Data.Range import Data.Schema import qualified Data.Swagger as S -import qualified Data.Swagger.Build.Api as Doc import qualified Data.Text as Text import Imports import Wire.API.Asset (AssetKey (..)) @@ -127,36 +122,11 @@ instance ToSchema Asset where enum @Text @NamedSwaggerDoc "AssetType" $ element "image" () -modelAsset :: Doc.Model -modelAsset = Doc.defineModel "UserAsset" $ do - Doc.description "User profile asset" - Doc.property "key" Doc.string' $ - Doc.description "The unique asset key" - Doc.property "type" typeAssetType $ - Doc.description "The asset type" - Doc.property "size" typeAssetSize $ - Doc.description "The asset size / format" - -typeAssetType :: Doc.DataType -typeAssetType = - Doc.string $ - Doc.enum - [ "image" - ] - data AssetSize = AssetComplete | AssetPreview deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform AssetSize) deriving (FromJSON, ToJSON, S.ToSchema) via Schema AssetSize -typeAssetSize :: Doc.DataType -typeAssetSize = - Doc.string $ - Doc.enum - [ "preview", - "complete" - ] - instance ToSchema AssetSize where schema = enum @Text "AssetSize" $ @@ -255,14 +225,6 @@ data ManagedBy deriving (Arbitrary) via (GenericUniform ManagedBy) deriving (ToJSON, FromJSON, S.ToSchema) via (Schema ManagedBy) -typeManagedBy :: Doc.DataType -typeManagedBy = - Doc.string $ - Doc.enum - [ "wire", - "scim" - ] - instance ToSchema ManagedBy where schema = enum @Text "ManagedBy" $ diff --git a/libs/wire-api/src/Wire/API/User/RichInfo.hs b/libs/wire-api/src/Wire/API/User/RichInfo.hs index fe133caf09..80af42e9fb 100644 --- a/libs/wire-api/src/Wire/API/User/RichInfo.hs +++ b/libs/wire-api/src/Wire/API/User/RichInfo.hs @@ -40,10 +40,6 @@ module Wire.API.User.RichInfo -- * RichField RichField (..), - - -- * Swagger - modelRichInfo, - modelRichField, ) where @@ -59,7 +55,6 @@ import qualified Data.Map as Map 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 import Imports import qualified Test.QuickCheck as QC @@ -151,15 +146,6 @@ fromRichInfoAssocList (RichInfoAssocList riList) = riList' = normalizeRichInfoAssocListInt riList riMap = Map.fromList $ (\(RichField k v) -> (k, v)) <$> riList' --- | TODO: this is model is wrong, it says nothing about the map part. -modelRichInfo :: Doc.Model -modelRichInfo = Doc.defineModel "RichInfo" $ do - Doc.description "Rich info about the user" - Doc.property "fields" (Doc.array (Doc.ref modelRichField)) $ - Doc.description "List of fields" - Doc.property "version" Doc.int32' $ - Doc.description "Format version (the current version is 0)" - instance A.ToJSON RichInfoMapAndList where toJSON u = A.object @@ -205,10 +191,10 @@ instance A.FromJSON RichInfoMapAndList where v -> Aeson.typeMismatch "A.Object or A.Array" v Just v -> Aeson.typeMismatch "A.Object" v - mapKeys :: (Eq k2, Ord k2) => (k1 -> k2) -> Map k1 v -> Map k2 v + mapKeys :: Ord k2 => (k1 -> k2) -> Map k1 v -> Map k2 v mapKeys f = Map.fromList . map (Data.Bifunctor.first f) . Map.toList - lookupOrFail :: (MonadFail m, Show k, Eq k, Ord k) => k -> Map k v -> m v + lookupOrFail :: (MonadFail m, Show k, Ord k) => k -> Map k v -> m v lookupOrFail key theMap = case Map.lookup key theMap of Nothing -> fail $ "key '" ++ show key ++ "' not found" Just v -> pure v @@ -290,14 +276,6 @@ data RichField = RichField deriving stock (Eq, Show, Generic) deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema RichField) -modelRichField :: Doc.Model -modelRichField = Doc.defineModel "RichField" $ do - Doc.description "RichInfo field" - Doc.property "type" Doc.string' $ - Doc.description "Field name" - Doc.property "value" Doc.string' $ - Doc.description "Field value" - instance ToSchema RichField where -- NB: "name" would be a better name for 'richFieldType', but "type" is used because we -- also have "type" in SCIM; and the reason we use "type" for SCIM is that @{"type": ..., diff --git a/libs/wire-api/src/Wire/API/User/Search.hs b/libs/wire-api/src/Wire/API/User/Search.hs index 2ad1da7d7d..90cddf0cea 100644 --- a/libs/wire-api/src/Wire/API/User/Search.hs +++ b/libs/wire-api/src/Wire/API/User/Search.hs @@ -30,11 +30,6 @@ module Wire.API.User.Search TeamUserSearchSortBy (..), FederatedUserSearchPolicy (..), PagingState (..), - - -- * Swagger - modelSearchResult, - modelSearchContact, - modelTeamContact, ) where @@ -54,7 +49,6 @@ import Data.Schema import Data.String.Conversions (cs) import Data.Swagger (ToParamSchema (..)) 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, toText, validateBase64Url) import Imports @@ -137,18 +131,6 @@ deriving via (Schema (SearchResult Contact)) instance S.ToSchema (SearchResult C deriving via (Schema (SearchResult TeamContact)) instance S.ToSchema (SearchResult TeamContact) -modelSearchResult :: Doc.Model -> Doc.Model -modelSearchResult modelContact = Doc.defineModel "SearchResult" $ do - Doc.description "Search Result" - Doc.property "found" Doc.int32' $ - Doc.description "Total number of hits" - Doc.property "returned" Doc.int32' $ - Doc.description "Number of hits returned" - Doc.property "took" Doc.int32' $ - Doc.description "Search time in ms" - Doc.property "documents" (Doc.array (Doc.ref modelContact)) $ - Doc.description "List of contacts found" - -------------------------------------------------------------------------------- -- Contact @@ -165,22 +147,6 @@ data Contact = Contact deriving (Arbitrary) via (GenericUniform Contact) deriving (ToJSON, FromJSON, S.ToSchema) via Schema Contact -modelSearchContact :: Doc.Model -modelSearchContact = Doc.defineModel "Contact" $ do - Doc.description "Contact discovered through search" - Doc.property "id" Doc.string' $ - Doc.description "User ID" - Doc.property "name" Doc.string' $ - Doc.description "Name" - Doc.property "handle" Doc.string' $ - Doc.description "Handle" - Doc.property "accent_id" Doc.int32' $ do - Doc.description "Accent color" - Doc.optional - Doc.property "team" Doc.string' $ do - Doc.description "Team ID" - Doc.optional - instance ToSchema Contact where schema = objectWithDocModifier "Contact" (description ?~ "Contact discovered through search") $ @@ -231,41 +197,6 @@ data TeamContact = TeamContact deriving (Arbitrary) via (GenericUniform TeamContact) deriving (ToJSON, FromJSON) via (Schema TeamContact) -modelSso :: Doc.Model -modelSso = Doc.defineModel "Sso" $ do - Doc.description "Single Sign-On" - Doc.property "issuer" Doc.string' $ - Doc.description "Issuer" - Doc.property "nameid" Doc.string' $ - Doc.description "Name ID" - -modelTeamContact :: Doc.Model -modelTeamContact = Doc.defineModel "TeamContact" $ do - Doc.description "Contact discovered through search" - Doc.property "id" Doc.string' $ - Doc.description "User ID" - Doc.property "name" Doc.string' $ - Doc.description "Name" - Doc.property "handle" Doc.string' $ - Doc.description "Handle" - Doc.property "accent_id" Doc.int32' $ do - Doc.description "Accent color" - Doc.optional - Doc.property "team" Doc.string' $ do - Doc.description "Team ID" - Doc.optional - Doc.property "email" Doc.string' $ do - Doc.description "Email address" - Doc.optional - Doc.property "scim_external_id" Doc.string' $ do - Doc.description "SCIM external ID" - Doc.optional - Doc.property "sso" (Doc.ref modelSso) $ do - Doc.description "Single-Sign-On information" - Doc.property "email_unvalidated" Doc.string' $ do - Doc.description "Unvalidated email address" - Doc.optional - instance ToSchema TeamContact where schema = object "TeamContact" $ diff --git a/libs/wire-api/src/Wire/API/VersionInfo.hs b/libs/wire-api/src/Wire/API/VersionInfo.hs index 0d9707b5e0..7809b0411f 100644 --- a/libs/wire-api/src/Wire/API/VersionInfo.hs +++ b/libs/wire-api/src/Wire/API/VersionInfo.hs @@ -118,7 +118,7 @@ instance clientWithRoute pm (Proxy @api) req hoistClientMonad pm _ f = hoistClientMonad pm (Proxy @api) f -instance HasSwagger api => HasSwagger (Until v :> api) where +instance HasSwagger (Until v :> api) where toSwagger _ = mempty instance RoutesToPaths api => RoutesToPaths (Until v :> api) where 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 06f9b0b979..ef53079d9b 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 @@ -1278,6 +1278,10 @@ tests = testObjects [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_17, "testObject_WithStatus_team_17.json") ], + testGroup "Golden: WithStatus_team 12" $ + testObjects + [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_18, "testObject_WithStatus_team_18.json") + ], testGroup "Golden: InvitationRequest_team" $ testObjects [(Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_1, "testObject_InvitationRequest_team_1.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_2, "testObject_InvitationRequest_team_2.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_3, "testObject_InvitationRequest_team_3.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_4, "testObject_InvitationRequest_team_4.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_5, "testObject_InvitationRequest_team_5.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_6, "testObject_InvitationRequest_team_6.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_7, "testObject_InvitationRequest_team_7.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_8, "testObject_InvitationRequest_team_8.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_9, "testObject_InvitationRequest_team_9.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_10, "testObject_InvitationRequest_team_10.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_11, "testObject_InvitationRequest_team_11.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_12, "testObject_InvitationRequest_team_12.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_13, "testObject_InvitationRequest_team_13.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_14, "testObject_InvitationRequest_team_14.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_15, "testObject_InvitationRequest_team_15.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_16, "testObject_InvitationRequest_team_16.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_17, "testObject_InvitationRequest_team_17.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_18, "testObject_InvitationRequest_team_18.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_19, "testObject_InvitationRequest_team_19.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_20, "testObject_InvitationRequest_team_20.json")], testGroup "Golden: Invitation_team" $ diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewConv_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewConv_user.hs index 571060d758..cbd89a8260 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewConv_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewConv_user.hs @@ -52,8 +52,7 @@ testObject_NewConv_user_1 = newConvMessageTimer = Just (Ms {ms = 3320987366258987}), newConvReceiptMode = Just (ReceiptMode {unReceiptMode = 1}), newConvUsersRole = fromJust (parseRoleName "8tp2gs7b6"), - newConvProtocol = ProtocolProteusTag, - newConvCreatorClient = Nothing + newConvProtocol = ProtocolProteusTag } testObject_NewConv_user_3 :: NewConv @@ -72,6 +71,5 @@ testObject_NewConv_user_3 = ( parseRoleName "y3otpiwu615lvvccxsq0315jj75jquw01flhtuf49t6mzfurvwe3_sh51f4s257e2x47zo85rif_xyiyfldpan3g4r6zr35rbwnzm0k" ), - newConvProtocol = ProtocolMLSTag, - newConvCreatorClient = Just (ClientId "beef") + newConvProtocol = ProtocolMLSTag } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatus_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatus_team.hs index 0b100dc130..8d4b48f5a1 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatus_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatus_team.hs @@ -20,6 +20,7 @@ module Test.Wire.API.Golden.Generated.WithStatus_team where import Data.Domain +import Data.Time.Clock.POSIX (posixSecondsToUTCTime) import Imports import Wire.API.Team.Feature hiding (withStatus) import qualified Wire.API.Team.Feature as F @@ -75,5 +76,8 @@ testObject_WithStatus_team_16 = withStatus FeatureStatusDisabled LockStatusUnloc testObject_WithStatus_team_17 :: WithStatus SearchVisibilityInboundConfig testObject_WithStatus_team_17 = withStatus FeatureStatusEnabled LockStatusUnlocked SearchVisibilityInboundConfig +testObject_WithStatus_team_18 :: WithStatus MlsE2EIdConfig +testObject_WithStatus_team_18 = withStatus FeatureStatusEnabled LockStatusLocked (MlsE2EIdConfig (Just (posixSecondsToUTCTime 1676377048))) + withStatus :: FeatureStatus -> LockStatus -> cfg -> WithStatus cfg withStatus fs ls cfg = F.withStatus fs ls cfg FeatureTTLUnlimited diff --git a/libs/wire-api/test/golden/testObject_NewConv_user_3.json b/libs/wire-api/test/golden/testObject_NewConv_user_3.json index 16db2cc149..112404a376 100644 --- a/libs/wire-api/test/golden/testObject_NewConv_user_3.json +++ b/libs/wire-api/test/golden/testObject_NewConv_user_3.json @@ -9,7 +9,6 @@ "guest" ], "conversation_role": "y3otpiwu615lvvccxsq0315jj75jquw01flhtuf49t6mzfurvwe3_sh51f4s257e2x47zo85rif_xyiyfldpan3g4r6zr35rbwnzm0k", - "creator_client": "beef", "protocol": "mls", "qualified_users": [], "users": [] diff --git a/libs/wire-api/test/golden/testObject_NewConv_v2_user_3.json b/libs/wire-api/test/golden/testObject_NewConv_v2_user_3.json index 799071f5b3..f4db13e892 100644 --- a/libs/wire-api/test/golden/testObject_NewConv_v2_user_3.json +++ b/libs/wire-api/test/golden/testObject_NewConv_v2_user_3.json @@ -10,7 +10,6 @@ "guest" ], "conversation_role": "y3otpiwu615lvvccxsq0315jj75jquw01flhtuf49t6mzfurvwe3_sh51f4s257e2x47zo85rif_xyiyfldpan3g4r6zr35rbwnzm0k", - "creator_client": "beef", "protocol": "mls", "qualified_users": [], "users": [] diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_18.json b/libs/wire-api/test/golden/testObject_WithStatus_team_18.json new file mode 100644 index 0000000000..dcf13549a5 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_WithStatus_team_18.json @@ -0,0 +1,8 @@ +{ + "config": { + "verificationExpiration": 1676377048 + }, + "lockStatus": "locked", + "status": "enabled", + "ttl": "unlimited" +} diff --git a/libs/wire-api/test/unit/Main.hs b/libs/wire-api/test/unit/Main.hs index 46178a1d7a..7c3c3249c7 100644 --- a/libs/wire-api/test/unit/Main.hs +++ b/libs/wire-api/test/unit/Main.hs @@ -25,12 +25,14 @@ import Test.Tasty import qualified Test.Wire.API.Call.Config as Call.Config import qualified Test.Wire.API.Conversation as Conversation import qualified Test.Wire.API.MLS as MLS +import qualified Test.Wire.API.RawJson as RawJson 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.Routes.Version as Routes.Version import qualified Test.Wire.API.Swagger as Swagger import qualified Test.Wire.API.Team.Export as Team.Export import qualified Test.Wire.API.Team.Member as Team.Member @@ -59,5 +61,7 @@ main = Roundtrip.CSV.tests, Routes.tests, Conversation.tests, - MLS.tests + MLS.tests, + Routes.Version.tests, + RawJson.tests ] diff --git a/libs/wire-api/test/unit/Test/Wire/API/MLS.hs b/libs/wire-api/test/unit/Test/Wire/API/MLS.hs index 8d623a2bb3..ea608ae122 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/MLS.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/MLS.hs @@ -290,11 +290,12 @@ spawn cp minput = do (mout, ex) <- withCreateProcess cp { std_out = CreatePipe, - std_in = if isJust minput then CreatePipe else Inherit + std_in = CreatePipe } $ \minh mouth _ ph -> - let writeInput = for_ ((,) <$> minput <*> minh) $ \(input, inh) -> - BS.hPutStr inh input >> hClose inh + let writeInput = for_ minh $ \inh -> do + forM_ minput $ BS.hPutStr inh + hClose inh readOutput = (,) <$> traverse BS.hGetContents mouth <*> waitForProcess ph in snd <$> concurrently writeInput readOutput case (mout, ex) of diff --git a/libs/wire-api/test/unit/Test/Wire/API/RawJson.hs b/libs/wire-api/test/unit/Test/Wire/API/RawJson.hs new file mode 100644 index 0000000000..4d4e1788cf --- /dev/null +++ b/libs/wire-api/test/unit/Test/Wire/API/RawJson.hs @@ -0,0 +1,22 @@ +module Test.Wire.API.RawJson (tests) where + +import Data.Proxy +import Imports +import Servant.API +import Test.Tasty +import Test.Tasty.QuickCheck +import Wire.API.RawJson + +tests :: TestTree +tests = + testGroup "RawJson" $ + [ testProperty "MimeUnrender lifts any string to RawJson" testMimeUnrender + ] + +testMimeUnrender :: Property +testMimeUnrender = + forAll + (arbitrary :: (Gen LByteString)) + ( \t -> + mimeUnrender (Proxy @JSON) t == (Right . RawJson) t + ) diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs index 9f0bd912dd..3586bb0a0b 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs @@ -46,6 +46,8 @@ import qualified Wire.API.Provider.External as Provider.External import qualified Wire.API.Provider.Service as Provider.Service import qualified Wire.API.Provider.Service.Tag as Provider.Service.Tag import qualified Wire.API.Push.Token as Push.Token +import qualified Wire.API.Routes.Internal.Galley.TeamsIntra as TeamsIntra +import qualified Wire.API.SystemSettings as SystemSettings import qualified Wire.API.Team as Team import qualified Wire.API.Team.Conversation as Team.Conversation import qualified Wire.API.Team.Feature as Team.Feature @@ -179,6 +181,9 @@ tests = testRoundTrip @Push.Token.PushToken, testRoundTrip @Push.Token.PushTokenList, testRoundTrip @Scim.CreateScimToken, + testRoundTrip @SystemSettings.SystemSettings, + testRoundTrip @SystemSettings.SystemSettingsPublic, + testRoundTrip @SystemSettings.SystemSettingsInternal, testRoundTrip @Team.BindingNewTeam, testRoundTrip @Team.TeamBinding, testRoundTrip @Team.Team, @@ -310,7 +315,12 @@ tests = testRoundTrip @User.Search.PagingState, testRoundTrip @User.Search.TeamContact, testRoundTrip @(Wrapped.Wrapped "some_int" Int), - testRoundTrip @Conversation.Action.SomeConversationAction + testRoundTrip @Conversation.Action.SomeConversationAction, + testRoundTrip @TeamsIntra.GuardLegalholdPolicyConflicts, + testRoundTrip @TeamsIntra.TeamStatus, + testRoundTrip @TeamsIntra.TeamStatusUpdate, + testRoundTrip @TeamsIntra.TeamData, + testRoundTrip @TeamsIntra.TeamName ] testRoundTrip :: diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/CSV.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/CSV.hs index 3585739dab..9a2b86bf48 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/CSV.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/CSV.hs @@ -43,8 +43,8 @@ testRoundTrip = testProperty msg trip counterexample (show $ encodeCSV v) $ Right v === (decodeCSV . encodeCSV) v - encodeCSV :: (DefaultOrdered a, ToNamedRecord a) => [a] -> LByteString + encodeCSV :: [a] -> LByteString encodeCSV = encodeDefaultOrderedByName - decodeCSV :: FromNamedRecord a => LByteString -> Either String [a] + decodeCSV :: LByteString -> Either String [a] decodeCSV bstr = decodeByName bstr <&> (snd >>> V.toList) diff --git a/libs/wire-api/test/unit/Test/Wire/API/Routes/Version.hs b/libs/wire-api/test/unit/Test/Wire/API/Routes/Version.hs new file mode 100644 index 0000000000..9af4812e89 --- /dev/null +++ b/libs/wire-api/test/unit/Test/Wire/API/Routes/Version.hs @@ -0,0 +1,22 @@ +module Test.Wire.API.Routes.Version where + +import Imports +import qualified Test.Tasty as T +import Test.Tasty.HUnit +import Wire.API.Routes.Version + +tests :: T.TestTree +tests = + T.testGroup "Version" $ + [ T.testGroup + "toPathComponent" + [testCase "serialise different versions" testToPathComponent] + ] + +testToPathComponent :: Assertion +testToPathComponent = do + "v0" @=? toPathComponent V0 + "v1" @=? toPathComponent V1 + "v2" @=? toPathComponent V2 + "v3" @=? toPathComponent V3 + "v4" @=? toPathComponent V4 diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 255c044065..8571f041dd 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -82,8 +82,12 @@ library Wire.API.Routes.Internal.Brig.EJPD Wire.API.Routes.Internal.Cannon Wire.API.Routes.Internal.Cargohold + Wire.API.Routes.Internal.Galley + Wire.API.Routes.Internal.Galley.ConversationsIntra Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti + Wire.API.Routes.Internal.Galley.TeamsIntra Wire.API.Routes.Internal.LegalHold + Wire.API.Routes.Internal.Spar Wire.API.Routes.LowLevelStream Wire.API.Routes.MultiTablePaging Wire.API.Routes.MultiTablePaging.State @@ -104,6 +108,7 @@ library Wire.API.Routes.Public.Galley.Team Wire.API.Routes.Public.Galley.TeamConversation Wire.API.Routes.Public.Galley.TeamMember + Wire.API.Routes.Public.Galley.TeamNotification Wire.API.Routes.Public.Gundeck Wire.API.Routes.Public.Proxy Wire.API.Routes.Public.Spar @@ -114,8 +119,8 @@ library Wire.API.Routes.Versioned Wire.API.Routes.WebSocket Wire.API.ServantProto - Wire.API.Swagger Wire.API.SwaggerHelper + Wire.API.SwaggerServant Wire.API.SystemSettings Wire.API.Team Wire.API.Team.Conversation @@ -198,6 +203,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: aeson >=2.0.1.0 @@ -266,13 +272,15 @@ library , servant-swagger , servant-swagger-ui , singletons + , singletons-base + , singletons-th , sop-core , string-conversions - , swagger >=0.1 , swagger2 , tagged , text >=0.11 , time >=1.4 + , transitive-anns , types-common >=0.16 , unordered-containers >=0.2 , uri-bytestring >=0.2 @@ -585,7 +593,7 @@ test-suite wire-api-golden-tests ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -with-rtsopts=-N + -threaded -with-rtsopts=-N -Wredundant-constraints build-depends: aeson >=2.0.1.0 @@ -644,12 +652,14 @@ test-suite wire-api-tests Test.Wire.API.Call.Config Test.Wire.API.Conversation Test.Wire.API.MLS + Test.Wire.API.RawJson 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.Routes.Version Test.Wire.API.Swagger Test.Wire.API.Team.Export Test.Wire.API.Team.Member @@ -702,7 +712,7 @@ test-suite wire-api-tests ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -with-rtsopts=-N + -threaded -with-rtsopts=-N -Wredundant-constraints build-depends: aeson >=2.0.1.0 diff --git a/libs/wire-message-proto-lens/wire-message-proto-lens.cabal b/libs/wire-message-proto-lens/wire-message-proto-lens.cabal index 4677d7bbf0..b3a9b35bc9 100644 --- a/libs/wire-message-proto-lens/wire-message-proto-lens.cabal +++ b/libs/wire-message-proto-lens/wire-message-proto-lens.cabal @@ -72,7 +72,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -fno-warn-redundant-constraints + -Wredundant-constraints ghc-prof-options: -fprof-auto-exported build-depends: diff --git a/libs/zauth/src/Data/ZAuth/Creation.hs b/libs/zauth/src/Data/ZAuth/Creation.hs index 22542e5727..4e55a930c9 100644 --- a/libs/zauth/src/Data/ZAuth/Creation.hs +++ b/libs/zauth/src/Data/ZAuth/Creation.hs @@ -175,5 +175,5 @@ signToken h a = Create $ do f <- (! (h ^. key - 1)) <$> asks zSign liftIO . f . toStrict . toLazyByteString $ writeData h a -expiry :: (Functor m, MonadIO m) => Integer -> m POSIXTime +expiry :: MonadIO m => Integer -> m POSIXTime expiry d = (fromInteger d +) <$> liftIO getPOSIXTime diff --git a/libs/zauth/src/Data/ZAuth/Token.hs b/libs/zauth/src/Data/ZAuth/Token.hs index 83b4ff1f31..561878ce0b 100644 --- a/libs/zauth/src/Data/ZAuth/Token.hs +++ b/libs/zauth/src/Data/ZAuth/Token.hs @@ -1,6 +1,9 @@ {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} +-- Disabling for this module, as Getters have a functor +-- constraint that GHC is complaining about. +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- diff --git a/libs/zauth/src/Data/ZAuth/Validation.hs b/libs/zauth/src/Data/ZAuth/Validation.hs index 97c2a96836..be21a983da 100644 --- a/libs/zauth/src/Data/ZAuth/Validation.hs +++ b/libs/zauth/src/Data/ZAuth/Validation.hs @@ -130,5 +130,5 @@ check t = do throwError Expired pure t -now :: (Functor m, MonadIO m) => m Integer +now :: MonadIO m => m Integer now = floor <$> liftIO getPOSIXTime diff --git a/libs/zauth/zauth.cabal b/libs/zauth/zauth.cabal index d81ea68e66..7ddc1a138c 100644 --- a/libs/zauth/zauth.cabal +++ b/libs/zauth/zauth.cabal @@ -64,7 +64,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -funbox-strict-fields + -funbox-strict-fields -Wredundant-constraints build-depends: attoparsec >=0.11 @@ -132,6 +132,7 @@ executable zauth ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: base @@ -200,6 +201,7 @@ test-suite zauth-unit ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: base diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index 900d716ce9..1a59b723a1 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -57,38 +57,11 @@ { lib, fetchgit }: hself: hsuper: let gitPins = { - HaskellNet-SSL = { + transitive-anns = { src = fetchgit { - url = "https://github.com/dpwright/HaskellNet-SSL"; - rev = "ca84ef29a93eaef7673fa58056cdd8dae1568d2d"; - sha256 = "1w23xgjdq22px90p12yw30psagc668n7l183bqvf8x075s77wckr"; - }; - }; - prometheus-haskell = { - src = fetchgit { - url = "https://github.com/fimad/prometheus-haskell"; - rev = "2e3282e5fb27ba8d989c271a0a989823fad7ec43"; - sha256 = "0vfzysn9sgpxymfvpahxrp74fczgjnw3kgknj6zk0473qk85488f"; - }; - packages = { - wai-middleware-prometheus = "wai-middleware-prometheus"; - }; - }; - hs-collectd = { - src = fetchgit { - url = "https://github.com/kim/hs-collectd"; - rev = "885da222be2375f78c7be36127620ed772b677c9"; - sha256 = "1a3jwj0h2vzgjvzaa4jghmxkjwbzisq4qc7dldc42zi4jaq7lix7"; - }; - }; - hs-certificate = { - src = fetchgit { - url = "https://github.com/vincenthz/hs-certificate"; - rev = "a899bda3d7666d25143be7be8f3105fc076703d9"; - sha256 = "0ivc4l3c272i7w37rfgsbwnxa3fzfmghwddlqvzj5jj3zx5lyqlk"; - }; - packages = { - x509-store = "x509-store"; + url = "https://github.com/wireapp/transitive-anns"; + rev = "c3bdc423f84bf15fe8b3618b5dddd5764fc8a470"; + sha256 = "sha256-mWBZ2uY0shlxNRceyC2Zu1f3Kr4IDtT/rOL7CKWgilA="; }; }; amazonka = { @@ -113,8 +86,8 @@ let bloodhound = { src = fetchgit { url = "https://github.com/wireapp/bloodhound"; - rev = "c68e6d877d87988331bf391ed16780383a58eefa"; - sha256 = "0fr5xgq8f1nmcbk8srrhyf4vad4xm5iqr974jgqfg1mg31y85h0x"; + rev = "abf819a4a6ec7601f1e58cb8da13b2fdad377d9e"; + sha256 = "sha256-m1O+F/mOJN5z5WNChmeyHP4dtmLRkl2YnLlTuwzRelk="; }; }; cryptobox-haskell = { @@ -124,13 +97,6 @@ let sha256 = "0dgizj1kc135yzzqdf5l7f5ax0qpvrr8mxvg7s1dbm01cf11aqzn"; }; }; - multihash = { - src = fetchgit { - url = "https://github.com/wireapp/haskell-multihash.git"; - rev = "300a6f46384bfca33e545c8bab52ef3717452d12"; - sha256 = "0lcm6argp49fin4va7c50l1lj84xcm3cqzijzssfdgplimpmllma"; - }; - }; hsaml2 = { src = fetchgit { url = "https://github.com/wireapp/hsaml2"; @@ -138,13 +104,6 @@ let sha256 = "16hj3i4h5rwhr8kqrs7345wg7v10ahwjd3fdp2qx3c5z4qls6prr"; }; }; - hspec-wai = { - src = fetchgit { - url = "https://github.com/wireapp/hspec-wai"; - rev = "0a5142cd3ba48116ff059c041348b817fb7bdb25"; - sha256 = "1yqkla7467fgb23yw689xh15zjn38rkc7ckf18cfalpc2ff5wfq1"; - }; - }; http-client = { src = fetchgit { url = "https://github.com/wireapp/http-client"; @@ -158,39 +117,40 @@ let http-conduit = "http-conduit"; }; }; - http2 = { + hspec-wai = { src = fetchgit { - url = "https://github.com/wireapp/http2"; - rev = "aa3501ad58e1abbd196781fac25a84f41ec2a787"; - sha256 = "09h86fkk8p7szq08x0iszaq16mhbylxivfc0apvj58d98wl8l6lq"; + url = "https://github.com/wireapp/hspec-wai"; + rev = "6984a06b0c6294677c49d59382d48f975a8733d4"; + sha256 = "sha256-6FLTMMqvL0xFa5zsMnjVAmdpghmdeBl813bWcOyQo5E="; }; }; saml2-web-sso = { src = fetchgit { url = "https://github.com/wireapp/saml2-web-sso"; - rev = "74371cd775cb98d6cf85f6e182244a3c4fd48702"; - sha256 = "1w23yz2iiayniymk7k4g8gww7268187cayw0c8m3bz2hbnvbyfbc"; + rev = "b79a45ac98b1f592ac18511fce48ed88d2e931c9"; + sha256 = "sha256-g2lbKt3+hToVFQvaHOa9dg4HqAL7YgReo8fy7wQavmY="; }; }; swagger2 = { src = fetchgit { - url = "https://github.com/wireapp/swagger2"; - rev = "ba916df2775bb38ec603b726bbebfb65a908317a"; - sha256 = "sha256-IcsrJ5ur8Zm7Xp1PQBOb+2N7T8WMI8jJ6YuDv8ypsPQ="; + url = "https://github.com/GetShopTV/swagger2"; + rev = "d79deca03b714cdd4531217831a8305068b2e8f9"; + sha256 = "sha256-R3p0L0TgM0Bspe5z6vauwdPq9TmEWpMC53DBkMtCEoE="; }; }; + # MR: https://gitlab.com/twittner/cql-io/-/merge_requests/20 cql-io = { src = fetchgit { - url = "https://gitlab.com/axeman/cql-io"; + url = "https://gitlab.com/wireapp/forks/cql-io"; rev = "c2b6aa995b5817ed7c78c53f72d5aa586ef87c36"; - sha256 = "1wncign8ilvqs9qyl6pkz66x2s8dgwhnfdjw82wv38ijmr95di0c"; + sha256 = "sha256-DMRWUq4yorG5QFw2ZyF/DWnRjfnzGupx0njTiOyLzPI="; }; }; - swagger = { + wai-predicates = { src = fetchgit { - url = "https://gitlab.com/axeman/swagger"; - rev = "e2d3f5b5274b8d8d301b5377b0af4319cea73f9e"; - sha256 = "1zj3fqlvcvp9s0myb98b6s9mpmiqamyxn2d3jan55irdgm53prmv"; + url = "https://gitlab.com/wireapp/forks/wai-predicates.git"; + rev = "ff95282a982ab45cced70656475eaf2cefaa26ea"; + sha256 = "sha256-x2XSv2+/+DG9FXN8hfUWGNIO7V4iBhlzYz19WWKaLKQ="; }; }; wai-routing = { @@ -200,6 +160,7 @@ let sha256 = "18icwks9jc6sy42vcvj2ysaip2s0dsrpvm9sy608b6nq6kk1ahlk"; }; }; + # PR: https://github.com/UnkindPartition/tasty/pull/351 tasty = { src = fetchgit { url = "https://github.com/wireapp/tasty"; @@ -218,47 +179,41 @@ let sha256 = "sha256-yiw6hg3guRWS6CVdrUY8wyIDxoqfGjIVMrEtP+Fys0Y="; }; }; + # Not tested/relased yet + # https://github.com/dylex/invertible/commit/e203c6a729fde87b1f903c3f468f739a085fb446 + invertible = { + src = fetchgit { + url = "https://github.com/dylex/invertible.git"; + rev = "e203c6a729fde87b1f903c3f468f739a085fb446"; + sha256 = "sha256-G6PX5lpU18oWLkwIityN4Hs0HuwQrq9T51kxbsdpK3M="; + }; + }; + tinylog = { + src = fetchgit { + url = "https://gitlab.com/wireapp/forks/tinylog.git"; + rev = "9609104263e8cd2a631417c1c3ef23e090de0d09"; + sha256 = "sha256-htEIJY+LmIMACVZrflU60+X42/g14NxUyFM7VJs4E6w="; + }; + }; }; hackagePins = { + # Major re-write upstream, we should get rid of this dependency rather than + # adapt to upstream. wai-route = { version = "0.4.0"; sha256 = "sha256-DSMckKIeVE/buSMg8Mq+mUm1bYPYB7veA11Ns7vTBbc="; }; - partial-isomorphisms = { - version = "0.2.2.1"; - sha256 = "sha256-TdsLB0ueaUUllLdvcGu3YNQXCfGRRk5WxP3deHEbHGI="; - }; - kind-generics = { - version = "0.4.1.2"; - sha256 = "sha256-orDfC5+QXRlAMVaqAhT1Fo7Eh/AnobROWeliZqEAXZU="; - }; - kind-generics-th = { - version = "0.2.2.2"; - sha256 = "sha256-nPuRq19UGVXe4YrITAZcF+U4TUBo5APMT2Nh9NqIkxQ="; - }; polysemy = { version = "1.8.0.0"; sha256 = "sha256-AdxxKWXdUjZiHLDj6iswMWpycs7mFB8eKhBR4ljF6kk="; }; - polysemy-check = { - version = "0.9.0.1"; - sha256 = "sha256-CsL2vMxAmpvVVR/iUnZAkbcRLiy/a8ulJQ6QwtCYmRM="; + HsOpenSSL = { + version = "0.11.7.5"; + sha256 = "sha256-CfH1YJSGuF4O1aUfdJwUZKRrVzv5nSPhwoI7mf9ewEg="; }; - polysemy-plugin = { - version = "0.4.3.1"; - sha256 = "sha256-0vkLYNZISr3fmmQvD8qdLkn2GHc80l1GzJuOmqjqXE4="; - }; - singletons = { - version = "2.7"; - sha256 = "sha256-q7yc/wyGSyYI0KdgHgRi0WISv9WEibxQ5yM7cSjXS2s="; - }; - th-desugar = { - version = "1.11"; - sha256 = "sha256-07sUW1ufEM7Xqv6C2rlFGI5CDO5cchDOND7QFstKu5g="; - }; - one-liner = { - version = "1.0"; - sha256 = "sha256-dv/W8hIPoHVevxiiCb6OfeP53O/9HPgUiqOHGSNb/pk="; + http2 = { + version = "4.0.0"; + sha256 = "sha256-9rBhklwuuKZXWH4yV4tb7Sp5chR9AmBAMRBztDjx0uI="; }; }; # Name -> Source -> Maybe Subpath -> Drv diff --git a/nix/manual-overrides.nix b/nix/manual-overrides.nix index 1d5e8c8a44..807e39fbd1 100644 --- a/nix/manual-overrides.nix +++ b/nix/manual-overrides.nix @@ -2,31 +2,30 @@ # 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: { - network-arbitrary = hlib.markUnbroken (hlib.doJailbreak hsuper.network-arbitrary); - cql = hlib.markUnbroken hsuper.cql; - lens-datetime = hlib.markUnbroken (hlib.doJailbreak hsuper.lens-datetime); - wai-predicates = hlib.markUnbroken hsuper.wai-predicates; + aeson = hsuper.aeson_2_1_1_0; + binary-parsers = hlib.markUnbroken (hlib.doJailbreak hsuper.binary-parsers); bytestring-arbitrary = hlib.markUnbroken (hlib.doJailbreak hsuper.bytestring-arbitrary); + cql = hlib.markUnbroken hsuper.cql; + hashtables = hsuper.hashtables_1_3; invertible = hlib.markUnbroken hsuper.invertible; + lens-datetime = hlib.markUnbroken (hlib.doJailbreak hsuper.lens-datetime); + network-arbitrary = hlib.markUnbroken (hlib.doJailbreak hsuper.network-arbitrary); + one-liner = hlib.doJailbreak hsuper.one-liner; + polysemy = hlib.doJailbreak hsuper.polysemy; polysemy-check = hlib.markUnbroken (hlib.doJailbreak hsuper.polysemy-check); - swagger = hlib.doJailbreak hsuper.swagger; - multihash = hlib.markUnbroken (hlib.doJailbreak hsuper.multihash); - hashable = hsuper.hashable_1_4_1_0; - hashable-time = hsuper.hashable-time_0_3; - text-short = hlib.dontCheck hsuper.text-short; - aeson = hsuper.aeson_2_1_1_0; - lens-aeson = hsuper.lens-aeson_1_2_2; - swagger2 = hlib.doJailbreak hsuper.swagger2; - servant-swagger-ui-core = hlib.doJailbreak hsuper.servant-swagger-ui-core; - servant-swagger-ui = hlib.doJailbreak hsuper.servant-swagger-ui; - sodium-crypto-sign = hlib.addPkgconfigDepend hsuper.sodium-crypto-sign libsodium.dev; + polysemy-plugin = hlib.doJailbreak hsuper.polysemy-plugin; + quickcheck-state-machine = hlib.dontCheck hsuper.quickcheck-state-machine; servant-foreign = hlib.doJailbreak hsuper.servant-foreign; servant-multipart = hlib.doJailbreak hsuper.servant-multipart; - hashtables = hsuper.hashtables_1_3; - quickcheck-state-machine = hlib.dontCheck hsuper.quickcheck-state-machine; - quickcheck-arbitrary-template = hlib.markUnbroken (hsuper.quickcheck-arbitrary-template); + servant-swagger-ui = hlib.doJailbreak hsuper.servant-swagger-ui; + servant-swagger-ui-core = hlib.doJailbreak hsuper.servant-swagger-ui-core; + sodium-crypto-sign = hlib.addPkgconfigDepend hsuper.sodium-crypto-sign libsodium.dev; + swagger2 = hlib.doJailbreak hsuper.swagger2; + text-icu-translit = hlib.markUnbroken (hlib.dontCheck hsuper.text-icu-translit); + text-short = hlib.dontCheck hsuper.text-short; + type-errors = hlib.dontCheck hsuper.type-errors; wai-middleware-prometheus = hlib.doJailbreak hsuper.wai-middleware-prometheus; - one-liner = hlib.doJailbreak hsuper.one-liner; + wai-predicates = hlib.markUnbroken hsuper.wai-predicates; # Some test seems to be broken hsaml2 = hlib.dontCheck hsuper.hsaml2; @@ -36,12 +35,14 @@ hself: hsuper: { cql-io = hlib.dontCheck hsuper.cql-io; # Needs network access to running ES + # also the test suite doesn't compile https://github.com/NixOS/nixpkgs/pull/167957 + # due to related broken quickcheck-arbitrary-template bloodhound = hlib.dontCheck hsuper.bloodhound; # These tests require newer version on hspec-wai, which doesn't work with some of the wire-server packages. amazonka = hlib.doJailbreak (hlib.dontCheck hsuper.amazonka); - amazonka-core = hlib.doJailbreak (hlib.dontCheck hsuper.amazonka-core); amazonka-cloudfront = hlib.dontCheck hsuper.amazonka-cloudfront; + amazonka-core = hlib.doJailbreak (hlib.dontCheck hsuper.amazonka-core); amazonka-dynamodb = hlib.dontCheck hsuper.amazonka-dynamodb; amazonka-s3 = hlib.dontCheck hsuper.amazonka-s3; amazonka-ses = hlib.dontCheck hsuper.amazonka-ses; @@ -52,13 +53,16 @@ hself: hsuper: { servant-server = hlib.dontCheck hsuper.servant-server; # Build toool dependencies of local packages - wire-message-proto-lens = hlib.addBuildTool hsuper.wire-message-proto-lens protobuf; types-common-journal = hlib.addBuildTool hsuper.types-common-journal protobuf; wire-api = hlib.addBuildTool hsuper.wire-api mls-test-cli; + wire-message-proto-lens = hlib.addBuildTool hsuper.wire-message-proto-lens protobuf; # Make hoogle static to reduce size of the hoogle image hoogle = hlib.justStaticExecutables hsuper.hoogle; # Postie has been fixed upstream (master) postie = hlib.markUnbroken (hlib.doJailbreak hsuper.postie); + + # This would not be necessary if we could pull revision -r1 from 0.2.2.3 + kind-generics-th = hlib.doJailbreak hsuper.kind-generics-th; } diff --git a/nix/nginz.nix b/nix/nginz.nix index 67636b9180..074d5079e9 100644 --- a/nix/nginz.nix +++ b/nix/nginz.nix @@ -59,6 +59,13 @@ let ]; }; + # Docker tools doesn't create tmp directories but nginx needs this and so we + # have to create it ourself. + tmpDir = runCommand "tmp-dir" { } '' + mkdir -p $out/tmp + mkdir -p $out/var/tmp + ''; + nginzImage = dockerTools.streamLayeredImage { name = "quay.io/wire/nginz"; maxLayers = 10; @@ -70,7 +77,14 @@ let coreutils nginxFakeNss nginz # so preStop lifecycle hook in cannon can nginx -c … quit + tmpDir ]; + # 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 + ''; 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" ]; diff --git a/nix/overlay.nix b/nix/overlay.nix index d63b121432..767abcaa20 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -61,6 +61,7 @@ self: super: { nginxModules = super.nginxModules // { zauth = { + name = "zauth"; src = ../services/nginz/third_party/nginx-zauth-module; inputs = [ self.pkg-config self.zauth ]; }; @@ -87,18 +88,7 @@ self: super: { inherit (super) stdenv fetchurl; }; - helm = staticBinaryInTarball { - pname = "helm"; - version = "3.6.3"; - - darwinAmd64Url = "https://get.helm.sh/helm-v3.6.3-darwin-amd64.tar.gz"; - darwinAmd64Sha256 = "0djjvgla8cw27h8s4y6jby19f74j58byb2vfv590cd03vlbzz8c4"; - - linuxAmd64Url = "https://get.helm.sh/helm-v3.6.3-linux-amd64.tar.gz"; - linuxAmd64Sha256 = "0qp28fq137b07haz4vsdbc5biagh60dcs29jj70ksqi5k6201h87"; - - inherit (super) stdenv fetchurl; - }; + helm = super.callPackage ./pkgs/helm { }; helmfile = staticBinary { pname = "helmfile"; @@ -140,8 +130,4 @@ self: super: { inherit (super) stdenv fetchurl; }; - - # This is to match the ormolu version that ships with HLS. - # This doesn't compile with ghc8107 howerver, so we use ghc92 - ormolu = super.haskell.lib.justStaticExecutables (super.haskell.lib.doJailbreak super.haskell.packages.ghc92.ormolu_0_5_0_1); } diff --git a/nix/pkgs/helm/default.nix b/nix/pkgs/helm/default.nix new file mode 100644 index 0000000000..8b68403913 --- /dev/null +++ b/nix/pkgs/helm/default.nix @@ -0,0 +1,52 @@ +# Copied from nixpkgs and modified because it seems too complicated to override +# buildGoModule packages. +{ lib, stdenv, buildGoModule, fetchFromGitHub, installShellFiles }: + +buildGoModule rec { + pname = "kubernetes-helm"; + version = "3.11.0-patched"; + + src = fetchFromGitHub { + owner = "wireapp"; + repo = "helm"; + rev = "949de3195be5b3d21ed707da18ee3bcb2a9a2af8"; + sha256 = "sha256-alyR6+gm7WEvFfJxHl9a0jpC3+457Kg6aRHcidA0RZg="; + }; + vendorSha256 = "sha256-LRMDrBSl5EGQqQt5FUU4JJHqdwfYt5qsVpe76jUQBVI="; + + subPackages = [ "cmd/helm" ]; + ldflags = [ + "-w" + "-s" + "-X helm.sh/helm/v3/internal/version.version=v${version}" + "-X helm.sh/helm/v3/internal/version.gitCommit=${src.rev}" + ]; + + preCheck = '' + # skipping version tests because they require dot git directory + substituteInPlace cmd/helm/version_test.go \ + --replace "TestVersion" "SkipVersion" + '' + lib.optionalString stdenv.isLinux '' + # skipping plugin tests on linux + substituteInPlace cmd/helm/plugin_test.go \ + --replace "TestPluginDynamicCompletion" "SkipPluginDynamicCompletion" \ + --replace "TestLoadPlugins" "SkipLoadPlugins" + substituteInPlace cmd/helm/helm_test.go \ + --replace "TestPluginExitCode" "SkipPluginExitCode" + ''; + + nativeBuildInputs = [ installShellFiles ]; + postInstall = '' + $out/bin/helm completion bash > helm.bash + $out/bin/helm completion zsh > helm.zsh + installShellCompletion helm.{bash,zsh} + ''; + + meta = with lib; { + homepage = "https://github.com/kubernetes/helm"; + description = "A package manager for kubernetes"; + mainProgram = "helm"; + license = licenses.asl20; + maintainers = with maintainers; [ rlupton20 edude03 saschagrunert Frostman Chili-Man techknowlogick ]; + }; +} diff --git a/nix/pkgs/mls-test-cli/default.nix b/nix/pkgs/mls-test-cli/default.nix index 7d7d696113..8562916aca 100644 --- a/nix/pkgs/mls-test-cli/default.nix +++ b/nix/pkgs/mls-test-cli/default.nix @@ -5,12 +5,13 @@ , pkg-config , rustPlatform , stdenv +, gitMinimal }: rustPlatform.buildRustPackage rec { name = "mls-test-cli-${version}"; version = "0.6.0"; - nativeBuildInputs = [ pkg-config perl ]; + nativeBuildInputs = [ pkg-config perl gitMinimal ]; buildInputs = [ libsodium ]; src = fetchFromGitHub { owner = "wireapp"; diff --git a/nix/pkgs/rusty_jwt_tools_ffi/add-Cargo.lock.patch b/nix/pkgs/rusty_jwt_tools_ffi/add-Cargo.lock.patch index 5ac52f2aaf..9de4d963b0 100644 --- a/nix/pkgs/rusty_jwt_tools_ffi/add-Cargo.lock.patch +++ b/nix/pkgs/rusty_jwt_tools_ffi/add-Cargo.lock.patch @@ -1,41 +1,129 @@ -From 7d2af044a0a8d003936b5013defff915c4ca52dc Mon Sep 17 00:00:00 2001 +From 13a4229f8ae9f91f57a620a06aa1d6771aaad168 Mon Sep 17 00:00:00 2001 From: Leif Battermann -Date: Tue, 13 Sep 2022 12:08:40 +0200 -Subject: [PATCH] cargo lock +Date: Wed, 1 Mar 2023 15:27:03 +0100 +Subject: [PATCH] generate new cargo.lock --- - .gitignore | 1 - - Cargo.lock | 1279 ++++++++++++++++++++++++++++++++++++++++++++++++++++ - 2 files changed, 1279 insertions(+), 1 deletion(-) + Cargo.lock | 3006 ++++++++++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 3006 insertions(+) create mode 100644 Cargo.lock -diff --git a/.gitignore b/.gitignore -index a178556..68bc967 100644 ---- a/.gitignore -+++ b/.gitignore -@@ -1,7 +1,6 @@ - # Rust - /target - /debug --/Cargo.lock - - # Idea - .idea diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 -index 0000000..3f52488 +index 0000000..1e1537b --- /dev/null +++ b/Cargo.lock -@@ -0,0 +1,1279 @@ +@@ -0,0 +1,3006 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] ++name = "adler" ++version = "1.0.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" ++ ++[[package]] ++name = "aho-corasick" ++version = "0.7.20" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" ++dependencies = [ ++ "memchr", ++] ++ ++[[package]] ++name = "android_system_properties" ++version = "0.1.5" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" ++dependencies = [ ++ "libc", ++] ++ ++[[package]] +name = "anyhow" -+version = "1.0.64" ++version = "1.0.69" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" ++ ++[[package]] ++name = "asn1-rs" ++version = "0.5.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "cf6690c370453db30743b373a60ba498fc0d6d83b11f4abfd87a84a075db5dd4" ++dependencies = [ ++ "asn1-rs-derive", ++ "asn1-rs-impl", ++ "displaydoc", ++ "nom", ++ "num-traits", ++ "rusticata-macros", ++ "thiserror", ++ "time 0.3.20", ++] ++ ++[[package]] ++name = "asn1-rs-derive" ++version = "0.4.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" ++dependencies = [ ++ "proc-macro2", ++ "quote", ++ "syn", ++ "synstructure", ++] ++ ++[[package]] ++name = "asn1-rs-impl" ++version = "0.1.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" ++dependencies = [ ++ "proc-macro2", ++ "quote", ++ "syn", ++] ++ ++[[package]] ++name = "asserhttp" ++version = "0.6.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "337b30b878c9c627e04044451ed6c2626dae094674b76d26807ea88e2ed03274" ++dependencies = [ ++ "anyhow", ++ "assert-json-diff", ++ "futures-lite", ++ "http-types", ++ "regex", ++ "reqwest", ++ "serde", ++ "serde_json", ++ "tonic-build", ++] ++ ++[[package]] ++name = "assert-json-diff" ++version = "2.0.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" ++dependencies = [ ++ "serde", ++ "serde_json", ++] ++ ++[[package]] ++name = "async-channel" ++version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "b9a8f622bcf6ff3df478e9deba3e03e4e04b300f8e6a139e192c05fa3490afc7" ++checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" ++dependencies = [ ++ "concurrent-queue", ++ "event-listener", ++ "futures-core", ++] + +[[package]] +name = "autocfg" @@ -51,15 +139,21 @@ index 0000000..3f52488 + +[[package]] +name = "base64" -+version = "0.13.0" ++version = "0.13.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" ++ ++[[package]] ++name = "base64" ++version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" ++checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" + +[[package]] +name = "base64ct" -+version = "1.5.2" ++version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "ea2b2456fd614d856680dcd9fcc660a51a820fa09daef2e49772b56a193c8474" ++checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "binstring" @@ -68,6 +162,23 @@ index 0000000..3f52488 +checksum = "7e0d60973d9320722cb1206f412740e162a33b8547ea8d6be75d7cff237c7a85" + +[[package]] ++name = "biscuit" ++version = "0.6.0-beta1" ++source = "git+https://github.com/beltram/biscuit?tag=v0.6.0-pre.core-crypto-0.6.0#36b6f30964142f0ca5bc17c908e8b2ad78febaf6" ++dependencies = [ ++ "base64 0.21.0", ++ "chrono", ++ "data-encoding", ++ "num-bigint", ++ "num-traits", ++ "once_cell", ++ "rand 0.8.5", ++ "ring 0.17.0-not-released-yet", ++ "serde", ++ "serde_json", ++] ++ ++[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" @@ -83,10 +194,21 @@ index 0000000..3f52488 +] + +[[package]] ++name = "bollard-stubs" ++version = "1.41.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "ed2f2e73fffe9455141e170fb9c1feb0ac521ec7e7dcd47a7cab72a658490fb8" ++dependencies = [ ++ "chrono", ++ "serde", ++ "serde_with", ++] ++ ++[[package]] +name = "bumpalo" -+version = "3.11.0" ++version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" ++checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" + +[[package]] +name = "byteorder" @@ -95,24 +217,121 @@ index 0000000..3f52488 +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] ++name = "bytes" ++version = "1.4.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" ++ ++[[package]] ++name = "cc" ++version = "1.0.79" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" ++ ++[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] ++name = "chrono" ++version = "0.4.23" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" ++dependencies = [ ++ "iana-time-zone", ++ "js-sys", ++ "num-integer", ++ "num-traits", ++ "serde", ++ "time 0.1.45", ++ "wasm-bindgen", ++ "winapi", ++] ++ ++[[package]] ++name = "clap" ++version = "4.1.8" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "c3d7ae14b20b94cb02149ed21a86c423859cbe18dc7ed69845cace50e52b40a5" ++dependencies = [ ++ "bitflags", ++ "clap_derive", ++ "clap_lex", ++ "is-terminal", ++ "once_cell", ++ "strsim", ++ "termcolor", ++] ++ ++[[package]] ++name = "clap_derive" ++version = "4.1.8" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "44bec8e5c9d09e439c4335b1af0abaab56dcf3b94999a936e1bb47b9134288f0" ++dependencies = [ ++ "heck", ++ "proc-macro-error", ++ "proc-macro2", ++ "quote", ++ "syn", ++] ++ ++[[package]] ++name = "clap_lex" ++version = "0.3.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "350b9cf31731f9957399229e9b2adc51eeabdfbe9d71d9a0552275fd12710d09" ++dependencies = [ ++ "os_str_bytes", ++] ++ ++[[package]] +name = "coarsetime" -+version = "0.1.22" ++version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "454038500439e141804c655b4cd1bc6a70bcb95cd2bc9463af5661b6956f0e46" ++checksum = "a90d114103adbc625300f346d4d09dfb4ab1c4a8df6868435dd903392ecf4354" +dependencies = [ + "libc", + "once_cell", -+ "wasi", ++ "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] ++name = "codespan-reporting" ++version = "0.11.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" ++dependencies = [ ++ "termcolor", ++ "unicode-width", ++] ++ ++[[package]] ++name = "concurrent-queue" ++version = "2.1.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e" ++dependencies = [ ++ "crossbeam-utils", ++] ++ ++[[package]] ++name = "console" ++version = "0.15.5" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60" ++dependencies = [ ++ "encode_unicode", ++ "lazy_static", ++ "libc", ++ "unicode-width", ++ "windows-sys 0.42.0", ++] ++ ++[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" @@ -124,15 +343,21 @@ index 0000000..3f52488 + +[[package]] +name = "const-oid" -+version = "0.7.1" ++version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" ++checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" + +[[package]] -+name = "const-oid" -+version = "0.9.0" ++name = "convert_case" ++version = "0.4.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" ++ ++[[package]] ++name = "core-foundation-sys" ++version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "722e23542a15cea1f65d4a1419c4cfd7a26706c70871a13a04238ca3f40f1661" ++checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cpufeatures" @@ -144,23 +369,31 @@ index 0000000..3f52488 +] + +[[package]] -+name = "crypto-bigint" -+version = "0.3.2" ++name = "crc32fast" ++version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" ++checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ -+ "generic-array", -+ "subtle", ++ "cfg-if", ++] ++ ++[[package]] ++name = "crossbeam-utils" ++version = "0.8.15" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" ++dependencies = [ ++ "cfg-if", +] + +[[package]] +name = "crypto-bigint" -+version = "0.4.8" ++version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "9f2b443d17d49dad5ef0ede301c3179cc923b8822f3393b4d2c28c269dd4a122" ++checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array", -+ "rand_core", ++ "rand_core 0.6.4", + "subtle", + "zeroize", +] @@ -182,58 +415,180 @@ index 0000000..3f52488 +checksum = "f3b7eb4404b8195a9abb6356f4ac07d8ba267045c8d6d220ac4dc992e6cc75df" + +[[package]] -+name = "der" -+version = "0.5.1" ++name = "cxx" ++version = "1.0.91" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "86d3488e7665a7a483b57e25bdd90d0aeb2bc7608c8d0346acf2ad3f1caf1d62" ++dependencies = [ ++ "cc", ++ "cxxbridge-flags", ++ "cxxbridge-macro", ++ "link-cplusplus", ++] ++ ++[[package]] ++name = "cxx-build" ++version = "1.0.91" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "48fcaf066a053a41a81dfb14d57d99738b767febb8b735c3016e469fac5da690" ++dependencies = [ ++ "cc", ++ "codespan-reporting", ++ "once_cell", ++ "proc-macro2", ++ "quote", ++ "scratch", ++ "syn", ++] ++ ++[[package]] ++name = "cxxbridge-flags" ++version = "1.0.91" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "a2ef98b8b717a829ca5603af80e1f9e2e48013ab227b68ef37872ef84ee479bf" ++ ++[[package]] ++name = "cxxbridge-macro" ++version = "1.0.91" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892" ++dependencies = [ ++ "proc-macro2", ++ "quote", ++ "syn", ++] ++ ++[[package]] ++name = "darling" ++version = "0.13.4" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" ++dependencies = [ ++ "darling_core", ++ "darling_macro", ++] ++ ++[[package]] ++name = "darling_core" ++version = "0.13.4" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" ++dependencies = [ ++ "fnv", ++ "ident_case", ++ "proc-macro2", ++ "quote", ++ "strsim", ++ "syn", ++] ++ ++[[package]] ++name = "darling_macro" ++version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" ++checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ -+ "const-oid 0.7.1", -+ "crypto-bigint 0.3.2", -+ "pem-rfc7468 0.3.1", ++ "darling_core", ++ "quote", ++ "syn", +] + +[[package]] ++name = "data-encoding" ++version = "2.3.3" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" ++ ++[[package]] +name = "der" -+version = "0.6.0" ++version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "13dd2ae565c0a381dde7fade45fce95984c568bdcb4700a4fdbe3175e0380b2f" ++checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ -+ "const-oid 0.9.0", -+ "pem-rfc7468 0.6.0", ++ "const-oid", ++ "pem-rfc7468", + "zeroize", +] + +[[package]] ++name = "der-parser" ++version = "8.1.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "42d4bc9b0db0a0df9ae64634ac5bdefb7afcb534e182275ca0beadbe486701c1" ++dependencies = [ ++ "asn1-rs", ++ "displaydoc", ++ "nom", ++ "num-bigint", ++ "num-traits", ++ "rusticata-macros", ++] ++ ++[[package]] ++name = "derive_more" ++version = "0.99.17" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" ++dependencies = [ ++ "convert_case", ++ "proc-macro2", ++ "quote", ++ "rustc_version", ++ "syn", ++] ++ ++[[package]] +name = "digest" -+version = "0.10.3" ++version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" ++checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", ++ "const-oid", + "crypto-common", + "subtle", +] + +[[package]] ++name = "displaydoc" ++version = "0.2.3" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886" ++dependencies = [ ++ "proc-macro2", ++ "quote", ++ "syn", ++] ++ ++[[package]] +name = "ecdsa" -+version = "0.14.5" ++version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "1826508d57f3140a2e8e3c307b19915a266c92a1b8c2f6bb54e29e5d72a394ae" ++checksum = "12844141594ad74185a926d030f3b605f6a903b4e3fec351f3ea338ac5b7637e" +dependencies = [ -+ "der 0.6.0", ++ "der", + "elliptic-curve", + "rfc6979", -+ "signature", ++ "signature 2.0.0", +] + +[[package]] +name = "ed25519-compact" -+version = "1.0.11" ++version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "24e1f30f0312ac83726c1197abeacd91c9557f8a623e904a009ae6bc529ae8d8" ++checksum = "6a3d382e8464107391c8706b4c14b087808ecb909f6c15c34114bc42e53a9e4c" +dependencies = [ + "ct-codecs", -+ "getrandom", ++ "getrandom 0.2.8", ++] ++ ++[[package]] ++name = "either" ++version = "1.8.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" ++dependencies = [ ++ "serde", +] + +[[package]] @@ -243,50 +598,135 @@ index 0000000..3f52488 +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct", -+ "base64ct", -+ "crypto-bigint 0.4.8", -+ "der 0.6.0", ++ "crypto-bigint", ++ "der", + "digest", + "ff", + "generic-array", + "group", + "hkdf", -+ "pem-rfc7468 0.6.0", -+ "pkcs8 0.9.0", -+ "rand_core", ++ "pem-rfc7468", ++ "pkcs8", ++ "rand_core 0.6.4", + "sec1", -+ "serde_json", -+ "serdect", + "subtle", + "zeroize", +] + +[[package]] -+name = "ff" -+version = "0.12.0" ++name = "encode_unicode" ++version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "df689201f395c6b90dfe87127685f8dbfc083a5e779e613575d8bd7314300c3e" -+dependencies = [ -+ "rand_core", -+ "subtle", -+] ++checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] -+name = "fluvio-wasm-timer" -+version = "0.2.5" ++name = "encoding_rs" ++version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "b768c170dc045fa587a8f948c91f9bcfb87f774930477c6215addf54317f137f" ++checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ -+ "futures", -+ "js-sys", -+ "parking_lot", -+ "pin-utils", -+ "wasm-bindgen", -+ "wasm-bindgen-futures", -+ "web-sys", ++ "cfg-if", +] + +[[package]] ++name = "errno" ++version = "0.2.8" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" ++dependencies = [ ++ "errno-dragonfly", ++ "libc", ++ "winapi", ++] ++ ++[[package]] ++name = "errno-dragonfly" ++version = "0.1.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" ++dependencies = [ ++ "cc", ++ "libc", ++] ++ ++[[package]] ++name = "event-listener" ++version = "2.5.3" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" ++ ++[[package]] ++name = "fastrand" ++version = "1.9.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" ++dependencies = [ ++ "instant", ++] ++ ++[[package]] ++name = "ff" ++version = "0.12.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" ++dependencies = [ ++ "rand_core 0.6.4", ++ "subtle", ++] ++ ++[[package]] ++name = "fixedbitset" ++version = "0.4.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" ++ ++[[package]] ++name = "flate2" ++version = "1.0.25" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" ++dependencies = [ ++ "crc32fast", ++ "miniz_oxide", ++] ++ ++[[package]] ++name = "fluvio-wasm-timer" ++version = "0.2.5" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "b768c170dc045fa587a8f948c91f9bcfb87f774930477c6215addf54317f137f" ++dependencies = [ ++ "futures", ++ "js-sys", ++ "parking_lot", ++ "pin-utils", ++ "wasm-bindgen", ++ "wasm-bindgen-futures", ++ "web-sys", ++] ++ ++[[package]] ++name = "fnv" ++version = "1.0.7" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" ++ ++[[package]] ++name = "foreign-types" ++version = "0.3.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" ++dependencies = [ ++ "foreign-types-shared", ++] ++ ++[[package]] ++name = "foreign-types-shared" ++version = "0.1.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" ++ ++[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" @@ -297,9 +737,9 @@ index 0000000..3f52488 + +[[package]] +name = "futures" -+version = "0.3.24" ++version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c" ++checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" +dependencies = [ + "futures-channel", + "futures-core", @@ -312,9 +752,9 @@ index 0000000..3f52488 + +[[package]] +name = "futures-channel" -+version = "0.3.24" ++version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050" ++checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" +dependencies = [ + "futures-core", + "futures-sink", @@ -322,15 +762,15 @@ index 0000000..3f52488 + +[[package]] +name = "futures-core" -+version = "0.3.24" ++version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" ++checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" + +[[package]] +name = "futures-executor" -+version = "0.3.24" ++version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab" ++checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" +dependencies = [ + "futures-core", + "futures-task", @@ -339,15 +779,30 @@ index 0000000..3f52488 + +[[package]] +name = "futures-io" -+version = "0.3.24" ++version = "0.3.26" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" ++ ++[[package]] ++name = "futures-lite" ++version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" ++checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" ++dependencies = [ ++ "fastrand", ++ "futures-core", ++ "futures-io", ++ "memchr", ++ "parking", ++ "pin-project-lite", ++ "waker-fn", ++] + +[[package]] +name = "futures-macro" -+version = "0.3.24" ++version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" ++checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" +dependencies = [ + "proc-macro2", + "quote", @@ -356,15 +811,15 @@ index 0000000..3f52488 + +[[package]] +name = "futures-sink" -+version = "0.3.24" ++version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" ++checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" + +[[package]] +name = "futures-task" -+version = "0.3.24" ++version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" ++checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" + +[[package]] +name = "futures-timer" @@ -374,9 +829,9 @@ index 0000000..3f52488 + +[[package]] +name = "futures-util" -+version = "0.3.24" ++version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" ++checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" +dependencies = [ + "futures-channel", + "futures-core", @@ -402,29 +857,92 @@ index 0000000..3f52488 + +[[package]] +name = "getrandom" -+version = "0.2.7" ++version = "0.1.16" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" ++dependencies = [ ++ "cfg-if", ++ "libc", ++ "wasi 0.9.0+wasi-snapshot-preview1", ++] ++ ++[[package]] ++name = "getrandom" ++version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" ++checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "js-sys", + "libc", -+ "wasi", ++ "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "group" -+version = "0.12.0" ++version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "7391856def869c1c81063a03457c676fbcd419709c3dfb33d8d319de484b154d" ++checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff", -+ "rand_core", ++ "rand_core 0.6.4", + "subtle", +] + +[[package]] ++name = "h2" ++version = "0.3.16" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d" ++dependencies = [ ++ "bytes", ++ "fnv", ++ "futures-core", ++ "futures-sink", ++ "futures-util", ++ "http", ++ "indexmap", ++ "slab", ++ "tokio", ++ "tokio-util", ++ "tracing", ++] ++ ++[[package]] ++name = "hashbrown" ++version = "0.9.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" ++ ++[[package]] ++name = "heck" ++version = "0.4.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" ++ ++[[package]] ++name = "hermit-abi" ++version = "0.2.6" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" ++dependencies = [ ++ "libc", ++] ++ ++[[package]] ++name = "hermit-abi" ++version = "0.3.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" ++ ++[[package]] ++name = "hex" ++version = "0.4.3" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" ++ ++[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" @@ -444,29 +962,150 @@ index 0000000..3f52488 + +[[package]] +name = "hmac-sha1-compact" -+version = "1.1.1" ++version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "d103cfecf6edf3f7d1dc7c5ab64e99488c0f8d11786e43b40873e66e8489d014" ++checksum = "05e2440a0078e20c3b68ca01234cea4219f23e64b0c0bdb1200c5550d54239bb" + +[[package]] +name = "hmac-sha256" -+version = "1.1.4" ++version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "fd29dbba58ee5314f3ec570066d78a3f4772bf45b322efcf2ce2a43af69a4d85" ++checksum = "fc736091aacb31ddaa4cd5f6988b3c21e99913ac846b41f32538c5fae5d71bfe" +dependencies = [ + "digest", +] + +[[package]] +name = "hmac-sha512" -+version = "1.1.2" ++version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "a928b002dff1780b7fa21056991d395770ab9359154b8c1724c4d0511dad0a65" ++checksum = "520c9c3f6040661669bc5c91e551b605a520c8e0a63a766a91a65adef734d151" +dependencies = [ + "digest", +] + +[[package]] ++name = "http" ++version = "0.2.9" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" ++dependencies = [ ++ "bytes", ++ "fnv", ++ "itoa", ++] ++ ++[[package]] ++name = "http-body" ++version = "0.4.5" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" ++dependencies = [ ++ "bytes", ++ "http", ++ "pin-project-lite", ++] ++ ++[[package]] ++name = "http-types" ++version = "2.12.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad" ++dependencies = [ ++ "anyhow", ++ "async-channel", ++ "base64 0.13.1", ++ "futures-lite", ++ "infer", ++ "pin-project-lite", ++ "rand 0.7.3", ++ "serde", ++ "serde_json", ++ "serde_qs", ++ "serde_urlencoded", ++ "url", ++] ++ ++[[package]] ++name = "httparse" ++version = "1.8.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" ++ ++[[package]] ++name = "httpdate" ++version = "1.0.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" ++ ++[[package]] ++name = "hyper" ++version = "0.14.24" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c" ++dependencies = [ ++ "bytes", ++ "futures-channel", ++ "futures-core", ++ "futures-util", ++ "h2", ++ "http", ++ "http-body", ++ "httparse", ++ "httpdate", ++ "itoa", ++ "pin-project-lite", ++ "socket2", ++ "tokio", ++ "tower-service", ++ "tracing", ++ "want", ++] ++ ++[[package]] ++name = "hyper-rustls" ++version = "0.23.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" ++dependencies = [ ++ "http", ++ "hyper", ++ "rustls", ++ "tokio", ++ "tokio-rustls", ++] ++ ++[[package]] ++name = "iana-time-zone" ++version = "0.1.53" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" ++dependencies = [ ++ "android_system_properties", ++ "core-foundation-sys", ++ "iana-time-zone-haiku", ++ "js-sys", ++ "wasm-bindgen", ++ "winapi", ++] ++ ++[[package]] ++name = "iana-time-zone-haiku" ++version = "0.1.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" ++dependencies = [ ++ "cxx", ++ "cxx-build", ++] ++ ++[[package]] ++name = "ident_case" ++version = "1.0.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" ++ ++[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" @@ -477,6 +1116,22 @@ index 0000000..3f52488 +] + +[[package]] ++name = "indexmap" ++version = "1.6.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" ++dependencies = [ ++ "autocfg", ++ "hashbrown", ++] ++ ++[[package]] ++name = "infer" ++version = "0.2.3" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" ++ ++[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" @@ -489,24 +1144,91 @@ index 0000000..3f52488 +] + +[[package]] ++name = "io-lifetimes" ++version = "1.0.5" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" ++dependencies = [ ++ "libc", ++ "windows-sys 0.45.0", ++] ++ ++[[package]] ++name = "ipnet" ++version = "2.7.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" ++ ++[[package]] ++name = "is-terminal" ++version = "0.4.4" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857" ++dependencies = [ ++ "hermit-abi 0.3.1", ++ "io-lifetimes", ++ "rustix", ++ "windows-sys 0.45.0", ++] ++ ++[[package]] ++name = "itertools" ++version = "0.10.5" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" ++dependencies = [ ++ "either", ++] ++ ++[[package]] +name = "itoa" -+version = "1.0.3" ++version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" ++checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" ++ ++[[package]] ++name = "josekit" ++version = "0.8.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "9ef871a7a5f162afa718c416e9cbdd54241a58c922e07870e898ebad2425d8d8" ++dependencies = [ ++ "anyhow", ++ "base64 0.13.1", ++ "flate2", ++ "once_cell", ++ "openssl", ++ "regex", ++ "serde", ++ "serde_json", ++ "thiserror", ++ "time 0.3.20", ++] + +[[package]] +name = "js-sys" -+version = "0.3.59" ++version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" ++checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +dependencies = [ + "wasm-bindgen", +] + +[[package]] ++name = "json-patch" ++version = "0.3.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "e712e62827c382a77b87f590532febb1f8b2fdbc3eefa1ee37fe7281687075ef" ++dependencies = [ ++ "serde", ++ "serde_json", ++ "thiserror", ++ "treediff", ++] ++ ++[[package]] +name = "jwt-simple" -+version = "0.10.0" -+source = "git+https://github.com/beltram/rust-jwt-simple?branch=master#e05b2875285a79ee8e06639d21fde827466ec2cb" ++version = "0.11.3" ++source = "git+https://github.com/wireapp/rust-jwt-simple?tag=v0.11.3-pre.core-crypto-0.6.0#15a69f82288d68b74a75c1364e5d4bf681f1c07b" +dependencies = [ + "anyhow", + "binstring", @@ -519,25 +1241,27 @@ index 0000000..3f52488 + "k256", + "p256", + "p384", -+ "rand", ++ "rand 0.8.5", + "rsa", + "serde", + "serde_json", -+ "spki 0.5.4", ++ "spki", + "thiserror", + "zeroize", +] + +[[package]] +name = "k256" -+version = "0.11.4" ++version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "6db2573d3fd3e4cc741affc9b5ce1a8ce36cf29f09f80f36da4309d0ae6d7854" ++checksum = "92a55e0ff3b72c262bcf041d9e97f1b84492b68f1c1a384de2323d3dc9403397" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", ++ "once_cell", + "sha2", ++ "signature 2.0.0", +] + +[[package]] @@ -546,26 +1270,41 @@ index 0000000..3f52488 +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ -+ "spin", ++ "spin 0.5.2", +] + +[[package]] +name = "libc" -+version = "0.2.132" ++version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" ++checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "libm" -+version = "0.2.5" ++version = "0.2.6" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" ++ ++[[package]] ++name = "link-cplusplus" ++version = "1.0.8" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" ++dependencies = [ ++ "cc", ++] ++ ++[[package]] ++name = "linux-raw-sys" ++version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "292a948cd991e376cf75541fe5b97a1081d713c618b4f1b9500f8844e49eb565" ++checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + +[[package]] +name = "lock_api" -+version = "0.4.8" ++version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "9f80bf5aacaf25cbfc8210d1cfb718f2bf3b11c4c54e5afe36c236853a8ec390" ++checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", @@ -587,24 +1326,84 @@ index 0000000..3f52488 +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] -+name = "num-bigint-dig" -+version = "0.8.1" ++name = "mime" ++version = "0.3.16" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" ++ ++[[package]] ++name = "minimal-lexical" ++version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "566d173b2f9406afbc5510a90925d5a2cd80cae4605631f1212303df265de011" ++checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" ++ ++[[package]] ++name = "miniz_oxide" ++version = "0.6.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ -+ "byteorder", -+ "lazy_static", -+ "libm", -+ "num-integer", -+ "num-iter", -+ "num-traits", -+ "rand", -+ "smallvec", -+ "zeroize", ++ "adler", +] + +[[package]] -+name = "num-integer" ++name = "mio" ++version = "0.8.6" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" ++dependencies = [ ++ "libc", ++ "log", ++ "wasi 0.11.0+wasi-snapshot-preview1", ++ "windows-sys 0.45.0", ++] ++ ++[[package]] ++name = "multimap" ++version = "0.8.3" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" ++ ++[[package]] ++name = "nom" ++version = "7.1.3" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" ++dependencies = [ ++ "memchr", ++ "minimal-lexical", ++] ++ ++[[package]] ++name = "num-bigint" ++version = "0.4.3" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" ++dependencies = [ ++ "autocfg", ++ "num-integer", ++ "num-traits", ++] ++ ++[[package]] ++name = "num-bigint-dig" ++version = "0.8.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905" ++dependencies = [ ++ "byteorder", ++ "lazy_static", ++ "libm", ++ "num-integer", ++ "num-iter", ++ "num-traits", ++ "rand 0.8.5", ++ "smallvec", ++ "zeroize", ++] ++ ++[[package]] ++name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" @@ -635,34 +1434,106 @@ index 0000000..3f52488 +] + +[[package]] ++name = "num_cpus" ++version = "1.15.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" ++dependencies = [ ++ "hermit-abi 0.2.6", ++ "libc", ++] ++ ++[[package]] ++name = "oid-registry" ++version = "0.6.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" ++dependencies = [ ++ "asn1-rs", ++] ++ ++[[package]] +name = "once_cell" -+version = "1.14.0" ++version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" ++checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" ++ ++[[package]] ++name = "openssl" ++version = "0.10.45" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1" ++dependencies = [ ++ "bitflags", ++ "cfg-if", ++ "foreign-types", ++ "libc", ++ "once_cell", ++ "openssl-macros", ++ "openssl-sys", ++] ++ ++[[package]] ++name = "openssl-macros" ++version = "0.1.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" ++dependencies = [ ++ "proc-macro2", ++ "quote", ++ "syn", ++] ++ ++[[package]] ++name = "openssl-sys" ++version = "0.9.80" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7" ++dependencies = [ ++ "autocfg", ++ "cc", ++ "libc", ++ "pkg-config", ++ "vcpkg", ++] ++ ++[[package]] ++name = "os_str_bytes" ++version = "6.4.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" + +[[package]] +name = "p256" -+version = "0.11.1" ++version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" ++checksum = "49c124b3cbce43bcbac68c58ec181d98ed6cc7e6d0aa7c3ba97b2563410b0e55" +dependencies = [ + "ecdsa", + "elliptic-curve", ++ "primeorder", + "sha2", +] + +[[package]] +name = "p384" -+version = "0.11.2" ++version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "dfc8c5bf642dde52bb9e87c0ecd8ca5a76faac2eeed98dedb7c717997e1080aa" ++checksum = "630a4a9b2618348ececfae61a4905f564b817063bf2d66cdfc2ced523fe1d2d4" +dependencies = [ + "ecdsa", + "elliptic-curve", ++ "primeorder", + "sha2", +] + +[[package]] ++name = "parking" ++version = "2.0.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" ++ ++[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" @@ -675,9 +1546,9 @@ index 0000000..3f52488 + +[[package]] +name = "parking_lot_core" -+version = "0.8.5" ++version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" ++checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", @@ -688,12 +1559,12 @@ index 0000000..3f52488 +] + +[[package]] -+name = "pem-rfc7468" -+version = "0.3.1" ++name = "pem" ++version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "01de5d978f34aa4b2296576379fcc416034702fd94117c56ffd8a1a767cefb30" ++checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ -+ "base64ct", ++ "base64 0.13.1", +] + +[[package]] @@ -712,6 +1583,16 @@ index 0000000..3f52488 +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] ++name = "petgraph" ++version = "0.6.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "4a13a2fa9d0b63e5f22328828741e523766fff0ee9e779316902290dff3f824f" ++dependencies = [ ++ "fixedbitset", ++ "indexmap", ++] ++ ++[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" @@ -725,69 +1606,185 @@ index 0000000..3f52488 + +[[package]] +name = "pkcs1" -+version = "0.3.3" ++version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "a78f66c04ccc83dd4486fd46c33896f4e17b24a7a3a6400dedc48ed0ddd72320" ++checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719" +dependencies = [ -+ "der 0.5.1", -+ "pkcs8 0.8.0", ++ "der", ++ "pkcs8", ++ "spki", + "zeroize", +] + +[[package]] +name = "pkcs8" -+version = "0.8.0" ++version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" ++checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ -+ "der 0.5.1", -+ "spki 0.5.4", -+ "zeroize", ++ "der", ++ "spki", +] + +[[package]] -+name = "pkcs8" -+version = "0.9.0" ++name = "pkg-config" ++version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" ++checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" ++ ++[[package]] ++name = "ppv-lite86" ++version = "0.2.17" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" ++ ++[[package]] ++name = "prettyplease" ++version = "0.1.23" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "e97e3215779627f01ee256d2fad52f3d95e8e1c11e9fc6fd08f7cd455d5d5c78" +dependencies = [ -+ "der 0.6.0", -+ "spki 0.6.0", ++ "proc-macro2", ++ "syn", +] + +[[package]] -+name = "ppv-lite86" -+version = "0.2.16" ++name = "primeorder" ++version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" ++checksum = "0b54f7131b3dba65a2f414cf5bd25b66d4682e4608610668eae785750ba4c5b2" ++dependencies = [ ++ "elliptic-curve", ++] ++ ++[[package]] ++name = "proc-macro-error" ++version = "1.0.4" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" ++dependencies = [ ++ "proc-macro-error-attr", ++ "proc-macro2", ++ "quote", ++ "syn", ++ "version_check", ++] ++ ++[[package]] ++name = "proc-macro-error-attr" ++version = "1.0.4" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" ++dependencies = [ ++ "proc-macro2", ++ "quote", ++ "version_check", ++] + +[[package]] +name = "proc-macro2" -+version = "1.0.43" ++version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" ++checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] ++name = "prost" ++version = "0.11.8" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "e48e50df39172a3e7eb17e14642445da64996989bc212b583015435d39a58537" ++dependencies = [ ++ "bytes", ++ "prost-derive", ++] ++ ++[[package]] ++name = "prost-build" ++version = "0.11.8" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "2c828f93f5ca4826f97fedcbd3f9a536c16b12cff3dbbb4a007f932bbad95b12" ++dependencies = [ ++ "bytes", ++ "heck", ++ "itertools", ++ "lazy_static", ++ "log", ++ "multimap", ++ "petgraph", ++ "prettyplease", ++ "prost", ++ "prost-types", ++ "regex", ++ "syn", ++ "tempfile", ++ "which", ++] ++ ++[[package]] ++name = "prost-derive" ++version = "0.11.8" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "4ea9b0f8cbe5e15a8a042d030bd96668db28ecb567ec37d691971ff5731d2b1b" ++dependencies = [ ++ "anyhow", ++ "itertools", ++ "proc-macro2", ++ "quote", ++ "syn", ++] ++ ++[[package]] ++name = "prost-types" ++version = "0.11.8" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "379119666929a1afd7a043aa6cf96fa67a6dce9af60c88095a4686dbce4c9c88" ++dependencies = [ ++ "prost", ++] ++ ++[[package]] +name = "quote" -+version = "1.0.21" ++version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" ++checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" ++version = "0.7.3" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" ++dependencies = [ ++ "getrandom 0.1.16", ++ "libc", ++ "rand_chacha 0.2.2", ++ "rand_core 0.5.1", ++ "rand_hc", ++] ++ ++[[package]] ++name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", -+ "rand_chacha", -+ "rand_core", ++ "rand_chacha 0.3.1", ++ "rand_core 0.6.4", ++] ++ ++[[package]] ++name = "rand_chacha" ++version = "0.2.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" ++dependencies = [ ++ "ppv-lite86", ++ "rand_core 0.5.1", +] + +[[package]] @@ -797,16 +1794,45 @@ index 0000000..3f52488 +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", -+ "rand_core", ++ "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" -+version = "0.6.3" ++version = "0.5.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" ++dependencies = [ ++ "getrandom 0.1.16", ++] ++ ++[[package]] ++name = "rand_core" ++version = "0.6.4" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" ++dependencies = [ ++ "getrandom 0.2.8", ++] ++ ++[[package]] ++name = "rand_hc" ++version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" ++checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ -+ "getrandom", ++ "rand_core 0.5.1", ++] ++ ++[[package]] ++name = "rcgen" ++version = "0.9.2" ++source = "git+https://github.com/wireapp/rcgen?tag=v1.2.0-pre.core-crypto-0.6.0#1e893c3444a9a1625dc8f3fe7be6df025f64646a" ++dependencies = [ ++ "pem", ++ "ring 0.17.0-not-released-yet", ++ "time 0.3.20", ++ "yasna", +] + +[[package]] @@ -819,21 +1845,104 @@ index 0000000..3f52488 +] + +[[package]] ++name = "regex" ++version = "1.7.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" ++dependencies = [ ++ "aho-corasick", ++ "memchr", ++ "regex-syntax", ++] ++ ++[[package]] ++name = "regex-syntax" ++version = "0.6.28" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" ++ ++[[package]] ++name = "reqwest" ++version = "0.11.14" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9" ++dependencies = [ ++ "base64 0.21.0", ++ "bytes", ++ "encoding_rs", ++ "futures-core", ++ "futures-util", ++ "h2", ++ "http", ++ "http-body", ++ "hyper", ++ "hyper-rustls", ++ "ipnet", ++ "js-sys", ++ "log", ++ "mime", ++ "once_cell", ++ "percent-encoding", ++ "pin-project-lite", ++ "rustls", ++ "rustls-pemfile", ++ "serde", ++ "serde_json", ++ "serde_urlencoded", ++ "tokio", ++ "tokio-rustls", ++ "tower-service", ++ "url", ++ "wasm-bindgen", ++ "wasm-bindgen-futures", ++ "web-sys", ++ "winreg", ++] ++ ++[[package]] +name = "rfc6979" -+version = "0.3.0" ++version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "88c86280f057430a52f4861551b092a01b419b8eacefc7c995eacb9dc132fe32" ++checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ -+ "crypto-bigint 0.4.8", ++ "crypto-bigint", + "hmac", + "zeroize", +] + +[[package]] ++name = "ring" ++version = "0.16.20" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" ++dependencies = [ ++ "cc", ++ "libc", ++ "once_cell", ++ "spin 0.5.2", ++ "untrusted 0.7.1", ++ "web-sys", ++ "winapi", ++] ++ ++[[package]] ++name = "ring" ++version = "0.17.0-not-released-yet" ++source = "git+https://github.com/briansmith/ring.git?rev=450ada28#450ada288f1805795140097ec96396b890bcf722" ++dependencies = [ ++ "cc", ++ "getrandom 0.2.8", ++ "libc", ++ "spin 0.9.5", ++ "untrusted 0.9.0", ++ "winapi", ++] ++ ++[[package]] +name = "rsa" -+version = "0.6.1" ++version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "4cf22754c49613d2b3b119f0e5d46e34a2c628a937e3024b8762de4e7d8c710b" ++checksum = "094052d5470cbcef561cb848a7209968c9f12dfa6d668f4bca048ac5de51099c" +dependencies = [ + "byteorder", + "digest", @@ -842,8 +1951,9 @@ index 0000000..3f52488 + "num-iter", + "num-traits", + "pkcs1", -+ "pkcs8 0.8.0", -+ "rand_core", ++ "pkcs8", ++ "rand_core 0.6.4", ++ "signature 1.6.4", + "smallvec", + "subtle", + "zeroize", @@ -851,9 +1961,9 @@ index 0000000..3f52488 + +[[package]] +name = "rstest" -+version = "0.15.0" ++version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "e9c9dc66cc29792b663ffb5269be669f1613664e69ad56441fdb895c2347b930" ++checksum = "b07f2d176c472198ec1e6551dc7da28f1c089652f66a7b722676c2238ebc0edf" +dependencies = [ + "futures", + "futures-timer", @@ -863,24 +1973,26 @@ index 0000000..3f52488 + +[[package]] +name = "rstest_macros" -+version = "0.14.0" ++version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "5015e68a0685a95ade3eee617ff7101ab6a3fc689203101ca16ebc16f2b89c66" ++checksum = "7229b505ae0706e64f37ffc54a9c163e11022a6636d58fe1f3f52018257ff9f7" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "rustc_version", + "syn", ++ "unicode-ident", +] + +[[package]] +name = "rstest_reuse" -+version = "0.4.0" ++version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "f9b5aed35457441e7e0db509695ba3932d4c47e046777141c167efe584d0ec17" ++checksum = "45f80dcc84beab3a327bbe161f77db25f336a1452428176787c8c79ac79d7073" +dependencies = [ + "quote", ++ "rand 0.8.5", + "rustc_version", + "syn", +] @@ -895,89 +2007,418 @@ index 0000000..3f52488 +] + +[[package]] ++name = "rusticata-macros" ++version = "4.1.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" ++dependencies = [ ++ "nom", ++] ++ ++[[package]] ++name = "rustix" ++version = "0.36.8" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644" ++dependencies = [ ++ "bitflags", ++ "errno", ++ "io-lifetimes", ++ "libc", ++ "linux-raw-sys", ++ "windows-sys 0.45.0", ++] ++ ++[[package]] ++name = "rustls" ++version = "0.20.8" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" ++dependencies = [ ++ "log", ++ "ring 0.16.20", ++ "sct", ++ "webpki", ++] ++ ++[[package]] ++name = "rustls-pemfile" ++version = "1.0.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" ++dependencies = [ ++ "base64 0.21.0", ++] ++ ++[[package]] ++name = "rusty-acme" ++version = "0.2.0" ++dependencies = [ ++ "base64 0.21.0", ++ "jwt-simple", ++ "rcgen", ++ "reqwest", ++ "rusty-jwt-tools", ++ "serde", ++ "serde_json", ++ "testcontainers", ++ "thiserror", ++ "time 0.3.20", ++ "url", ++ "wasm-bindgen-test", ++ "x509-parser", ++] ++ ++[[package]] ++name = "rusty-jwt-cli" ++version = "0.2.0" ++dependencies = [ ++ "anyhow", ++ "clap", ++ "console", ++ "jwt-simple", ++ "rusty-jwt-tools", ++ "serde_json", ++] ++ ++[[package]] +name = "rusty-jwt-tools" -+version = "0.1.0" ++version = "0.2.0" +dependencies = [ -+ "base64", ++ "base64 0.21.0", ++ "biscuit", ++ "chrono", + "ed25519-compact", -+ "elliptic-curve", ++ "either", + "fluvio-wasm-timer", -+ "getrandom", ++ "indexmap", ++ "josekit", ++ "json-patch", + "jwt-simple", + "p256", + "p384", -+ "rand", ++ "rand 0.8.5", ++ "rand_chacha 0.3.1", ++ "reqwest", + "rstest", + "rstest_reuse", ++ "sec1", + "serde", + "serde_json", ++ "sha2", + "thiserror", ++ "time 0.3.20", + "url", + "uuid", + "wasm-bindgen-test", ++ "zeroize", +] + +[[package]] +name = "rusty-jwt-tools-ffi" -+version = "0.1.0" ++version = "0.2.0" +dependencies = [ -+ "cfg-if", + "rusty-jwt-tools", ++ "uuid", +] + +[[package]] +name = "ryu" -+version = "1.0.11" ++version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" ++checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" ++ ++[[package]] ++name = "scoped-tls" ++version = "1.0.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" ++ ++[[package]] ++name = "scopeguard" ++version = "1.1.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" ++ ++[[package]] ++name = "scratch" ++version = "1.0.3" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" ++ ++[[package]] ++name = "sct" ++version = "0.7.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" ++dependencies = [ ++ "ring 0.16.20", ++ "untrusted 0.7.1", ++] ++ ++[[package]] ++name = "sec1" ++version = "0.3.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" ++dependencies = [ ++ "base16ct", ++ "der", ++ "generic-array", ++ "pkcs8", ++ "subtle", ++ "zeroize", ++] ++ ++[[package]] ++name = "semver" ++version = "1.0.16" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" ++ ++[[package]] ++name = "serde" ++version = "1.0.152" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" ++dependencies = [ ++ "serde_derive", ++] ++ ++[[package]] ++name = "serde_derive" ++version = "1.0.152" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" ++dependencies = [ ++ "proc-macro2", ++ "quote", ++ "syn", ++] ++ ++[[package]] ++name = "serde_json" ++version = "1.0.93" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" ++dependencies = [ ++ "indexmap", ++ "itoa", ++ "ryu", ++ "serde", ++] ++ ++[[package]] ++name = "serde_qs" ++version = "0.8.5" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" ++dependencies = [ ++ "percent-encoding", ++ "serde", ++ "thiserror", ++] ++ ++[[package]] ++name = "serde_urlencoded" ++version = "0.7.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" ++dependencies = [ ++ "form_urlencoded", ++ "itoa", ++ "ryu", ++ "serde", ++] ++ ++[[package]] ++name = "serde_with" ++version = "1.14.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" ++dependencies = [ ++ "serde", ++ "serde_with_macros", ++] ++ ++[[package]] ++name = "serde_with_macros" ++version = "1.5.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" ++dependencies = [ ++ "darling", ++ "proc-macro2", ++ "quote", ++ "syn", ++] ++ ++[[package]] ++name = "sha2" ++version = "0.10.6" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" ++dependencies = [ ++ "cfg-if", ++ "cpufeatures", ++ "digest", ++] ++ ++[[package]] ++name = "signature" ++version = "1.6.4" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" ++dependencies = [ ++ "digest", ++ "rand_core 0.6.4", ++] ++ ++[[package]] ++name = "signature" ++version = "2.0.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "8fe458c98333f9c8152221191a77e2a44e8325d0193484af2e9421a53019e57d" ++dependencies = [ ++ "digest", ++ "rand_core 0.6.4", ++] ++ ++[[package]] ++name = "slab" ++version = "0.4.8" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" ++dependencies = [ ++ "autocfg", ++] ++ ++[[package]] ++name = "smallvec" ++version = "1.10.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" ++ ++[[package]] ++name = "socket2" ++version = "0.4.7" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" ++dependencies = [ ++ "libc", ++ "winapi", ++] ++ ++[[package]] ++name = "spin" ++version = "0.5.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" ++ ++[[package]] ++name = "spin" ++version = "0.9.5" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "7dccf47db1b41fa1573ed27ccf5e08e3ca771cb994f776668c5ebda893b248fc" ++ ++[[package]] ++name = "spki" ++version = "0.6.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" ++dependencies = [ ++ "base64ct", ++ "der", ++] ++ ++[[package]] ++name = "strsim" ++version = "0.10.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" ++ ++[[package]] ++name = "subtle" ++version = "2.4.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" ++ ++[[package]] ++name = "syn" ++version = "1.0.109" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" ++dependencies = [ ++ "proc-macro2", ++ "quote", ++ "unicode-ident", ++] + +[[package]] -+name = "scoped-tls" -+version = "1.0.0" ++name = "synstructure" ++version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" ++checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" ++dependencies = [ ++ "proc-macro2", ++ "quote", ++ "syn", ++ "unicode-xid", ++] + +[[package]] -+name = "scopeguard" -+version = "1.1.0" ++name = "tempfile" ++version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" ++checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" ++dependencies = [ ++ "cfg-if", ++ "fastrand", ++ "redox_syscall", ++ "rustix", ++ "windows-sys 0.42.0", ++] + +[[package]] -+name = "sec1" -+version = "0.3.0" ++name = "termcolor" ++version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" ++checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ -+ "base16ct", -+ "der 0.6.0", -+ "generic-array", -+ "pkcs8 0.9.0", -+ "serdect", -+ "subtle", -+ "zeroize", ++ "winapi-util", +] + +[[package]] -+name = "semver" -+version = "1.0.13" ++name = "testcontainers" ++version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "93f6841e709003d68bb2deee8c343572bf446003ec20a583e76f7b15cebf3711" ++checksum = "0e2b1567ca8a2b819ea7b28c92be35d9f76fb9edb214321dcc86eb96023d1f87" ++dependencies = [ ++ "bollard-stubs", ++ "futures", ++ "hex", ++ "hmac", ++ "log", ++ "rand 0.8.5", ++ "serde", ++ "serde_json", ++ "sha2", ++] + +[[package]] -+name = "serde" -+version = "1.0.144" ++name = "thiserror" ++version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" ++checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ -+ "serde_derive", ++ "thiserror-impl", +] + +[[package]] -+name = "serde_derive" -+version = "1.0.144" ++name = "thiserror-impl" ++version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" ++checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", @@ -985,168 +2426,219 @@ index 0000000..3f52488 +] + +[[package]] -+name = "serde_json" -+version = "1.0.85" ++name = "time" ++version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" ++checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ -+ "itoa", -+ "ryu", -+ "serde", ++ "libc", ++ "wasi 0.10.0+wasi-snapshot-preview1", ++ "winapi", +] + +[[package]] -+name = "serdect" -+version = "0.1.0" ++name = "time" ++version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "038fce1bf4d74b9b30ea7dcd59df75ba8ec669a5dcb3cc64fbfcef7334ced32c" ++checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +dependencies = [ -+ "base16ct", ++ "itoa", ++ "js-sys", + "serde", ++ "time-core", ++ "time-macros", +] + +[[package]] -+name = "sha2" -+version = "0.10.5" ++name = "time-core" ++version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "cf9db03534dff993187064c4e0c05a5708d2a9728ace9a8959b77bedf415dac5" -+dependencies = [ -+ "cfg-if", -+ "cpufeatures", -+ "digest", -+] ++checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] -+name = "signature" -+version = "1.6.0" ++name = "time-macros" ++version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "f0ea32af43239f0d353a7dd75a22d94c329c8cdaafdcb4c1c1335aa10c298a4a" ++checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +dependencies = [ -+ "digest", -+ "rand_core", ++ "time-core", +] + +[[package]] -+name = "slab" -+version = "0.4.7" ++name = "tinyvec" ++version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" ++checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ -+ "autocfg", ++ "tinyvec_macros", +] + +[[package]] -+name = "smallvec" -+version = "1.9.0" ++name = "tinyvec_macros" ++version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" ++checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] -+name = "spin" -+version = "0.5.2" ++name = "tokio" ++version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" ++checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" ++dependencies = [ ++ "autocfg", ++ "bytes", ++ "libc", ++ "memchr", ++ "mio", ++ "num_cpus", ++ "pin-project-lite", ++ "socket2", ++ "tokio-macros", ++ "windows-sys 0.42.0", ++] + +[[package]] -+name = "spki" -+version = "0.5.4" ++name = "tokio-macros" ++version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" ++checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +dependencies = [ -+ "base64ct", -+ "der 0.5.1", ++ "proc-macro2", ++ "quote", ++ "syn", +] + +[[package]] -+name = "spki" -+version = "0.6.0" ++name = "tokio-rustls" ++version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" ++checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ -+ "base64ct", -+ "der 0.6.0", ++ "rustls", ++ "tokio", ++ "webpki", +] + +[[package]] -+name = "subtle" -+version = "2.4.1" ++name = "tokio-util" ++version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" ++checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" ++dependencies = [ ++ "bytes", ++ "futures-core", ++ "futures-sink", ++ "pin-project-lite", ++ "tokio", ++ "tracing", ++] + +[[package]] -+name = "syn" -+version = "1.0.99" ++name = "tonic-build" ++version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" ++checksum = "5bf5e9b9c0f7e0a7c027dcfaba7b2c60816c7049171f679d99ee2ff65d0de8c4" +dependencies = [ ++ "prettyplease", + "proc-macro2", ++ "prost-build", + "quote", -+ "unicode-ident", ++ "syn", +] + +[[package]] -+name = "thiserror" -+version = "1.0.34" ++name = "tower-service" ++version = "0.3.2" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" ++ ++[[package]] ++name = "tracing" ++version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "8c1b05ca9d106ba7d2e31a9dab4a64e7be2cce415321966ea3132c49a656e252" ++checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ -+ "thiserror-impl", ++ "cfg-if", ++ "pin-project-lite", ++ "tracing-core", +] + +[[package]] -+name = "thiserror-impl" -+version = "1.0.34" ++name = "tracing-core" ++version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "e8f2591983642de85c921015f3f070c665a197ed69e417af436115e3a1407487" ++checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ -+ "proc-macro2", -+ "quote", -+ "syn", ++ "once_cell", +] + +[[package]] -+name = "tinyvec" -+version = "1.6.0" ++name = "treediff" ++version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" ++checksum = "52984d277bdf2a751072b5df30ec0377febdb02f7696d64c2d7d54630bac4303" +dependencies = [ -+ "tinyvec_macros", ++ "serde_json", +] + +[[package]] -+name = "tinyvec_macros" -+version = "0.1.0" ++name = "try-lock" ++version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" ++checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "typenum" -+version = "1.15.0" ++version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" ++checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicode-bidi" -+version = "0.3.8" ++version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" ++checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58" + +[[package]] +name = "unicode-ident" -+version = "1.0.3" ++version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" ++checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "unicode-normalization" -+version = "0.1.21" ++version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" ++checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] ++name = "unicode-width" ++version = "0.1.10" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" ++ ++[[package]] ++name = "unicode-xid" ++version = "0.2.4" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" ++ ++[[package]] ++name = "untrusted" ++version = "0.7.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" ++ ++[[package]] ++name = "untrusted" ++version = "0.9.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" ++ ++[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1160,20 +2652,54 @@ index 0000000..3f52488 + +[[package]] +name = "uuid" -+version = "1.1.2" ++version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" ++checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" +dependencies = [ -+ "getrandom", ++ "getrandom 0.2.8", +] + +[[package]] ++name = "vcpkg" ++version = "0.2.15" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" ++ ++[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] ++name = "waker-fn" ++version = "1.1.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" ++ ++[[package]] ++name = "want" ++version = "0.3.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" ++dependencies = [ ++ "log", ++ "try-lock", ++] ++ ++[[package]] ++name = "wasi" ++version = "0.9.0+wasi-snapshot-preview1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" ++ ++[[package]] ++name = "wasi" ++version = "0.10.0+wasi-snapshot-preview1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" ++ ++[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1181,9 +2707,9 @@ index 0000000..3f52488 + +[[package]] +name = "wasm-bindgen" -+version = "0.2.82" ++version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" ++checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", @@ -1191,9 +2717,9 @@ index 0000000..3f52488 + +[[package]] +name = "wasm-bindgen-backend" -+version = "0.2.82" ++version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" ++checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +dependencies = [ + "bumpalo", + "log", @@ -1206,9 +2732,9 @@ index 0000000..3f52488 + +[[package]] +name = "wasm-bindgen-futures" -+version = "0.4.32" ++version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "fa76fb221a1f8acddf5b54ace85912606980ad661ac7a503b4570ffd3a624dad" ++checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" +dependencies = [ + "cfg-if", + "js-sys", @@ -1218,9 +2744,9 @@ index 0000000..3f52488 + +[[package]] +name = "wasm-bindgen-macro" -+version = "0.2.82" ++version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" ++checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", @@ -1228,9 +2754,9 @@ index 0000000..3f52488 + +[[package]] +name = "wasm-bindgen-macro-support" -+version = "0.2.82" ++version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" ++checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +dependencies = [ + "proc-macro2", + "quote", @@ -1241,15 +2767,15 @@ index 0000000..3f52488 + +[[package]] +name = "wasm-bindgen-shared" -+version = "0.2.82" ++version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" ++checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" + +[[package]] +name = "wasm-bindgen-test" -+version = "0.3.32" ++version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "513df541345bb9fcc07417775f3d51bbb677daf307d8035c0afafd87dc2e6599" ++checksum = "6db36fc0f9fb209e88fb3642590ae0205bb5a56216dabd963ba15879fe53a30b" +dependencies = [ + "console_error_panic_hook", + "js-sys", @@ -1261,9 +2787,9 @@ index 0000000..3f52488 + +[[package]] +name = "wasm-bindgen-test-macro" -+version = "0.3.32" ++version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "6150d36a03e90a3cf6c12650be10626a9902d70c5270fd47d7a47e5389a10d56" ++checksum = "0734759ae6b3b1717d661fe4f016efcfb9828f5edb4520c18eaee05af3b43be9" +dependencies = [ + "proc-macro2", + "quote", @@ -1271,15 +2797,36 @@ index 0000000..3f52488 + +[[package]] +name = "web-sys" -+version = "0.3.59" ++version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" -+checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" ++checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] ++name = "webpki" ++version = "0.22.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" ++dependencies = [ ++ "ring 0.16.20", ++ "untrusted 0.7.1", ++] ++ ++[[package]] ++name = "which" ++version = "4.4.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" ++dependencies = [ ++ "either", ++ "libc", ++ "once_cell", ++] ++ ++[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1296,16 +2843,183 @@ index 0000000..3f52488 +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] ++name = "winapi-util" ++version = "0.1.5" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" ++dependencies = [ ++ "winapi", ++] ++ ++[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] ++name = "windows-sys" ++version = "0.42.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" ++dependencies = [ ++ "windows_aarch64_gnullvm", ++ "windows_aarch64_msvc", ++ "windows_i686_gnu", ++ "windows_i686_msvc", ++ "windows_x86_64_gnu", ++ "windows_x86_64_gnullvm", ++ "windows_x86_64_msvc", ++] ++ ++[[package]] ++name = "windows-sys" ++version = "0.45.0" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" ++dependencies = [ ++ "windows-targets", ++] ++ ++[[package]] ++name = "windows-targets" ++version = "0.42.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" ++dependencies = [ ++ "windows_aarch64_gnullvm", ++ "windows_aarch64_msvc", ++ "windows_i686_gnu", ++ "windows_i686_msvc", ++ "windows_x86_64_gnu", ++ "windows_x86_64_gnullvm", ++ "windows_x86_64_msvc", ++] ++ ++[[package]] ++name = "windows_aarch64_gnullvm" ++version = "0.42.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" ++ ++[[package]] ++name = "windows_aarch64_msvc" ++version = "0.42.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" ++ ++[[package]] ++name = "windows_i686_gnu" ++version = "0.42.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" ++ ++[[package]] ++name = "windows_i686_msvc" ++version = "0.42.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" ++ ++[[package]] ++name = "windows_x86_64_gnu" ++version = "0.42.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" ++ ++[[package]] ++name = "windows_x86_64_gnullvm" ++version = "0.42.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" ++ ++[[package]] ++name = "windows_x86_64_msvc" ++version = "0.42.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" ++ ++[[package]] ++name = "winreg" ++version = "0.10.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" ++dependencies = [ ++ "winapi", ++] ++ ++[[package]] ++name = "wire-e2e-identity" ++version = "0.2.0" ++dependencies = [ ++ "asserhttp", ++ "base64 0.21.0", ++ "derive_more", ++ "hex", ++ "hyper", ++ "itertools", ++ "jwt-simple", ++ "rand 0.8.5", ++ "reqwest", ++ "rusty-acme", ++ "rusty-jwt-tools", ++ "serde", ++ "serde_json", ++ "testcontainers", ++ "thiserror", ++ "tokio", ++ "url", ++ "uuid", ++ "wasm-bindgen-test", ++ "x509-parser", ++] ++ ++[[package]] ++name = "x509-parser" ++version = "0.14.0" ++source = "git+https://github.com/wireapp/x509-parser?tag=v1.0.2-pre.core-crypto-0.6.0#4fbc4c795fc84537ba24eb91e563c9e80d3bd5d4" ++dependencies = [ ++ "asn1-rs", ++ "base64 0.21.0", ++ "chrono", ++ "data-encoding", ++ "der-parser", ++ "lazy_static", ++ "nom", ++ "oid-registry", ++ "ring 0.17.0-not-released-yet", ++ "rusticata-macros", ++ "thiserror", ++] ++ ++[[package]] ++name = "yasna" ++version = "0.5.1" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "aed2e7a52e3744ab4d0c05c20aa065258e84c49fd4226f5191b2ed29712710b4" ++dependencies = [ ++ "time 0.3.20", ++] ++ ++[[package]] +name = "zeroize" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" --- ++dependencies = [ ++ "zeroize_derive", ++] ++ ++[[package]] ++name = "zeroize_derive" ++version = "1.3.3" ++source = "registry+https://github.com/rust-lang/crates.io-index" ++checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c" ++dependencies = [ ++ "proc-macro2", ++ "quote", ++ "syn", ++ "synstructure", ++] +-- 2.34.1 diff --git a/nix/pkgs/rusty_jwt_tools_ffi/default.nix b/nix/pkgs/rusty_jwt_tools_ffi/default.nix index f59bcd9bcd..04cd3597c7 100644 --- a/nix/pkgs/rusty_jwt_tools_ffi/default.nix +++ b/nix/pkgs/rusty_jwt_tools_ffi/default.nix @@ -1,20 +1,27 @@ { fetchFromGitHub , lib , rustPlatform +, pkg-config +, perl +, gitMinimal }: rustPlatform.buildRustPackage rec { name = "rusty_jwt-tools_ffi-${version}"; - version = "0.1.0"; + version = "0.2.0"; + nativeBuildInputs = [ pkg-config perl gitMinimal ]; src = fetchFromGitHub { owner = "wireapp"; repo = "rusty-jwt-tools"; # if you update this, please generate a new Cargo.lock file es described below at `cargoPatches` - rev = "6370cd556f03f6834d0b8043615ffaf0044ef1fa"; - sha256 = "sha256-vnTvKITie4pu+ISIl/RdYPfb/yWCdCI9eHl1KcZb050="; + rev = "a68ed483f7e98613c0d5c3608c684f25225a58d3"; + sha256 = "sha256-+2fjwtG80l8Vt48QWKm4wevY7MQRAwuo4YFbjB+6w9I="; }; - cargoBuildFeatures = "haskell"; - cargoSha256 = "sha256-9etHOl3B/ybKdKMRUDb/VPxg4ghlIe75smWuupLORU8="; + doCheck = false; + cargoSha256 = "sha256-BHq28U3OzYCPNmfnxlmXsz9XYEy1kRiNrFM9OTnAkk0="; + cargoDepsHook = '' + mkdir -p rusty_jwt-tools_ffi-${version}-vendor.tar.gz/ring/.git + ''; cargoPatches = [ # a patch file to add/update Cargo.lock in the source code # it's good practice not to add Cargo.lock to the source code for libraries @@ -23,7 +30,7 @@ rustPlatform.buildRustPackage rec { # - `git clone git@github.com:wireapp/rusty-jwt-tools.git` # - checkout the commit specified in `rev` # - create a new branch: `git checkout -b patch-cargo-lock-` (replace `` with the commit hash) - # - `cargo build --release --features haskell` + # - `cargo clean && cargo build --release` # - `git add -f Cargo.lock` # - `git commit -am "generate new cargo.lock"` # - `git format-patch main` diff --git a/nix/pkgs/zauth/default.nix b/nix/pkgs/zauth/default.nix index 5ae2d991a6..ef1248b73c 100644 --- a/nix/pkgs/zauth/default.nix +++ b/nix/pkgs/zauth/default.nix @@ -5,12 +5,13 @@ , pkg-config , rustPlatform , stdenv +, gitMinimal }: rustPlatform.buildRustPackage rec { name = "libzauth-${version}"; version = "3.0.0"; - nativeBuildInputs = [ pkg-config ]; + nativeBuildInputs = [ pkg-config gitMinimal ]; buildInputs = [ libsodium ]; src = nix-gitignore.gitignoreSourcePure [ ../../../.gitignore ] ../../../libs/libzauth; sourceRoot = "libzauth/libzauth-c"; diff --git a/nix/sources.json b/nix/sources.json index 2c25068a16..4ab28f3ee1 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -5,10 +5,10 @@ "homepage": "https://github.com/NixOS/nixpkgs", "owner": "NixOS", "repo": "nixpkgs", - "rev": "1f3ebb2bd1a353a42e8f833895c26d8415c7b791", - "sha256": "03y1a3lv44b4fdnykyms5nd24v2mqn8acz1xa4jkbmryc29rsgcw", + "rev": "8c619a1f3cedd16ea172146e30645e703d21bfc1", + "sha256": "1zgm94b79yfqfdcqcj8bxfjzcl7gp9pd7jph9jnlxx8m36cgsn3a", "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/1f3ebb2bd1a353a42e8f833895c26d8415c7b791.tar.gz", + "url": "https://github.com/NixOS/nixpkgs/archive/8c619a1f3cedd16ea172146e30645e703d21bfc1.tar.gz", "url_template": "https://github.com///archive/.tar.gz" } } diff --git a/nix/wire-server.nix b/nix/wire-server.nix index 586338588f..8fc1b22edd 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -145,7 +145,7 @@ let ) executablesMap; - hPkgs = localMods@{ enableOptimization, enableDocs, enableTests }: pkgs.haskell.packages.ghc8107.override { + hPkgs = localMods@{ enableOptimization, enableDocs, enableTests }: pkgs.haskell.packages.ghc92.override { overrides = lib.composeManyExtensions [ pinnedPackages (localPackages localMods) @@ -298,6 +298,8 @@ let pkgs.cabal2nix pkgs.gnumake pkgs.gnused + pkgs.parallel + pkgs.ripgrep pkgs.helm pkgs.helmfile pkgs.hlint @@ -308,6 +310,8 @@ let pkgs.ormolu pkgs.shellcheck pkgs.treefmt + pkgs.gawk + pkgs.cfssl (hlib.justStaticExecutables pkgs.haskellPackages.cabal-fmt) ] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.skopeo @@ -363,13 +367,13 @@ in devEnv = pkgs.buildEnv { name = "wire-server-dev-env"; paths = commonTools ++ [ - (pkgs.haskell-language-server.override { supportedGhcVersions = [ "8107" ]; }) + (pkgs.haskell-language-server.override { supportedGhcVersions = [ "92" ]; }) pkgs.ghcid - pkgs.cfssl pkgs.kind pkgs.netcat pkgs.niv - pkgs.python3 + (pkgs.python3.withPackages + (ps: with ps; [ pyyaml ])) pkgs.rsync pkgs.wget pkgs.yq diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index ef708454da..3e4d9ef358 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -17,6 +17,7 @@ extra-source-files: library -- cabal-fmt: expand src exposed-modules: + Brig.Allowlists Brig.API Brig.API.Auth Brig.API.Client @@ -130,9 +131,7 @@ library Brig.User.Search.TeamUserSearch Brig.User.Template Brig.Version - Brig.Whitelist Brig.ZAuth - Main other-modules: Paths_brig hs-source-dirs: src @@ -180,6 +179,7 @@ library -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -funbox-strict-fields -fplugin=Polysemy.Plugin + -fplugin=TransitiveAnns.Plugin -Wredundant-constraints build-depends: aeson >=2.0.1.0 @@ -248,7 +248,6 @@ library , mmorph , MonadRandom >=0.5 , mtl >=2.1 - , multihash >=0.1.3 , mwc-random , network >=2.4 , network-conduit-tls @@ -281,7 +280,6 @@ library , statistics >=0.13 , stomp-queue >=0.3 , string-conversions - , swagger >=0.1 , swagger2 , tagged , template >=0.2 @@ -293,6 +291,7 @@ library , time-units , tinylog >=0.10 , transformers >=0.3 + , transitive-anns , types-common >=0.16 , types-common-aws , types-common-journal >=0.1 @@ -316,7 +315,7 @@ library default-language: Haskell2010 executable brig - main-is: src/Main.hs + main-is: exec/Main.hs other-modules: Paths_brig default-extensions: NoImplicitPrelude @@ -362,7 +361,7 @@ executable brig -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -funbox-strict-fields -threaded -with-rtsopts=-N -with-rtsopts=-T - -rtsopts + -rtsopts -Wredundant-constraints build-depends: base @@ -421,6 +420,7 @@ executable brig-index -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -funbox-strict-fields -threaded -with-rtsopts=-N + -Wredundant-constraints build-depends: base @@ -516,7 +516,8 @@ executable brig-integration ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -funbox-strict-fields + -funbox-strict-fields -threaded -with-rtsopts=-N + -Wredundant-constraints build-depends: aeson @@ -619,37 +620,6 @@ executable brig-schema -- cabal-fmt: expand schema/src other-modules: Main - V10 - V11 - V12 - V13 - V14 - V15 - V16 - V17 - V18 - V19 - V20 - V21 - V22 - V23 - V24 - V25 - V28 - V29 - V30 - V31 - V32 - V33 - V34 - V35 - V36 - V37 - V38 - V39 - V40 - V41 - V42 V43 V44 V45 @@ -681,7 +651,6 @@ executable brig-schema V71_AddTableVCodesThrottle V72_AddNonceTable V73_ReplaceNonceTable - V9 V_FUTUREWORK hs-source-dirs: schema/src @@ -728,7 +697,7 @@ executable brig-schema ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -funbox-strict-fields + -funbox-strict-fields -Wredundant-constraints build-depends: base @@ -803,6 +772,7 @@ test-suite brig-tests -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -funbox-strict-fields -threaded -with-rtsopts=-N + -Wredundant-constraints build-depends: aeson diff --git a/services/brig/brig.integration.yaml b/services/brig/brig.integration.yaml index e990d9be5f..ae6441ab88 100644 --- a/services/brig/brig.integration.yaml +++ b/services/brig/brig.integration.yaml @@ -138,9 +138,9 @@ turn: tokenTTL: 21600 optSettings: - setActivationTimeout: 5 - setVerificationTimeout: 5 - setTeamInvitationTimeout: 5 + setActivationTimeout: 10 + setVerificationTimeout: 10 + setTeamInvitationTimeout: 10 setExpiredUserCleanupTimeout: 1 setTwilio: test/resources/twilio-credentials.yaml setNexmo: test/resources/nexmo-credentials.yaml @@ -193,8 +193,12 @@ optSettings: setDpopTokenExpirationTimeSecs: 300 # 5 minutes setPublicKeyBundle: test/resources/jwt/ed25519_bundle.pem setEnableMLS: true + # To only allow specific email address domains to register, uncomment and update the setting below + # setAllowlistEmailDomains: + # - wire.com + # To only allow specific phone number prefixes to register uncomment and update the settings below + # setAllowlistPhonePrefixes: + # - "+1555555" logLevel: Warn -# ^ NOTE: We log too much in brig, if we set this to Info like other services, running tests -# produces too many logs, hence this is set to Warn. logNetStrings: false diff --git a/services/brig/default.nix b/services/brig/default.nix index 6c98c83546..c5c3f34187 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -80,7 +80,6 @@ , mmorph , MonadRandom , mtl -, multihash , mwc-random , network , network-conduit-tls @@ -121,7 +120,6 @@ , stomp-queue , streaming-commons , string-conversions -, swagger , swagger2 , tagged , tasty @@ -138,6 +136,7 @@ , time-units , tinylog , transformers +, transitive-anns , types-common , types-common-aws , types-common-journal @@ -233,7 +232,6 @@ mkDerivation { mmorph MonadRandom mtl - multihash mwc-random network network-conduit-tls @@ -266,7 +264,6 @@ mkDerivation { statistics stomp-queue string-conversions - swagger swagger2 tagged template @@ -278,6 +275,7 @@ mkDerivation { time-units tinylog transformers + transitive-anns types-common types-common-aws types-common-journal diff --git a/services/brig/docs/swagger-v3.json b/services/brig/docs/swagger-v3.json new file mode 100644 index 0000000000..c366175b56 --- /dev/null +++ b/services/brig/docs/swagger-v3.json @@ -0,0 +1,20848 @@ +{ + "definitions": { + "": { + "description": "Username to use for authenticating against the given TURN servers", + "type": "string" + }, + "ASCII": { + "example": "aGVsbG8", + "type": "string" + }, + "Access": { + "description": "Transport", + "enum": [ + "GCM", + "APNS", + "APNS_SANDBOX", + "APNS_VOIP", + "APNS_VOIP_SANDBOX" + ], + "type": "string" + }, + "AccessRole": { + "description": "Which users/services can join conversations. This replaces legacy access roles and allows a more fine grained configuration of access roles, and in particular a separation of guest and services access.\n\nThis field is optional. If it is not present, the default will be `[team_member, non_team_member, service]`. Please note that an empty list is not allowed when creating a new conversation.", + "enum": [ + "team_member", + "non_team_member", + "guest", + "service" + ], + "type": "string" + }, + "AccessRoleLegacy": { + "description": "Deprecated, please use access_role_v2", + "enum": [ + "private", + "team", + "activated", + "non_activated" + ], + "type": "string" + }, + "AccessToken": { + "properties": { + "access_token": { + "description": "The opaque access token string", + "type": "string" + }, + "expires_in": { + "description": "The number of seconds this token is valid", + "type": "integer" + }, + "token_type": { + "$ref": "#/definitions/TokenType" + }, + "user": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "user", + "access_token", + "token_type", + "expires_in" + ], + "type": "object" + }, + "AccessTokenType": { + "enum": [ + "DPoP" + ], + "type": "string" + }, + "Action": { + "enum": [ + "add_conversation_member", + "remove_conversation_member", + "modify_conversation_name", + "modify_conversation_message_timer", + "modify_conversation_receipt_mode", + "modify_conversation_access", + "modify_other_conversation_member", + "leave_conversation", + "delete_conversation" + ], + "type": "string" + }, + "Activate": { + "description": "Data for an activation request.", + "properties": { + "code": { + "$ref": "#/definitions/ASCII" + }, + "dryrun": { + "description": "At least one of key, email, or phone has to be present while key takes precedence over email, and email takes precedence over phone. Whether to perform a dryrun, i.e. to only check whether activation would succeed. Dry-runs never issue access cookies or tokens on success but failures still count towards the maximum failure count.", + "type": "boolean" + }, + "email": { + "$ref": "#/definitions/Email" + }, + "key": { + "$ref": "#/definitions/ASCII" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + } + }, + "required": [ + "code", + "dryrun" + ], + "type": "object" + }, + "ActivationResponse": { + "description": "Response body of a successful activation request", + "properties": { + "email": { + "$ref": "#/definitions/Email" + }, + "first": { + "description": "Whether this is the first successful activation (i.e. account activation).", + "type": "boolean" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + }, + "sso_id": { + "$ref": "#/definitions/UserSSOId" + } + }, + "type": "object" + }, + "AllFeatureConfigs": { + "properties": { + "appLock": { + "$ref": "#/definitions/AppLockConfig.WithStatus" + }, + "classifiedDomains": { + "$ref": "#/definitions/ClassifiedDomainsConfig.WithStatus" + }, + "conferenceCalling": { + "$ref": "#/definitions/ConferenceCallingConfig.WithStatus" + }, + "conversationGuestLinks": { + "$ref": "#/definitions/GuestLinksConfig.WithStatus" + }, + "digitalSignatures": { + "$ref": "#/definitions/DigitalSignaturesConfig.WithStatus" + }, + "exposeInvitationURLsToTeamAdmin": { + "$ref": "#/definitions/ExposeInvitationURLsToTeamAdminConfig.WithStatus" + }, + "fileSharing": { + "$ref": "#/definitions/FileSharingConfig.WithStatus" + }, + "legalhold": { + "$ref": "#/definitions/LegalholdConfig.WithStatus" + }, + "mls": { + "$ref": "#/definitions/MLSConfig.WithStatus" + }, + "mlsE2EId": { + "$ref": "#/definitions/MlsE2EIdConfig.WithStatus" + }, + "outlookCalIntegration": { + "$ref": "#/definitions/OutlookCalIntegrationConfig.WithStatus" + }, + "searchVisibility": { + "$ref": "#/definitions/SearchVisibilityAvailableConfig.WithStatus" + }, + "searchVisibilityInbound": { + "$ref": "#/definitions/SearchVisibilityInboundConfig.WithStatus" + }, + "selfDeletingMessages": { + "$ref": "#/definitions/SelfDeletingMessagesConfig.WithStatus" + }, + "sndFactorPasswordChallenge": { + "$ref": "#/definitions/SndFactorPasswordChallengeConfig.WithStatus" + }, + "sso": { + "$ref": "#/definitions/SSOConfig.WithStatus" + }, + "validateSAMLemails": { + "$ref": "#/definitions/ValidateSAMLEmailsConfig.WithStatus" + } + }, + "required": [ + "legalhold", + "sso", + "searchVisibility", + "searchVisibilityInbound", + "validateSAMLemails", + "digitalSignatures", + "appLock", + "fileSharing", + "classifiedDomains", + "conferenceCalling", + "selfDeletingMessages", + "conversationGuestLinks", + "sndFactorPasswordChallenge", + "mls", + "exposeInvitationURLsToTeamAdmin", + "outlookCalIntegration", + "mlsE2EId" + ], + "type": "object" + }, + "Alpha": { + "enum": [ + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BOV", + "BRL", + "BSD", + "BTN", + "BWP", + "BYN", + "BZD", + "CAD", + "CDF", + "CHE", + "CHF", + "CHW", + "CLF", + "CLP", + "CNY", + "COP", + "COU", + "CRC", + "CUC", + "CUP", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EGP", + "ERN", + "ETB", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "INR", + "IQD", + "IRR", + "ISK", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KPW", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRO", + "MUR", + "MVR", + "MWK", + "MXN", + "MXV", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SDG", + "SEK", + "SGD", + "SHP", + "SLL", + "SOS", + "SRD", + "SSP", + "STD", + "SVC", + "SYP", + "SZL", + "THB", + "TJS", + "TMT", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "USN", + "UYI", + "UYU", + "UZS", + "VEF", + "VND", + "VUV", + "WST", + "XAF", + "XAG", + "XAU", + "XBA", + "XBB", + "XBC", + "XBD", + "XCD", + "XDR", + "XOF", + "XPD", + "XPF", + "XPT", + "XSU", + "XTS", + "XUA", + "XXX", + "YER", + "ZAR", + "ZMW", + "ZWL" + ], + "type": "string" + }, + "AppLockConfig": { + "properties": { + "enforceAppLock": { + "type": "boolean" + }, + "inactivityTimeoutSecs": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "enforceAppLock", + "inactivityTimeoutSecs" + ], + "type": "object" + }, + "AppLockConfig.WithStatus": { + "properties": { + "config": { + "$ref": "#/definitions/AppLockConfig" + }, + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "AppLockConfig.WithStatusNoLock": { + "properties": { + "config": { + "$ref": "#/definitions/AppLockConfig" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "ApproveLegalHoldForUserRequest": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "Asset": { + "properties": { + "domain": { + "$ref": "#/definitions/Domain" + }, + "expires": { + "$ref": "#/definitions/UTCTime" + }, + "key": { + "$ref": "#/definitions/AssetKey" + }, + "token": { + "$ref": "#/definitions/ASCII" + } + }, + "required": [ + "key", + "domain" + ], + "type": "object" + }, + "AssetKey": { + "example": "3-1-47de4580-ae51-4650-acbb-d10c028cb0ac", + "type": "string" + }, + "AssetSize": { + "enum": [ + "preview", + "complete" + ], + "type": "string" + }, + "AssetSource": {}, + "AssetType": { + "enum": [ + "image" + ], + "type": "string" + }, + "AuthnRequest": { + "properties": { + "iD": { + "$ref": "#/definitions/ID" + }, + "issueInstant": { + "$ref": "#/definitions/Time" + }, + "issuer": { + "type": "string" + }, + "nameIDPolicy": { + "$ref": "#/definitions/NameIdPolicy" + } + }, + "required": [ + "iD", + "issueInstant", + "issuer" + ], + "type": "object" + }, + "Base64ByteString": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "BindingNewTeamUser": { + "properties": { + "currency": { + "$ref": "#/definitions/Alpha" + }, + "icon": { + "$ref": "#/definitions/Icon" + }, + "icon_key": { + "description": "team icon asset key", + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "members": { + "description": "initial team member ids (between 1 and 127)" + }, + "name": { + "description": "team name", + "maxLength": 256, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name", + "icon" + ], + "type": "object" + }, + "Body": {}, + "CheckHandles": { + "properties": { + "handles": { + "items": { + "type": "string" + }, + "maxItems": 50, + "minItems": 1, + "type": "array" + }, + "return": { + "maximum": 10, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "handles", + "return" + ], + "type": "object" + }, + "CipherSuiteTag": { + "description": "The cipher suite of the corresponding MLS group", + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "ClassifiedDomainsConfig": { + "properties": { + "domains": { + "items": { + "$ref": "#/definitions/Domain" + }, + "type": "array" + } + }, + "required": [ + "domains" + ], + "type": "object" + }, + "ClassifiedDomainsConfig.WithStatus": { + "properties": { + "config": { + "$ref": "#/definitions/ClassifiedDomainsConfig" + }, + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "Client": { + "properties": { + "capabilities": { + "$ref": "#/definitions/ClientCapabilityList" + }, + "class": { + "$ref": "#/definitions/ClientClass" + }, + "cookie": { + "type": "string" + }, + "id": { + "$ref": "#/definitions/ClientId" + }, + "label": { + "type": "string" + }, + "location": { + "$ref": "#/definitions/Location" + }, + "mls_public_keys": { + "$ref": "#/definitions/MLSPublicKeys" + }, + "model": { + "type": "string" + }, + "time": { + "$ref": "#/definitions/UTCTime" + }, + "type": { + "$ref": "#/definitions/ClientType" + } + }, + "required": [ + "id", + "type", + "time" + ], + "type": "object" + }, + "ClientCapability": { + "enum": [ + "legalhold-implicit-consent" + ], + "type": "string" + }, + "ClientCapabilityList": { + "properties": { + "capabilities": { + "description": "Hints provided by the client for the backend so it can behave in a backwards-compatible way.", + "items": { + "$ref": "#/definitions/ClientCapability" + }, + "type": "array" + } + }, + "required": [ + "capabilities" + ], + "type": "object" + }, + "ClientClass": { + "enum": [ + "phone", + "tablet", + "desktop", + "legalhold" + ], + "type": "string" + }, + "ClientId": { + "description": "Client ID", + "type": "string" + }, + "ClientMismatch": { + "properties": { + "deleted": { + "$ref": "#/definitions/UserClients" + }, + "missing": { + "$ref": "#/definitions/UserClients" + }, + "redundant": { + "$ref": "#/definitions/UserClients" + }, + "time": { + "$ref": "#/definitions/UTCTime" + } + }, + "required": [ + "time", + "missing", + "redundant", + "deleted" + ], + "type": "object" + }, + "ClientPrekey": { + "properties": { + "client": { + "$ref": "#/definitions/ClientId" + }, + "prekey": { + "$ref": "#/definitions/Prekey" + } + }, + "required": [ + "client", + "prekey" + ], + "type": "object" + }, + "ClientType": { + "enum": [ + "temporary", + "permanent", + "legalhold" + ], + "type": "string" + }, + "CompletePasswordReset": { + "description": "Data to complete a password reset", + "properties": { + "code": { + "$ref": "#/definitions/ASCII" + }, + "email": { + "$ref": "#/definitions/Email" + }, + "key": { + "$ref": "#/definitions/ASCII" + }, + "password": { + "description": "New password (6 - 1024 characters)", + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + } + }, + "required": [ + "code", + "password" + ], + "type": "object" + }, + "ConferenceCallingConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "Connect": { + "properties": { + "email": { + "type": "string" + }, + "message": { + "type": "string" + }, + "name": { + "type": "string" + }, + "qualified_recipient": { + "$ref": "#/definitions/Qualified_UserId" + }, + "recipient": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "qualified_recipient" + ], + "type": "object" + }, + "ConnectionUpdate": { + "properties": { + "status": { + "$ref": "#/definitions/Relation" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Connections_Page": { + "properties": { + "connections": { + "items": { + "$ref": "#/definitions/UserConnection" + }, + "type": "array" + }, + "has_more": { + "type": "boolean" + }, + "paging_state": { + "$ref": "#/definitions/Connections_PagingState" + } + }, + "required": [ + "connections", + "has_more", + "paging_state" + ], + "type": "object" + }, + "Connections_PagingState": { + "type": "string" + }, + "Contact": { + "description": "Contact discovered through search", + "properties": { + "accent_id": { + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, + "handle": { + "type": "string" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "name": { + "type": "string" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_UserId" + }, + "team": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "qualified_id", + "name" + ], + "type": "object" + }, + "ConvMembers": { + "description": "Users of a conversation", + "properties": { + "others": { + "description": "All other current users of this conversation", + "items": { + "$ref": "#/definitions/OtherMember" + }, + "type": "array" + }, + "self": { + "$ref": "#/definitions/Member" + } + }, + "required": [ + "self", + "others" + ], + "type": "object" + }, + "ConvTeamInfo": { + "description": "Team information of this conversation", + "properties": { + "managed": { + "description": "This field MUST NOT be used by clients. It is here only for backwards compatibility of the interface." + }, + "teamid": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "teamid", + "managed" + ], + "type": "object" + }, + "ConvType": { + "enum": [ + 0, + 1, + 2, + 3 + ], + "type": "integer" + }, + "Conversation": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/definitions/Access" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/definitions/AccessRoleLegacy" + }, + "access_role_v2": { + "items": { + "$ref": "#/definitions/AccessRole" + }, + "type": "array" + }, + "cipher_suite": { + "$ref": "#/definitions/CipherSuiteTag" + }, + "creator": { + "$ref": "#/definitions/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + }, + "group_id": { + "$ref": "#/definitions/GroupId" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/definitions/ConvMembers" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, + "name": { + "type": "string" + }, + "protocol": { + "$ref": "#/definitions/Protocol" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/definitions/UUID" + }, + "type": { + "$ref": "#/definitions/ConvType" + } + }, + "required": [ + "qualified_id", + "type", + "creator", + "access", + "members", + "group_id", + "epoch", + "cipher_suite" + ], + "type": "object" + }, + "ConversationAccessData": { + "properties": { + "access": { + "items": { + "$ref": "#/definitions/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/definitions/AccessRole" + }, + "type": "array" + } + }, + "required": [ + "access", + "access_role" + ], + "type": "object" + }, + "ConversationAccessData2": { + "properties": { + "access": { + "items": { + "$ref": "#/definitions/Access" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/definitions/AccessRoleLegacy" + }, + "access_role_v2": { + "items": { + "$ref": "#/definitions/AccessRole" + }, + "type": "array" + } + }, + "required": [ + "access" + ], + "type": "object" + }, + "ConversationCode": { + "description": "Contains conversation properties to update", + "properties": { + "code": { + "$ref": "#/definitions/ASCII" + }, + "key": { + "$ref": "#/definitions/ASCII" + }, + "uri": { + "$ref": "#/definitions/HttpsUrl" + } + }, + "required": [ + "key", + "code" + ], + "type": "object" + }, + "ConversationCoverView": { + "description": "Limited view of Conversation.", + "properties": { + "id": { + "$ref": "#/definitions/UUID" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "ConversationIds_Page": { + "properties": { + "has_more": { + "type": "boolean" + }, + "paging_state": { + "$ref": "#/definitions/ConversationIds_PagingState" + }, + "qualified_conversations": { + "items": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "type": "array" + } + }, + "required": [ + "qualified_conversations", + "has_more", + "paging_state" + ], + "type": "object" + }, + "ConversationIds_PagingState": { + "type": "string" + }, + "ConversationMessageTimerUpdate": { + "description": "Contains conversation properties to update", + "properties": { + "message_timer": { + "format": "int64", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + } + }, + "type": "object" + }, + "ConversationReceiptModeUpdate": { + "description": "Contains conversation receipt mode to update to. Receipt mode tells clients whether certain types of receipts should be sent in the given conversation or not. How this value is interpreted is up to clients.", + "properties": { + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "receipt_mode" + ], + "type": "object" + }, + "ConversationRename": { + "properties": { + "name": { + "description": "The new conversation name", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "ConversationRole": { + "properties": { + "actions": { + "description": "The set of actions allowed for this role", + "items": { + "$ref": "#/definitions/Action" + }, + "type": "array" + }, + "conversation_role": { + "$ref": "#/definitions/RoleName" + } + } + }, + "ConversationRolesList": { + "properties": { + "conversation_roles": { + "items": { + "$ref": "#/definitions/ConversationRole" + }, + "type": "array" + } + }, + "required": [ + "conversation_roles" + ], + "type": "object" + }, + "ConversationsResponse": { + "description": "Response object for getting metadata of a list of conversations", + "properties": { + "failed": { + "description": "The server failed to fetch these conversations, most likely due to network issues while contacting a remote server", + "items": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "type": "array" + }, + "found": { + "items": { + "$ref": "#/definitions/Conversation" + }, + "type": "array" + }, + "not_found": { + "description": "These conversations either don't exist or are deleted.", + "items": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "type": "array" + } + }, + "required": [ + "found", + "not_found", + "failed" + ], + "type": "object" + }, + "Cookie": { + "properties": { + "created": { + "$ref": "#/definitions/UTCTime" + }, + "expires": { + "$ref": "#/definitions/UTCTime" + }, + "id": { + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "label": { + "type": "string" + }, + "successor": { + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "type": { + "$ref": "#/definitions/CookieType" + } + }, + "required": [ + "id", + "type", + "created", + "expires" + ], + "type": "object" + }, + "CookieList": { + "description": "List of cookie information", + "properties": { + "cookies": { + "items": { + "$ref": "#/definitions/Cookie" + }, + "type": "array" + } + }, + "required": [ + "cookies" + ], + "type": "object" + }, + "CookieType": { + "enum": [ + "session", + "persistent" + ], + "type": "string" + }, + "CreateScimToken": { + "properties": { + "description": { + "type": "string" + }, + "password": { + "type": "string" + }, + "verification_code": { + "type": "string" + } + }, + "required": [ + "description" + ], + "type": "object" + }, + "CreateScimTokenResponse": { + "properties": { + "info": { + "$ref": "#/definitions/ScimTokenInfo" + }, + "token": { + "description": "Authentication token", + "type": "string" + } + }, + "required": [ + "token", + "info" + ], + "type": "object" + }, + "CustomBackend": { + "description": "Description of a custom backend", + "properties": { + "config_json_url": { + "$ref": "#/definitions/HttpsUrl" + }, + "webapp_welcome_url": { + "$ref": "#/definitions/HttpsUrl" + } + }, + "required": [ + "config_json_url", + "webapp_welcome_url" + ], + "type": "object" + }, + "DPoPAccessToken": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "DPoPAccessTokenResponse": { + "properties": { + "expires_in": { + "format": "int64", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + }, + "token": { + "$ref": "#/definitions/DPoPAccessToken" + }, + "type": { + "$ref": "#/definitions/AccessTokenType" + } + }, + "required": [ + "token", + "type", + "expires_in" + ], + "type": "object" + }, + "DeleteClient": { + "properties": { + "password": { + "description": "The password of the authenticated user for verification. The password is not required for deleting temporary clients.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "DeleteUser": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "DeletionCodeTimeout": { + "properties": { + "expires_in": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "expires_in" + ], + "type": "object" + }, + "DeprecatedMatchingResult": { + "properties": { + "auto-connects": { + "items": {}, + "type": "array" + }, + "results": { + "items": {}, + "type": "array" + } + }, + "required": [ + "results", + "auto-connects" + ], + "type": "object" + }, + "DigitalSignaturesConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "DisableLegalHoldForUserRequest": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "Domain": { + "example": "example.com", + "type": "string" + }, + "Email": { + "description": "Email of the invitee", + "type": "string" + }, + "EmailUpdate": { + "properties": { + "email": { + "$ref": "#/definitions/Email" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "Event": { + "properties": { + "conversation": { + "$ref": "#/definitions/UUID" + }, + "data": { + "description": "Encrypted message of a conversation", + "example": "ZXhhbXBsZQo=", + "properties": { + "access": { + "items": { + "$ref": "#/definitions/Access" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/definitions/AccessRoleLegacy" + }, + "access_role_v2": { + "items": { + "$ref": "#/definitions/AccessRole" + }, + "type": "array" + }, + "cipher_suite": { + "$ref": "#/definitions/CipherSuiteTag" + }, + "code": { + "$ref": "#/definitions/ASCII" + }, + "conversation_role": { + "$ref": "#/definitions/RoleName" + }, + "creator": { + "$ref": "#/definitions/UUID" + }, + "data": { + "description": "Extra (symmetric) data (i.e. ciphertext, Base64 in JSON) that is common with all other recipients.", + "type": "string" + }, + "email": { + "type": "string" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + }, + "group_id": { + "$ref": "#/definitions/GroupId" + }, + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "key": { + "$ref": "#/definitions/ASCII" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/definitions/ConvMembers" + }, + "message": { + "type": "string" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, + "name": { + "type": "string" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "protocol": { + "$ref": "#/definitions/Protocol" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "qualified_recipient": { + "$ref": "#/definitions/Qualified_UserId" + }, + "qualified_target": { + "$ref": "#/definitions/Qualified_UserId" + }, + "qualified_user_ids": { + "items": { + "$ref": "#/definitions/Qualified_UserId" + }, + "type": "array" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "recipient": { + "$ref": "#/definitions/ClientId" + }, + "sender": { + "$ref": "#/definitions/ClientId" + }, + "status": { + "$ref": "#/definitions/TypingStatus" + }, + "target": { + "$ref": "#/definitions/UUID" + }, + "team": { + "$ref": "#/definitions/UUID" + }, + "text": { + "description": "The ciphertext for the recipient (Base64 in JSON)", + "type": "string" + }, + "type": { + "$ref": "#/definitions/ConvType" + }, + "uri": { + "$ref": "#/definitions/HttpsUrl" + }, + "user_ids": { + "description": "Deprecated, use qualified_user_ids", + "items": { + "$ref": "#/definitions/UUID" + }, + "type": "array" + }, + "users": { + "items": { + "$ref": "#/definitions/SimpleMember" + }, + "type": "array" + } + }, + "required": [ + "users", + "qualified_user_ids", + "user_ids", + "qualified_target", + "name", + "access", + "key", + "code", + "qualified_id", + "type", + "creator", + "members", + "group_id", + "epoch", + "cipher_suite", + "qualified_recipient", + "receipt_mode", + "sender", + "recipient", + "text", + "status" + ], + "type": "object" + }, + "from": { + "$ref": "#/definitions/UUID" + }, + "qualified_conversation": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "qualified_from": { + "$ref": "#/definitions/Qualified_UserId" + }, + "subconv": { + "type": "string" + }, + "time": { + "$ref": "#/definitions/UTCTime" + }, + "type": { + "$ref": "#/definitions/EventType" + } + }, + "required": [ + "type", + "data", + "qualified_conversation", + "qualified_from", + "time" + ], + "type": "object" + }, + "EventType": { + "enum": [ + "conversation.member-join", + "conversation.member-leave", + "conversation.member-update", + "conversation.rename", + "conversation.access-update", + "conversation.receipt-mode-update", + "conversation.message-timer-update", + "conversation.code-update", + "conversation.code-delete", + "conversation.create", + "conversation.delete", + "conversation.connect-request", + "conversation.typing", + "conversation.otr-message-add", + "conversation.mls-message-add", + "conversation.mls-welcome" + ], + "type": "string" + }, + "ExposeInvitationURLsToTeamAdminConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "ExposeInvitationURLsToTeamAdminConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "FeatureStatus": { + "enum": [ + "enabled", + "disabled" + ], + "type": "string" + }, + "FederatedUserSearchPolicy": { + "description": "Search policy that was applied when searching for users", + "enum": [ + "no_search", + "exact_handle_search", + "full_search" + ], + "type": "string" + }, + "FileSharingConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "FileSharingConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Fingerprint": { + "example": "ioy3GeIjgQRsobf2EKGO3O8mq/FofFxHRqy0T4ERIZ8=", + "type": "string" + }, + "FormRedirect": { + "properties": { + "uri": { + "type": "string" + }, + "xml": { + "$ref": "#/definitions/AuthnRequest" + } + }, + "type": "object" + }, + "GetPaginated_Connections": { + "description": "A request to list some or all of a user's Connections, including remote ones", + "properties": { + "paging_state": { + "$ref": "#/definitions/Connections_PagingState" + }, + "size": { + "description": "optional, must be <= 500, defaults to 100.", + "format": "int32", + "maximum": 500, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, + "GetPaginated_ConversationIds": { + "description": "A request to list some or all of a user's ConversationIds, including remote ones", + "properties": { + "paging_state": { + "$ref": "#/definitions/ConversationIds_PagingState" + }, + "size": { + "description": "optional, must be <= 1000, defaults to 1000.", + "format": "int32", + "maximum": 1000, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, + "GroupId": { + "description": "An MLS group identifier (at most 256 bytes long)", + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "GuestLinksConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "GuestLinksConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Handle": { + "type": "string" + }, + "HandleUpdate": { + "properties": { + "handle": { + "type": "string" + } + }, + "required": [ + "handle" + ], + "type": "object" + }, + "HttpsUrl": { + "example": "https://example.com", + "type": "string" + }, + "ID": { + "properties": { + "iD": { + "$ref": "#/definitions/XmlText" + } + }, + "required": [ + "iD" + ], + "type": "object" + }, + "Icon": { + "type": "string" + }, + "Id": { + "properties": { + "id": { + "$ref": "#/definitions/ClientId" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "IdPConfig": { + "properties": { + "extraInfo": { + "$ref": "#/definitions/WireIdP" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "metadata": { + "$ref": "#/definitions/IdPMetadata" + } + }, + "required": [ + "id", + "metadata", + "extraInfo" + ], + "type": "object" + }, + "IdPList": { + "properties": { + "providers": { + "items": { + "$ref": "#/definitions/IdPConfig" + }, + "type": "array" + } + }, + "required": [ + "providers" + ], + "type": "object" + }, + "IdPMetadata": { + "properties": { + "certAuthnResponse": { + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + }, + "issuer": { + "type": "string" + }, + "requestURI": { + "type": "string" + } + }, + "required": [ + "issuer", + "requestURI", + "certAuthnResponse" + ], + "type": "object" + }, + "IdPMetadataInfo": { + "maxProperties": 1, + "minProperties": 1, + "properties": { + "value": { + "type": "string" + } + }, + "type": "object" + }, + "Invitation": { + "description": "An invitation to join a team on Wire", + "properties": { + "created_at": { + "$ref": "#/definitions/UTCTime" + }, + "created_by": { + "$ref": "#/definitions/UUID" + }, + "email": { + "$ref": "#/definitions/Email" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "name": { + "description": "Name of the invitee (1 - 128 characters)", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + }, + "role": { + "$ref": "#/definitions/Role" + }, + "team": { + "$ref": "#/definitions/UUID" + }, + "url": { + "$ref": "#/definitions/URIRef Absolute" + } + }, + "required": [ + "team", + "id", + "created_at", + "email" + ], + "type": "object" + }, + "InvitationList": { + "description": "A list of sent team invitations.", + "properties": { + "has_more": { + "description": "Indicator that the server has more invitations than returned.", + "type": "boolean" + }, + "invitations": { + "items": { + "$ref": "#/definitions/Invitation" + }, + "type": "array" + } + }, + "required": [ + "invitations", + "has_more" + ], + "type": "object" + }, + "InvitationRequest": { + "description": "A request to join a team on Wire.", + "properties": { + "email": { + "$ref": "#/definitions/Email" + }, + "locale": { + "$ref": "#/definitions/Locale" + }, + "name": { + "description": "Name of the invitee (1 - 128 characters).", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + }, + "role": { + "$ref": "#/definitions/Role" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "InviteQualified": { + "properties": { + "conversation_role": { + "$ref": "#/definitions/RoleName" + }, + "qualified_users": { + "items": { + "$ref": "#/definitions/Qualified_UserId" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "qualified_users" + ], + "type": "object" + }, + "KeyPackage": { + "example": "a2V5IHBhY2thZ2UgZGF0YQo=", + "type": "string" + }, + "KeyPackageBundle": { + "properties": { + "key_packages": { + "items": { + "$ref": "#/definitions/KeyPackageBundleEntry" + }, + "type": "array" + } + }, + "required": [ + "key_packages" + ], + "type": "object" + }, + "KeyPackageBundleEntry": { + "properties": { + "client": { + "$ref": "#/definitions/ClientId" + }, + "domain": { + "$ref": "#/definitions/Domain" + }, + "key_package": { + "$ref": "#/definitions/KeyPackage" + }, + "key_package_ref": { + "$ref": "#/definitions/KeyPackageRef" + }, + "user": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "domain", + "user", + "client", + "key_package_ref", + "key_package" + ], + "type": "object" + }, + "KeyPackageRef": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "KeyPackageUpload": { + "properties": { + "key_packages": { + "items": { + "$ref": "#/definitions/KeyPackage" + }, + "type": "array" + } + }, + "required": [ + "key_packages" + ], + "type": "object" + }, + "LHServiceStatus": { + "enum": [ + "configured", + "not_configured", + "disabled" + ], + "type": "string" + }, + "LegalholdConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LegalholdConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "LimitedQualifiedUserIdList": { + "properties": { + "qualified_users": { + "items": { + "$ref": "#/definitions/Qualified_UserId" + }, + "type": "array" + } + }, + "required": [ + "qualified_users" + ], + "type": "object" + }, + "ListConversations": { + "description": "A request to list some of a user's conversations, including remote ones. Maximum 1000 qualified conversation IDs", + "properties": { + "qualified_ids": { + "items": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "maxItems": 1000, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "qualified_ids" + ], + "type": "object" + }, + "ListType": { + "description": "true if 'members' doesn't contain all team members", + "enum": [ + true, + false + ], + "type": "boolean" + }, + "ListUsersQuery": { + "description": "exactly one of qualified_ids or qualified_handles must be provided.", + "example": { + "qualified_ids": [ + { + "domain": "example.com", + "id": "00000000-0000-0000-0000-000000000000" + } + ] + }, + "properties": { + "qualified_handles": { + "items": { + "$ref": "#/definitions/Qualified_Handle" + }, + "type": "array" + }, + "qualified_ids": { + "items": { + "$ref": "#/definitions/Qualified_UserId" + }, + "type": "array" + } + }, + "type": "object" + }, + "Locale": { + "description": "Locale to use for the invitation.", + "type": "string" + }, + "LocaleUpdate": { + "properties": { + "locale": { + "$ref": "#/definitions/Locale" + } + }, + "required": [ + "locale" + ], + "type": "object" + }, + "Location": { + "properties": { + "lat": { + "format": "double", + "type": "number" + }, + "lon": { + "format": "double", + "type": "number" + } + }, + "required": [ + "lat", + "lon" + ], + "type": "object" + }, + "LockStatus": { + "enum": [ + "locked", + "unlocked" + ], + "type": "string" + }, + "Login": { + "properties": { + "code": { + "$ref": "#/definitions/LoginCode" + }, + "email": { + "$ref": "#/definitions/Email" + }, + "handle": { + "$ref": "#/definitions/Handle" + }, + "label": { + "description": "This label can be used to delete all cookies matching it (cf. /cookies/remove)", + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + }, + "verification_code": { + "$ref": "#/definitions/ASCII" + } + }, + "required": [ + "password", + "phone", + "code" + ], + "type": "object" + }, + "LoginCode": { + "type": "string" + }, + "LoginCodeTimeout": { + "description": "A response for a successfully sent login code", + "properties": { + "expires_in": { + "description": "Number of seconds before the login code expires", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "expires_in" + ], + "type": "object" + }, + "MLSConfig": { + "properties": { + "allowedCipherSuites": { + "items": { + "$ref": "#/definitions/CipherSuiteTag" + }, + "type": "array" + }, + "defaultCipherSuite": { + "$ref": "#/definitions/CipherSuiteTag" + }, + "defaultProtocol": { + "$ref": "#/definitions/Protocol" + }, + "protocolToggleUsers": { + "description": "allowlist of users that may change protocols", + "items": { + "$ref": "#/definitions/UUID" + }, + "type": "array" + } + }, + "required": [ + "protocolToggleUsers", + "defaultProtocol", + "allowedCipherSuites", + "defaultCipherSuite" + ], + "type": "object" + }, + "MLSConfig.WithStatus": { + "properties": { + "config": { + "$ref": "#/definitions/MLSConfig" + }, + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "MLSConfig.WithStatusNoLock": { + "properties": { + "config": { + "$ref": "#/definitions/MLSConfig" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "MLSMessageSendingStatus": { + "properties": { + "events": { + "description": "A list of events caused by sending the message.", + "items": { + "$ref": "#/definitions/Event" + }, + "type": "array" + }, + "time": { + "$ref": "#/definitions/UTCTime" + } + }, + "required": [ + "events", + "time" + ], + "type": "object" + }, + "MLSPublicKeys": { + "additionalProperties": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "description": "Mapping from signature scheme (tags) to public key data", + "example": { + "ed25519": "ZXhhbXBsZQo=" + }, + "type": "object" + }, + "ManagedBy": { + "enum": [ + "wire", + "scim" + ], + "type": "string" + }, + "Member": { + "description": "The user ID of the requestor", + "properties": { + "conversation_role": { + "$ref": "#/definitions/RoleName" + }, + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_UserId" + }, + "service": { + "$ref": "#/definitions/ServiceRef" + }, + "status": {}, + "status_ref": {}, + "status_time": {} + }, + "required": [ + "qualified_id" + ], + "type": "object" + }, + "MemberUpdate": { + "properties": { + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "type": "object" + }, + "MemberUpdateData": { + "properties": { + "conversation_role": { + "$ref": "#/definitions/RoleName" + }, + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "qualified_target": { + "$ref": "#/definitions/Qualified_UserId" + }, + "target": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "qualified_target" + ], + "type": "object" + }, + "MessageSendingStatus": { + "properties": { + "deleted": { + "$ref": "#/definitions/QualifiedUserClients" + }, + "failed_to_send": { + "$ref": "#/definitions/QualifiedUserClients" + }, + "missing": { + "$ref": "#/definitions/QualifiedUserClients" + }, + "redundant": { + "$ref": "#/definitions/QualifiedUserClients" + }, + "time": { + "$ref": "#/definitions/UTCTime" + } + }, + "required": [ + "time", + "missing", + "redundant", + "deleted", + "failed_to_send" + ], + "type": "object" + }, + "MlsE2EIdConfig": { + "properties": { + "verificationExpiration": { + "description": "Unix timestamp (number of seconds that have passed since 00:00:00 UTC on Thursday, 1 January 1970) after which the period for clients to verify their identity expires. When the timer goes off, they will be logged out and get the certificate automatically on their devices.", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + } + }, + "type": "object" + }, + "MlsE2EIdConfig.WithStatus": { + "properties": { + "config": { + "$ref": "#/definitions/MlsE2EIdConfig" + }, + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "MlsE2EIdConfig.WithStatusNoLock": { + "properties": { + "config": { + "$ref": "#/definitions/MlsE2EIdConfig" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "NameIDFormat": { + "enum": [ + "NameIDFUnspecified", + "NameIDFEmail", + "NameIDFX509", + "NameIDFWindows", + "NameIDFKerberos", + "NameIDFEntity", + "NameIDFPersistent", + "NameIDFTransient" + ], + "type": "string" + }, + "NameIdPolicy": { + "properties": { + "allowCreate": { + "type": "boolean" + }, + "format": { + "$ref": "#/definitions/NameIDFormat" + }, + "spNameQualifier": { + "$ref": "#/definitions/XmlText" + } + }, + "required": [ + "format", + "allowCreate" + ], + "type": "object" + }, + "NewAssetToken": { + "properties": { + "token": { + "$ref": "#/definitions/ASCII" + } + }, + "required": [ + "token" + ], + "type": "object" + }, + "NewClient": { + "properties": { + "capabilities": { + "description": "Hints provided by the client for the backend so it can behave in a backwards-compatible way.", + "items": { + "$ref": "#/definitions/ClientCapability" + }, + "type": "array" + }, + "class": { + "$ref": "#/definitions/ClientClass" + }, + "cookie": { + "description": "The cookie label, i.e. the label used when logging in.", + "type": "string" + }, + "label": { + "type": "string" + }, + "lastkey": { + "$ref": "#/definitions/Prekey" + }, + "mls_public_keys": { + "$ref": "#/definitions/MLSPublicKeys" + }, + "model": { + "type": "string" + }, + "password": { + "description": "The password of the authenticated user for verification. Note: Required for registration of the 2nd, 3rd, ... client.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "prekeys": { + "description": "Prekeys for other clients to establish OTR sessions.", + "items": { + "$ref": "#/definitions/Prekey" + }, + "type": "array" + }, + "type": { + "$ref": "#/definitions/ClientType" + }, + "verification_code": { + "$ref": "#/definitions/ASCII" + } + }, + "required": [ + "prekeys", + "lastkey", + "type" + ], + "type": "object" + }, + "NewConv": { + "description": "JSON object to create a new conversation. When using 'qualified_users' (preferred), you can omit 'users'", + "properties": { + "access": { + "items": { + "$ref": "#/definitions/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/definitions/AccessRole" + }, + "type": "array" + }, + "conversation_role": { + "$ref": "#/definitions/RoleName" + }, + "creator_client": { + "$ref": "#/definitions/ClientId" + }, + "message_timer": { + "description": "Per-conversation message timer", + "format": "int64", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "protocol": { + "$ref": "#/definitions/Protocol" + }, + "qualified_users": { + "description": "List of qualified user IDs (excluding the requestor) to be part of this conversation", + "items": { + "$ref": "#/definitions/Qualified_UserId" + }, + "type": "array" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/definitions/ConvTeamInfo" + }, + "users": { + "description": "List of user IDs (excluding the requestor) to be part of this conversation (deprecated)", + "items": { + "$ref": "#/definitions/UUID" + }, + "type": "array" + } + }, + "type": "object" + }, + "NewLegalHoldService": { + "properties": { + "auth_token": { + "$ref": "#/definitions/ASCII" + }, + "base_url": { + "$ref": "#/definitions/HttpsUrl" + }, + "public_key": { + "$ref": "#/definitions/ServiceKeyPEM" + } + }, + "required": [ + "base_url", + "public_key", + "auth_token" + ], + "type": "object" + }, + "NewPasswordReset": { + "description": "Data to initiate a password reset", + "properties": { + "email": { + "$ref": "#/definitions/Email" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + } + }, + "type": "object" + }, + "NewTeamMember": { + "description": "Required data when creating new team members", + "properties": { + "member": { + "description": "the team member to add (the legalhold_status field must be null or missing!)", + "properties": { + "created_at": { + "$ref": "#/definitions/UTCTime" + }, + "created_by": { + "$ref": "#/definitions/UUID" + }, + "permissions": { + "$ref": "#/definitions/Permissions" + }, + "user": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "user", + "permissions" + ], + "type": "object" + } + }, + "required": [ + "member" + ], + "type": "object" + }, + "NewUser": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/definitions/UserAsset" + }, + "type": "array" + }, + "email": { + "$ref": "#/definitions/Email" + }, + "email_code": { + "$ref": "#/definitions/ASCII" + }, + "expires_in": { + "maximum": 604800, + "minimum": 1, + "type": "integer" + }, + "invitation_code": { + "$ref": "#/definitions/ASCII" + }, + "label": { + "type": "string" + }, + "locale": { + "$ref": "#/definitions/Locale" + }, + "managed_by": { + "$ref": "#/definitions/ManagedBy" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + }, + "phone_code": { + "$ref": "#/definitions/ASCII" + }, + "picture": { + "$ref": "#/definitions/Pict" + }, + "sso_id": { + "$ref": "#/definitions/UserSSOId" + }, + "team": { + "$ref": "#/definitions/BindingNewTeamUser" + }, + "team_code": { + "$ref": "#/definitions/ASCII" + }, + "team_id": { + "$ref": "#/definitions/UUID" + }, + "uuid": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "NonBindingNewTeam": { + "properties": { + "icon": { + "$ref": "#/definitions/Icon" + }, + "icon_key": { + "description": "team icon asset key", + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "members": { + "description": "initial team member ids (between 1 and 127)", + "items": { + "$ref": "#/definitions/TeamMember" + }, + "maxItems": 127, + "minItems": 1, + "type": "array" + }, + "name": { + "description": "team name", + "maxLength": 256, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name", + "icon" + ], + "type": "object" + }, + "Object": { + "additionalProperties": true, + "description": "A single notification event", + "properties": { + "type": { + "description": "Event type", + "type": "string" + } + }, + "title": "Event", + "type": "object" + }, + "OtherMember": { + "properties": { + "conversation_role": { + "$ref": "#/definitions/RoleName" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_UserId" + }, + "service": { + "$ref": "#/definitions/ServiceRef" + }, + "status": { + "description": "deprecated", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + } + }, + "required": [ + "qualified_id" + ], + "type": "object" + }, + "OtherMemberUpdate": { + "description": "Update user properties of other members relative to a conversation", + "properties": { + "conversation_role": { + "$ref": "#/definitions/RoleName" + } + }, + "type": "object" + }, + "OtrMessage": { + "description": "Encrypted message of a conversation", + "properties": { + "data": { + "description": "Extra (symmetric) data (i.e. ciphertext, Base64 in JSON) that is common with all other recipients.", + "type": "string" + }, + "recipient": { + "$ref": "#/definitions/ClientId" + }, + "sender": { + "$ref": "#/definitions/ClientId" + }, + "text": { + "description": "The ciphertext for the recipient (Base64 in JSON)", + "type": "string" + } + }, + "required": [ + "sender", + "recipient", + "text" + ], + "type": "object" + }, + "OutlookCalIntegrationConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "OutlookCalIntegrationConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "OwnKeyPackages": { + "properties": { + "count": { + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + } + }, + "required": [ + "count" + ], + "type": "object" + }, + "PagingState": { + "description": "Paging state that should be supplied to retrieve the next page of results", + "type": "string" + }, + "PasswordChange": { + "description": "Data to change a password. The old password is required if a password already exists.", + "properties": { + "new_password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "old_password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "new_password" + ], + "type": "object" + }, + "PasswordReset": { + "description": "Data to complete a password reset", + "properties": { + "code": { + "$ref": "#/definitions/ASCII" + }, + "password": { + "description": "New password (6 - 1024 characters)", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "code", + "password" + ], + "type": "object" + }, + "Permissions": { + "properties": { + "copy": { + "format": "int64", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + }, + "self": { + "format": "int64", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "self", + "copy" + ], + "type": "object" + }, + "PhoneNumber": { + "description": "Phone number of the invitee, in the E.164 format", + "type": "string" + }, + "PhoneUpdate": { + "properties": { + "phone": { + "$ref": "#/definitions/PhoneNumber" + } + }, + "required": [ + "phone" + ], + "type": "object" + }, + "Pict": { + "items": {}, + "maxItems": 10, + "minItems": 0, + "type": "array" + }, + "Prekey": { + "properties": { + "id": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "key": { + "type": "string" + } + }, + "required": [ + "id", + "key" + ], + "type": "object" + }, + "PrekeyBundle": { + "properties": { + "clients": { + "items": { + "$ref": "#/definitions/ClientPrekey" + }, + "type": "array" + }, + "user": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "user", + "clients" + ], + "type": "object" + }, + "Priority": { + "enum": [ + "low", + "high" + ], + "type": "string" + }, + "PropertyKeysAndValues": { + "type": "object" + }, + "PropertyValue": { + "description": "An arbitrary JSON value for a property" + }, + "Protocol": { + "enum": [ + "proteus", + "mls" + ], + "type": "string" + }, + "PubClient": { + "properties": { + "class": { + "$ref": "#/definitions/ClientClass" + }, + "id": { + "$ref": "#/definitions/ClientId" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "PushToken": { + "description": "Native Push Token", + "properties": { + "app": { + "description": "Application", + "type": "string" + }, + "client": { + "$ref": "#/definitions/ClientId" + }, + "token": { + "description": "Access Token", + "type": "string" + }, + "transport": { + "$ref": "#/definitions/Access" + } + }, + "required": [ + "transport", + "app", + "token", + "client" + ], + "type": "object" + }, + "PushTokenList": { + "description": "List of Native Push Tokens", + "properties": { + "tokens": { + "description": "Push tokens", + "items": { + "$ref": "#/definitions/PushToken" + }, + "type": "array" + } + }, + "required": [ + "tokens" + ], + "type": "object" + }, + "QualifiedNewOtrMessage": { + "description": "This object can only be parsed from Protobuf.\nThe specification for the protobuf types is here: \nhttps://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto." + }, + "QualifiedUserClientPrekeyMap": { + "additionalProperties": { + "$ref": "#/definitions/UserClientPrekeyMap" + }, + "type": "object" + }, + "QualifiedUserClients": { + "additionalProperties": { + "additionalProperties": { + "items": { + "$ref": "#/definitions/ClientId" + }, + "type": "array" + }, + "type": "object" + }, + "description": "Clients that the message /should/ have been encrypted for, but wasn't.", + "example": { + "domain1.example.com": { + "000600d0-000b-9c1a-000d-a4130002c221": [ + "60f85e4b15ad3786", + "6e323ab31554353b" + ] + } + }, + "type": "object" + }, + "QualifiedUserIdList": { + "properties": { + "qualified_user_ids": { + "items": { + "$ref": "#/definitions/Qualified_UserId" + }, + "type": "array" + }, + "user_ids": { + "description": "Deprecated, use qualified_user_ids", + "items": { + "$ref": "#/definitions/UUID" + }, + "type": "array" + } + }, + "required": [ + "qualified_user_ids", + "user_ids" + ], + "type": "object" + }, + "QualifiedUserMap_Set_PubClient": { + "additionalProperties": { + "$ref": "#/definitions/UserMap_Set_PubClient" + }, + "description": "Map of Domain to (UserMap (Set_PubClient)).", + "example": { + "domain1.example.com": { + "000600d0-000b-9c1a-000d-a4130002c221": [ + { + "class": "legalhold", + "id": "d0" + } + ] + } + }, + "type": "object" + }, + "Qualified_ConvId": { + "properties": { + "domain": { + "$ref": "#/definitions/Domain" + }, + "id": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "domain", + "id" + ], + "type": "object" + }, + "Qualified_Handle": { + "properties": { + "domain": { + "$ref": "#/definitions/Domain" + }, + "handle": { + "$ref": "#/definitions/Handle" + } + }, + "required": [ + "domain", + "handle" + ], + "type": "object" + }, + "Qualified_UserId": { + "properties": { + "domain": { + "$ref": "#/definitions/Domain" + }, + "id": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "domain", + "id" + ], + "type": "object" + }, + "QueuedNotification": { + "description": "A single notification", + "properties": { + "id": { + "$ref": "#/definitions/UUID" + }, + "payload": { + "description": "List of events", + "items": { + "$ref": "#/definitions/Object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "id", + "payload" + ], + "type": "object" + }, + "QueuedNotificationList": { + "description": "Zero or more notifications", + "properties": { + "has_more": { + "description": "Whether there are still more notifications.", + "type": "boolean" + }, + "notifications": { + "description": "Notifications", + "items": { + "$ref": "#/definitions/QueuedNotification" + }, + "type": "array" + }, + "time": { + "$ref": "#/definitions/UTCTime" + } + }, + "required": [ + "notifications" + ], + "type": "object" + }, + "RTCConfiguration": { + "description": "A subset of the WebRTC 'RTCConfiguration' dictionary", + "properties": { + "ice_servers": { + "description": "Array of 'RTCIceServer' objects", + "items": { + "$ref": "#/definitions/RTCIceServer" + }, + "minItems": 1, + "type": "array" + }, + "sft_servers": { + "description": "Array of 'SFTServer' objects (optional)", + "items": { + "$ref": "#/definitions/SftServer" + }, + "minItems": 1, + "type": "array" + }, + "sft_servers_all": { + "description": "Array of all SFT servers", + "items": { + "$ref": "#/definitions/SftServer" + }, + "type": "array" + }, + "ttl": { + "description": "Number of seconds after which the configuration should be refreshed (advisory)", + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "ice_servers", + "ttl" + ], + "type": "object" + }, + "RTCIceServer": { + "description": "A subset of the WebRTC 'RTCIceServer' object", + "properties": { + "credential": { + "$ref": "#/definitions/ASCII" + }, + "urls": { + "description": "Array of TURN server addresses of the form 'turn::'", + "items": { + "$ref": "#/definitions/TurnURI" + }, + "minItems": 1, + "type": "array" + }, + "username": { + "$ref": "#/definitions/" + } + }, + "required": [ + "urls", + "username", + "credential" + ], + "type": "object" + }, + "Relation": { + "enum": [ + "accepted", + "blocked", + "pending", + "ignored", + "sent", + "cancelled", + "missing-legalhold-consent" + ], + "type": "string" + }, + "RemoveCookies": { + "description": "Data required to remove cookies", + "properties": { + "ids": { + "description": "A list of cookie IDs to revoke", + "items": { + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "type": "array" + }, + "labels": { + "description": "A list of cookie labels for which to revoke the cookies", + "items": { + "type": "string" + }, + "type": "array" + }, + "password": { + "description": "The user's password", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "RemoveLegalHoldSettingsRequest": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "RichField": { + "properties": { + "type": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "value" + ], + "type": "object" + }, + "RichInfoAssocList": { + "properties": { + "fields": { + "items": { + "$ref": "#/definitions/RichField" + }, + "type": "array" + }, + "version": { + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + } + }, + "required": [ + "version", + "fields" + ], + "type": "object" + }, + "Role": { + "description": "Role of the invited user", + "enum": [ + "owner", + "admin", + "member", + "partner" + ], + "type": "string" + }, + "RoleName": { + "description": "Role name, between 2 and 128 chars, 'wire_' prefix is reserved for roles designed by Wire (i.e., no custom roles can have the same prefix)", + "type": "string" + }, + "SSOConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "ScimTokenInfo": { + "properties": { + "created_at": { + "$ref": "#/definitions/UTCTime" + }, + "description": { + "type": "string" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "idp": { + "$ref": "#/definitions/UUID" + }, + "team": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "team", + "id", + "created_at", + "description" + ], + "type": "object" + }, + "ScimTokenList": { + "properties": { + "tokens": { + "items": { + "$ref": "#/definitions/ScimTokenInfo" + }, + "type": "array" + } + }, + "required": [ + "tokens" + ], + "type": "object" + }, + "SearchResult": { + "properties": { + "documents": { + "description": "List of contacts found", + "items": { + "$ref": "#/definitions/TeamContact" + }, + "type": "array" + }, + "found": { + "description": "Total number of hits", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, + "has_more": { + "description": "Indicates whether there are more results to be fetched", + "type": "boolean" + }, + "paging_state": { + "$ref": "#/definitions/PagingState" + }, + "returned": { + "description": "Total number of hits returned", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, + "search_policy": { + "$ref": "#/definitions/FederatedUserSearchPolicy" + }, + "took": { + "description": "Search time in ms", + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + } + }, + "required": [ + "found", + "returned", + "took", + "documents", + "search_policy" + ], + "type": "object" + }, + "SearchVisibilityAvailableConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "SearchVisibilityAvailableConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "SearchVisibilityInboundConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "SearchVisibilityInboundConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "SelfDeletingMessagesConfig": { + "properties": { + "enforcedTimeoutSeconds": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "enforcedTimeoutSeconds" + ], + "type": "object" + }, + "SelfDeletingMessagesConfig.WithStatus": { + "properties": { + "config": { + "$ref": "#/definitions/SelfDeletingMessagesConfig" + }, + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "SelfDeletingMessagesConfig.WithStatusNoLock": { + "properties": { + "config": { + "$ref": "#/definitions/SelfDeletingMessagesConfig" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "SendActivationCode": { + "description": "Data for requesting an email or phone activation code to be sent. One of 'email' or 'phone' must be present.", + "properties": { + "email": { + "$ref": "#/definitions/Email" + }, + "locale": { + "$ref": "#/definitions/Locale" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + }, + "voice_call": { + "description": "Request the code with a call instead (default is SMS).", + "type": "boolean" + } + }, + "type": "object" + }, + "SendLoginCode": { + "description": "Payload for requesting a login code to be sent", + "properties": { + "force": { + "type": "boolean" + }, + "phone": { + "description": "E.164 phone number to send the code to", + "type": "string" + }, + "voice_call": { + "description": "Request the code with a call instead (default is SMS)", + "type": "boolean" + } + }, + "required": [ + "phone" + ], + "type": "object" + }, + "SendVerificationCode": { + "properties": { + "action": { + "$ref": "#/definitions/VerificationAction" + }, + "email": { + "$ref": "#/definitions/Email" + } + }, + "required": [ + "action", + "email" + ], + "type": "object" + }, + "ServiceKeyPEM": { + "example": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+Kg/PHHU3atXrUbKnw0\nG06FliXcNt3lMwl2os5twEDcPPFw/feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPH\nWvUBdiLfGrZqJO223DB6D8K2Su/odmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKV\nVPOaOzgtAB21XKRiQ4ermqgi3/njr03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiS\nbUKr/BeArYRcjzr/h5m1In6fG/if9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg8\n7X883H+LA/d6X5CTiPv1VMxXdBUiGPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7la\nnQIDAQAB\n-----END PUBLIC KEY-----\n", + "type": "string" + }, + "ServiceRef": { + "properties": { + "id": { + "$ref": "#/definitions/UUID" + }, + "provider": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "id", + "provider" + ], + "type": "object" + }, + "SftServer": { + "description": "Inspired by WebRTC 'RTCIceServer' object, contains details of SFT servers", + "properties": { + "urls": { + "description": "Array containing exactly one SFT server address of the form 'https://:'", + "items": { + "$ref": "#/definitions/HttpsUrl" + }, + "type": "array" + } + }, + "required": [ + "urls" + ], + "type": "object" + }, + "SimpleMember": { + "properties": { + "conversation_role": { + "$ref": "#/definitions/RoleName" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_UserId" + } + }, + "required": [ + "qualified_id" + ], + "type": "object" + }, + "SimpleMembers": { + "properties": { + "user_ids": { + "description": "deprecated", + "items": { + "$ref": "#/definitions/UUID" + }, + "type": "array" + }, + "users": { + "items": { + "$ref": "#/definitions/SimpleMember" + }, + "type": "array" + } + }, + "required": [ + "users" + ], + "type": "object" + }, + "SndFactorPasswordChallengeConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "SndFactorPasswordChallengeConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Sso": { + "properties": { + "issuer": { + "type": "string" + }, + "nameid": { + "type": "string" + } + }, + "required": [ + "issuer", + "nameid" + ], + "type": "object" + }, + "SsoSettings": { + "properties": { + "default_sso_code": { + "$ref": "#/definitions/UUID" + } + }, + "type": "object" + }, + "SystemSettings": { + "properties": { + "setEnableMls": { + "description": "Whether MLS is enabled or not", + "type": "boolean" + }, + "setRestrictUserCreation": { + "description": "Do not allow certain user creation flows", + "type": "boolean" + } + }, + "required": [ + "setRestrictUserCreation", + "setEnableMls" + ], + "type": "object" + }, + "SystemSettingsPublic": { + "properties": { + "setRestrictUserCreation": { + "description": "Do not allow certain user creation flows", + "type": "boolean" + } + }, + "required": [ + "setRestrictUserCreation" + ], + "type": "object" + }, + "Team": { + "properties": { + "binding": { + "$ref": "#/definitions/TeamBinding" + }, + "creator": { + "$ref": "#/definitions/UUID" + }, + "icon": { + "$ref": "#/definitions/Icon" + }, + "icon_key": { + "type": "string" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "name": { + "type": "string" + }, + "splash_screen": { + "$ref": "#/definitions/Icon" + } + }, + "required": [ + "id", + "creator", + "name", + "icon" + ], + "type": "object" + }, + "TeamBinding": { + "enum": [ + true, + false + ], + "type": "boolean" + }, + "TeamContact": { + "properties": { + "accent_id": { + "maximum": 9223372036854776000, + "minimum": -9223372036854776000, + "type": "integer" + }, + "created_at": { + "$ref": "#/definitions/UTCTime" + }, + "email": { + "$ref": "#/definitions/Email" + }, + "email_unvalidated": { + "$ref": "#/definitions/Email" + }, + "handle": { + "type": "string" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "managed_by": { + "$ref": "#/definitions/ManagedBy" + }, + "name": { + "type": "string" + }, + "role": { + "$ref": "#/definitions/Role" + }, + "saml_idp": { + "type": "string" + }, + "scim_external_id": { + "type": "string" + }, + "sso": { + "$ref": "#/definitions/Sso" + }, + "team": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "id", + "name" + ], + "type": "object" + }, + "TeamConversation": { + "description": "Team conversation data", + "properties": { + "conversation": { + "$ref": "#/definitions/UUID" + }, + "managed": { + "description": "This field MUST NOT be used by clients. It is here only for backwards compatibility of the interface." + } + }, + "required": [ + "conversation", + "managed" + ], + "type": "object" + }, + "TeamConversationList": { + "description": "Team conversation list", + "properties": { + "conversations": { + "items": { + "$ref": "#/definitions/TeamConversation" + }, + "type": "array" + } + }, + "required": [ + "conversations" + ], + "type": "object" + }, + "TeamDeleteData": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "verification_code": { + "$ref": "#/definitions/ASCII" + } + }, + "type": "object" + }, + "TeamList": { + "properties": { + "has_more": { + "type": "boolean" + }, + "teams": { + "items": { + "$ref": "#/definitions/Team" + }, + "type": "array" + } + }, + "required": [ + "teams", + "has_more" + ], + "type": "object" + }, + "TeamMember": { + "description": "team member data", + "properties": { + "created_at": { + "$ref": "#/definitions/UTCTime" + }, + "created_by": { + "$ref": "#/definitions/UUID" + }, + "legalhold_status": { + "$ref": "#/definitions/UserLegalHoldStatus" + }, + "permissions": { + "$ref": "#/definitions/Permissions" + }, + "user": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "user" + ], + "type": "object" + }, + "TeamMemberDeleteData": { + "description": "Data for a team member deletion request in case of binding teams.", + "properties": { + "password": { + "description": "The account password to authorise the deletion.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "TeamMemberList": { + "description": "list of team member", + "properties": { + "hasMore": { + "$ref": "#/definitions/ListType" + }, + "members": { + "description": "the array of team members", + "items": { + "$ref": "#/definitions/TeamMember" + }, + "type": "array" + } + }, + "required": [ + "members", + "hasMore" + ], + "type": "object" + }, + "TeamMembersPage": { + "properties": { + "hasMore": { + "type": "boolean" + }, + "members": { + "items": { + "$ref": "#/definitions/TeamMember" + }, + "type": "array" + }, + "pagingState": { + "$ref": "#/definitions/TeamMembers_PagingState" + } + }, + "required": [ + "members", + "hasMore", + "pagingState" + ], + "type": "object" + }, + "TeamMembers_PagingState": { + "type": "string" + }, + "TeamSearchVisibility": { + "description": "value of visibility", + "enum": [ + "standard", + "no-name-outside-team" + ], + "type": "string" + }, + "TeamSearchVisibilityView": { + "description": "Search visibility value for the team", + "properties": { + "search_visibility": { + "$ref": "#/definitions/TeamSearchVisibility" + } + }, + "required": [ + "search_visibility" + ], + "type": "object" + }, + "TeamSize": { + "description": "A simple object with a total number of team members.", + "properties": { + "teamSize": { + "description": "Team size.", + "exclusiveMinimum": false, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "teamSize" + ], + "type": "object" + }, + "TeamUpdateData": { + "properties": { + "icon": { + "$ref": "#/definitions/Icon" + }, + "icon_key": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "splash_screen": { + "$ref": "#/definitions/Icon" + } + }, + "type": "object" + }, + "Time": { + "properties": { + "time": { + "$ref": "#/definitions/UTCTime" + } + }, + "required": [ + "time" + ], + "type": "object" + }, + "TokenType": { + "enum": [ + "Bearer" + ], + "type": "string" + }, + "TurnURI": { + "type": "string" + }, + "TypingData": { + "properties": { + "status": { + "$ref": "#/definitions/TypingStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "TypingStatus": { + "enum": [ + "started", + "stopped" + ], + "type": "string" + }, + "URIRef Absolute": { + "description": "URL of the invitation link to be sent to the invitee", + "type": "string" + }, + "UTCTime": { + "example": "2021-05-12T10:52:02.671Z", + "format": "yyyy-mm-ddThh:MM:ss.qqq", + "type": "string" + }, + "UUID": { + "example": "99db9768-04e3-4b5d-9268-831b6a25c4ab", + "format": "uuid", + "type": "string" + }, + "Unnamed": { + "properties": { + "created_at": { + "$ref": "#/definitions/UTCTime" + }, + "created_by": { + "$ref": "#/definitions/UUID" + }, + "permissions": { + "$ref": "#/definitions/Permissions" + }, + "user": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "user", + "permissions" + ], + "type": "object" + }, + "UpdateClient": { + "properties": { + "capabilities": { + "description": "Hints provided by the client for the backend so it can behave in a backwards-compatible way.", + "items": { + "$ref": "#/definitions/ClientCapability" + }, + "type": "array" + }, + "label": { + "description": "A new name for this client.", + "type": "string" + }, + "lastkey": { + "$ref": "#/definitions/Prekey" + }, + "mls_public_keys": { + "$ref": "#/definitions/MLSPublicKeys" + }, + "prekeys": { + "description": "New prekeys for other clients to establish OTR sessions.", + "items": { + "$ref": "#/definitions/Prekey" + }, + "type": "array" + } + }, + "type": "object" + }, + "User": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/definitions/UserAsset" + }, + "type": "array" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "$ref": "#/definitions/Email" + }, + "expires_at": { + "$ref": "#/definitions/UTCTime" + }, + "handle": { + "$ref": "#/definitions/Handle" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "locale": { + "$ref": "#/definitions/Locale" + }, + "managed_by": { + "$ref": "#/definitions/ManagedBy" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "phone": { + "$ref": "#/definitions/PhoneNumber" + }, + "picture": { + "$ref": "#/definitions/Pict" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_UserId" + }, + "service": { + "$ref": "#/definitions/ServiceRef" + }, + "sso_id": { + "$ref": "#/definitions/UserSSOId" + }, + "team": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "id", + "qualified_id", + "name", + "accent_id", + "locale" + ], + "type": "object" + }, + "UserAsset": { + "properties": { + "key": { + "$ref": "#/definitions/AssetKey" + }, + "size": { + "$ref": "#/definitions/AssetSize" + }, + "type": { + "$ref": "#/definitions/AssetType" + } + }, + "required": [ + "key", + "type" + ], + "type": "object" + }, + "UserClientMap": { + "additionalProperties": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "type": "object" + }, + "UserClientPrekeyMap": { + "additionalProperties": { + "additionalProperties": { + "properties": { + "id": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "key": { + "type": "string" + } + }, + "required": [ + "id", + "key" + ], + "type": "object" + }, + "type": "object" + }, + "example": { + "000600d0-000b-9c1a-000d-a4130002c221": { + "44901fb0712e588f": { + "id": 1, + "key": "pQABAQECoQBYIOjl7hw0D8YRNq..." + } + } + }, + "type": "object" + }, + "UserClients": { + "additionalProperties": { + "items": { + "$ref": "#/definitions/ClientId" + }, + "type": "array" + }, + "description": "Map of user id to list of client ids.", + "example": { + "000600d0-000b-9c1a-000d-a4130002c221": [ + "60f85e4b15ad3786", + "6e323ab31554353b" + ] + }, + "type": "object" + }, + "UserConnection": { + "properties": { + "conversation": { + "$ref": "#/definitions/UUID" + }, + "from": { + "$ref": "#/definitions/UUID" + }, + "last_update": { + "$ref": "#/definitions/UTCTime" + }, + "qualified_conversation": { + "$ref": "#/definitions/Qualified_ConvId" + }, + "qualified_to": { + "$ref": "#/definitions/Qualified_UserId" + }, + "status": { + "$ref": "#/definitions/Relation" + }, + "to": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "from", + "qualified_to", + "status", + "last_update" + ], + "type": "object" + }, + "UserIdList": { + "properties": { + "user_ids": { + "items": { + "$ref": "#/definitions/UUID" + }, + "type": "array" + } + }, + "required": [ + "user_ids" + ], + "type": "object" + }, + "UserLegalHoldStatus": { + "description": "The state of Legal Hold compliance for the member", + "enum": [ + "enabled", + "pending", + "disabled", + "no_consent" + ], + "type": "string" + }, + "UserLegalHoldStatusResponse": { + "properties": { + "client": { + "$ref": "#/definitions/Id" + }, + "last_prekey": { + "$ref": "#/definitions/Prekey" + }, + "status": { + "$ref": "#/definitions/UserLegalHoldStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "UserMap_Set_PubClient": { + "additionalProperties": { + "items": { + "$ref": "#/definitions/PubClient" + }, + "type": "array", + "uniqueItems": true + }, + "description": "Map of UserId to (Set PubClient)", + "example": { + "000600d0-000b-9c1a-000d-a4130002c221": [ + { + "class": "legalhold", + "id": "d0" + } + ] + }, + "type": "object" + }, + "UserProfile": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/definitions/UserAsset" + }, + "type": "array" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "$ref": "#/definitions/Email" + }, + "expires_at": { + "$ref": "#/definitions/UTCTime" + }, + "handle": { + "$ref": "#/definitions/Handle" + }, + "id": { + "$ref": "#/definitions/UUID" + }, + "legalhold_status": { + "$ref": "#/definitions/UserLegalHoldStatus" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/definitions/Pict" + }, + "qualified_id": { + "$ref": "#/definitions/Qualified_UserId" + }, + "service": { + "$ref": "#/definitions/ServiceRef" + }, + "team": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "qualified_id", + "name", + "accent_id", + "legalhold_status" + ], + "type": "object" + }, + "UserSSOId": { + "properties": { + "scim_external_id": { + "type": "string" + }, + "subject": { + "type": "string" + }, + "tenant": { + "type": "string" + } + }, + "type": "object" + }, + "UserUpdate": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/definitions/UserAsset" + }, + "type": "array" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/definitions/Pict" + } + }, + "type": "object" + }, + "ValidateSAMLEmailsConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/definitions/LockStatus" + }, + "status": { + "$ref": "#/definitions/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709552000, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "VerificationAction": { + "enum": [ + "create_scim_token", + "login", + "delete_team" + ], + "type": "string" + }, + "VerifyDeleteUser": { + "description": "Data for verifying an account deletion.", + "properties": { + "code": { + "$ref": "#/definitions/ASCII" + }, + "key": { + "$ref": "#/definitions/ASCII" + } + }, + "required": [ + "key", + "code" + ], + "type": "object" + }, + "Version": { + "enum": [ + 0, + 1, + 2, + 3 + ], + "type": "integer" + }, + "VersionInfo": { + "example": { + "development": [ + 3 + ], + "domain": "example.com", + "federation": false, + "supported": [ + 0, + 1, + 2, + 3 + ] + }, + "properties": { + "development": { + "items": { + "$ref": "#/definitions/Version" + }, + "type": "array" + }, + "domain": { + "$ref": "#/definitions/Domain" + }, + "federation": { + "type": "boolean" + }, + "supported": { + "items": { + "$ref": "#/definitions/Version" + }, + "type": "array" + } + }, + "required": [ + "supported", + "development", + "federation", + "domain" + ], + "type": "object" + }, + "ViewLegalHoldService": { + "properties": { + "settings": { + "$ref": "#/definitions/ViewLegalHoldServiceInfo" + }, + "status": { + "$ref": "#/definitions/LHServiceStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "ViewLegalHoldServiceInfo": { + "properties": { + "auth_token": { + "$ref": "#/definitions/ASCII" + }, + "base_url": { + "$ref": "#/definitions/HttpsUrl" + }, + "fingerprint": { + "$ref": "#/definitions/Fingerprint" + }, + "public_key": { + "$ref": "#/definitions/ServiceKeyPEM" + }, + "team_id": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "team_id", + "base_url", + "fingerprint", + "auth_token", + "public_key" + ], + "type": "object" + }, + "Welcome": { + "description": "This object can only be parsed in TLS format. Please refer to the MLS specification for details." + }, + "WireIdP": { + "properties": { + "apiVersion": { + "$ref": "#/definitions/WireIdPAPIVersion" + }, + "handle": { + "type": "string" + }, + "oldIssuers": { + "items": { + "type": "string" + }, + "type": "array" + }, + "replacedBy": { + "$ref": "#/definitions/UUID" + }, + "team": { + "$ref": "#/definitions/UUID" + } + }, + "required": [ + "team", + "oldIssuers", + "handle" + ], + "type": "object" + }, + "WireIdPAPIVersion": { + "enum": [ + "WireIdPAPIV1", + "WireIdPAPIV2" + ], + "type": "string" + }, + "XmlText": { + "properties": { + "fromXmlText": { + "type": "string" + } + }, + "required": [ + "fromXmlText" + ], + "type": "object" + }, + "new-otr-message": { + "properties": { + "data": { + "type": "string" + }, + "native_priority": { + "$ref": "#/definitions/Priority" + }, + "native_push": { + "type": "boolean" + }, + "recipients": { + "$ref": "#/definitions/UserClientMap" + }, + "report_missing": { + "items": { + "$ref": "#/definitions/UUID" + }, + "type": "array" + }, + "sender": { + "$ref": "#/definitions/ClientId" + }, + "transient": { + "type": "boolean" + } + }, + "required": [ + "sender", + "recipients" + ], + "type": "object" + } + }, + "info": { + "description": "## General\n\n### SSO Endpoints\n\n#### Overview\n\n`/sso/metadata` will be requested by the IdPs to learn how to talk to wire.\n\n`/sso/initiate-login`, `/sso/finalize-login` are for the SAML authentication handshake performed by a user in order to log into wire. They are not exactly standard in their details: they may return HTML or XML; redirect to error URLs instead of throwing errors, etc.\n\n`/identity-providers` end-points are for use in the team settings page when IdPs are registered. They talk json.\n\n\n#### Configuring IdPs\n\nIdPs usually allow you to copy the metadata into your clipboard. That should contain all the details you need to post the idp in your team under `/identity-providers`. (Team id is derived from the authorization credentials of the request.)\n\n##### okta.com\n\nOkta will ask you to provide two URLs when you set it up for talking to wireapp:\n\n1. The `Single sign on URL`. This is the end-point that accepts the user's credentials after successful authentication against the IdP. Choose `/sso/finalize-login` with schema and hostname of the wire server you are configuring.\n\n2. The `Audience URI`. You can find this in the metadata returned by the `/sso/metadata` end-point. It is the contents of the `md:OrganizationURL` element.\n\n##### centrify.com\n\nCentrify allows you to upload the metadata xml document that you get from the `/sso/metadata` end-point. You can also enter the metadata url and have centrify retrieve the xml, but to guarantee integrity of the setup, the metadata should be copied from the team settings page and pasted into the centrify setup page without any URL indirections.\n\n## Federation errors\n\nEndpoints involving federated calls to other domains can return some extra failure responses, common to all endpoints. Instead of listing them as possible responses for each endpoint, we document them here.\n\nFor errors that are more likely to be transient, we suggest clients to retry whatever request resulted in the error. Transient errors are indicated explicitly below.\n\n**Note**: when a failure occurs as a result of making a federated RPC to another backend, the error response contains the following extra fields:\n\n - `type`: \"federation\" (just the literal string in quotes, which can be used as an error type identifier when parsing errors)\n - `domain`: the target backend of the RPC that failed;\n - `path`: the path of the RPC that failed.\n\n### Domain errors\n\nErrors in this category result from trying to communicate with a backend that is considered non-existent or invalid. They can result from invalid user input or client issues, but they can also be a symptom of misconfiguration in one or multiple backends. These errors have a 4xx status code.\n\n - **Remote backend not found** (status: 422, label: `invalid-domain`): This backend attempted to contact a backend which does not exist or is not properly configured. For the most part, clients can consider this error equivalent to a domain not existing, although it should be noted that certain mistakes in the DNS configuration on a remote backend can lead to the backend not being recognized, and hence to this error. It is therefore not advisable to take any destructive action upon encountering this error, such as deleting remote users from conversations.\n - **Federation denied locally** (status: 400, label: `federation-denied`): This backend attempted an RPC to a non-whitelisted backend. Similar considerations as for the previous error apply.\n - **Federation not enabled** (status: 400, label: `federation-not-enabled`): Federation has not been configured for this backend. This will happen if a federation-aware client tries to talk to a backend for which federation is disabled, or if federation was disabled on the backend after reaching a federation-specific state (e.g. conversations with remote users). There is no way to cleanly recover from these errors at this point.\n\n### Local federation errors\n\nAn error in this category likely indicates an issue with the configuration of federation on the local backend. Possibly transient errors are indicated explicitly below. All these errors have a 500 status code.\n\n - **Federation unavailable** (status: 500, label: `federation-not-available`): Federation is configured for this backend, but the local federator cannot be reached. This can be transient, so clients should retry the request.\n - **Federation not implemented** (status: 500, label: `federation-not-implemented`): Federated behaviour for a certain endpoint is not yet implemented.\n - **Federator discovery failed** (status: 400, label: `discovery-failure`): A DNS error occurred during discovery of a remote backend. This can be transient, so clients should retry the request.\n - **Local federation error** (status: 500, label: `federation-local-error`): An error occurred in the communication between this backend and its local federator. These errors are most likely caused by bugs in the backend, and should be reported as such.\n\n### Remote federation errors\n\nErrors in this category are returned in case of communication issues between the local backend and a remote one, or if the remote side encountered an error while processing an RPC. Some errors in this category might be caused by incorrect client behaviour, wrong user input, or incorrect certificate configuration. Possibly transient errors are indicated explicitly. We use non-standard 5xx status codes for these errors.\n\n - **HTTP2 error** (status: 533, label: `federation-http2-error`): The current federator encountered an error when making an HTTP2 request to a remote one. Check the error message for more details.\n - **Connection refused** (status: 521, label: `federation-connection-refused`): The local federator could not connect to a remote one. This could be transient, so clients should retry the request.\n - **TLS failure**: (status: 525, label: `federation-tls-error`): An error occurred during the TLS handshake between the local federator and a remote one. This is most likely due to an issue with the certificate on the remote end.\n - **Remote federation error** (status: 533, label: `federation-remote-error`): The remote backend could not process a request coming from this backend. Check the error message for more details.\n - **Version negotiation error** (status: 533, label: `federation-version-error`): The remote backend returned invalid version information.\n\n### Backend compatibility errors\n\nAn error in this category will be returned when this backend makes an invalid or unsupported RPC to another backend. This can indicate some incompatibility between backends or a backend bug. These errors are unlikely to be transient, so retrying requests is *not* advised.\n\n - **Version mismatch** (status: 531, label: `federation-version-mismatch`): A remote backend is running an unsupported version of the federator.\n - **Invalid content type** (status: 533, label: `federation-invalid-content-type`): An RPC to another backend returned with an invalid content type.\n - **Unsupported content type** (status: 533, label: `federation-unsupported-content-type`): An RPC to another backend returned with an unsupported content type.\n", + "title": "Wire-Server API", + "version": "" + }, + "paths": { + "/access": { + "post": { + "description": "You can provide only a cookie or a cookie and token. Every other combination is invalid. Access tokens can be given as query parameter or authorisation header, with the latter being preferred.", + "parameters": [ + { + "in": "query", + "name": "client_id", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "headers": { + "Set-Cookie": { + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/AccessToken" + } + }, + "400": { + "description": "Invalid `client_id`" + }, + "403": { + "description": "Authentication failed (label: `invalid-credentials`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Obtain an access tokens for a cookie", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "on-user-deleted-connections" + ] + ] + } + }, + "/access/logout": { + "post": { + "description": "Calling this endpoint will effectively revoke the given cookie and subsequent calls to /access with the same cookie will result in a 403.", + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Logout" + }, + "403": { + "description": "Authentication failed (label: `invalid-credentials`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Log out in order to remove a cookie from the server" + } + }, + "/access/self/email": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/EmailUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "202": { + "description": "Update accepted and pending activation of the new email", + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "204": { + "description": "No update, current and new email address are the same", + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "400": { + "description": "Invalid e-mail address. (label: `invalid-email`) or `body`", + "schema": { + "example": { + "code": 400, + "label": "invalid-email", + "message": "Invalid e-mail address." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "403": { + "description": "Authentication failed (label: `invalid-credentials`)\n\nThe given phone number has been blacklisted due to suspected abuse or a complaint (label: `blacklisted-phone`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "blacklisted-phone", + "blacklisted-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The given e-mail address or phone number is in use. (label: `key-exists`)", + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address or phone number is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Change your email address" + } + }, + "/activate": { + "get": { + "description": "See also 'POST /activate' which has a larger feature set.", + "parameters": [ + { + "description": "Activation key", + "in": "query", + "name": "key", + "required": true, + "type": "string" + }, + { + "description": "Activation code", + "in": "query", + "name": "code", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Activation successful.\n\nActivation successful. (Dry run)\n\nActivation successful.", + "schema": { + "$ref": "#/definitions/ActivationResponse" + } + }, + "204": { + "description": "A recent activation was already successful." + }, + "400": { + "description": "Invalid `code` or `key`\n\nInvalid mobile phone number (label: `invalid-phone`)\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "404": { + "description": "Invalid activation code (label: `invalid-code`)\n\nUser does not exist (label: `invalid-code`)", + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "Invalid activation code" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The given e-mail address or phone number is in use. (label: `key-exists`)", + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address or phone number is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Activate (i.e. confirm) an email address or phone number.", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "on-user-deleted-connections" + ] + ] + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": "Activation only succeeds once and the number of failed attempts for a valid key is limited.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Activate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Activation successful.\n\nActivation successful. (Dry run)\n\nActivation successful.", + "schema": { + "$ref": "#/definitions/ActivationResponse" + } + }, + "204": { + "description": "A recent activation was already successful." + }, + "400": { + "description": "Invalid `body`\n\nInvalid mobile phone number (label: `invalid-phone`)\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "404": { + "description": "Invalid activation code (label: `invalid-code`)\n\nUser does not exist (label: `invalid-code`)", + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "Invalid activation code" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The given e-mail address or phone number is in use. (label: `key-exists`)", + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address or phone number is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Activate (i.e. confirm) an email address or phone number.", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "on-user-deleted-connections" + ] + ] + } + }, + "/activate/send": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SendActivationCode" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Activation code sent." + }, + "400": { + "description": "Invalid `body`\n\nInvalid mobile phone number (label: `invalid-phone`)\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "403": { + "description": "The given phone number has been blacklisted due to suspected abuse or a complaint (label: `blacklisted-phone`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)", + "schema": { + "example": { + "code": 403, + "label": "blacklisted-phone", + "message": "The given phone number has been blacklisted due to suspected abuse or a complaint" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "blacklisted-phone", + "blacklisted-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The given e-mail address or phone number is in use. (label: `key-exists`)", + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address or phone number is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "451": { + "description": "[Customer extension] the email domain example.com that you are attempting to register a user with has been blocked for creating wire users. Please contact your IT department. (label: `domain-blocked-for-registration`)", + "schema": { + "example": { + "code": 451, + "label": "domain-blocked-for-registration", + "message": "[Customer extension] the email domain example.com that you are attempting to register a user with has been blocked for creating wire users. Please contact your IT department." + }, + "properties": { + "code": { + "enum": [ + 451 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-blocked-for-registration" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Send (or resend) an email or phone activation code." + } + }, + "/api-version": { + "get": { + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/VersionInfo" + } + } + } + } + }, + "/assets": { + "post": { + "consumes": [ + "multipart/mixed" + ], + "parameters": [ + { + "description": "A body with content type `multipart/mixed body`. The first section's content type should be `application/json`. The second section's content type should be always be `application/octet-stream`. Other content types will be ignored by the server.", + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/AssetSource" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Asset posted", + "headers": { + "Location": { + "description": "Asset location", + "format": "url", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Asset" + } + }, + "400": { + "description": "Invalid `body`\n\nInvalid content length (label: `invalid-length`)" + }, + "413": { + "description": "Asset too large (label: `client-error`)", + "schema": { + "example": { + "code": 413, + "label": "client-error", + "message": "Asset too large" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Upload an asset" + } + }, + "/assets/{key_domain}/{key}": { + "delete": { + "description": "**Note**: only local assets can be deleted.", + "parameters": [ + { + "in": "path", + "name": "key_domain", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "key", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Asset deleted" + }, + "400": { + "description": "Invalid `key` or `key_domain`" + }, + "403": { + "description": "Unauthorised operation (label: `unauthorised`)", + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Asset not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Delete an asset" + }, + "get": { + "description": "**Note**: local assets result in a redirect, while remote assets are streamed directly.", + "parameters": [ + { + "in": "path", + "name": "key_domain", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "key", + "required": true, + "type": "string" + }, + { + "in": "header", + "name": "Asset-Token", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "asset_token", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Asset returned directly with content type `application/octet-stream`" + }, + "302": { + "description": "Asset found", + "headers": { + "Location": { + "description": "Asset location", + "format": "url", + "type": "string" + } + } + }, + "400": { + "description": "Invalid `asset_token` or `Asset-Token` or `key` or `key_domain`" + }, + "404": { + "description": "Asset not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Download an asset", + "x-wire-makes-federated-call-to": [ + [ + "cargohold", + "get-asset" + ], + [ + "cargohold", + "stream-asset" + ] + ] + } + }, + "/assets/{key}/token": { + "delete": { + "description": "**Note**: deleting the token makes the asset public.", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Asset token deleted" + }, + "400": { + "description": "Invalid `key`" + } + }, + "summary": "Delete an asset token" + }, + "post": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/NewAssetToken" + } + }, + "400": { + "description": "Invalid `key`" + }, + "403": { + "description": "Unauthorised operation (label: `unauthorised`)", + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Asset not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Renew an asset token" + } + }, + "/await": { + "get": { + "externalDocs": { + "description": "RFC 6455", + "url": "https://datatracker.ietf.org/doc/html/rfc6455" + }, + "parameters": [ + { + "description": "Client ID", + "in": "query", + "name": "client", + "required": false, + "type": "string" + } + ], + "responses": { + "101": { + "description": "Connection upgraded." + }, + "400": { + "description": "Invalid `client`" + }, + "426": { + "description": "Upgrade required." + } + }, + "summary": "Establish websocket connection" + } + }, + "/bot/assets": { + "post": { + "consumes": [ + "multipart/mixed" + ], + "parameters": [ + { + "description": "A body with content type `multipart/mixed body`. The first section's content type should be `application/json`. The second section's content type should be always be `application/octet-stream`. Other content types will be ignored by the server.", + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/AssetSource" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Asset posted", + "headers": { + "Location": { + "description": "Asset location", + "format": "url", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Asset" + } + }, + "400": { + "description": "Invalid `body`\n\nInvalid content length (label: `invalid-length`)" + }, + "413": { + "description": "Asset too large (label: `client-error`)", + "schema": { + "example": { + "code": 413, + "label": "client-error", + "message": "Asset too large" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Upload an asset" + } + }, + "/bot/assets/{key}": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Asset deleted" + }, + "400": { + "description": "Invalid `key`" + }, + "403": { + "description": "Unauthorised operation (label: `unauthorised`)", + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Asset not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Delete an asset" + }, + "get": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "type": "string" + }, + { + "in": "header", + "name": "Asset-Token", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "asset_token", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "302": { + "description": "Asset found", + "headers": { + "Location": { + "description": "Asset location", + "format": "url", + "type": "string" + } + } + }, + "400": { + "description": "Invalid `asset_token` or `Asset-Token` or `key`" + }, + "404": { + "description": "Asset not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Download an asset" + } + }, + "/bot/messages": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "query", + "name": "ignore_missing", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "report_missing", + "required": false, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/new-otr-message" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Message sent", + "schema": { + "$ref": "#/definitions/ClientMismatch" + } + }, + "400": { + "description": "Invalid `body` or `report_missing` or `ignore_missing`" + }, + "403": { + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)", + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)\n\nConversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "412": { + "description": "Missing clients", + "schema": { + "$ref": "#/definitions/ClientMismatch" + } + } + }, + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-message-sent" + ], + [ + "brig", + "get-user-clients" + ] + ] + } + }, + "/broadcast/otr/messages": { + "post": { + "consumes": [ + "application/json;charset=utf-8", + "application/x-protobuf" + ], + "description": "This endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo 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\nApart 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\nAll 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\nThis 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.", + "parameters": [ + { + "in": "query", + "name": "ignore_missing", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "report_missing", + "required": false, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/new-otr-message" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Message sent", + "schema": { + "$ref": "#/definitions/ClientMismatch" + } + }, + "400": { + "description": "Invalid `body` or `report_missing` or `ignore_missing`\n\nToo many users to fan out the broadcast event to (label: `too-many-users-to-broadcast`)" + }, + "403": { + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)", + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)\n\nNot a member of a binding team (label: `non-binding-team`)\n\nTeam not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "non-binding-team", + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "412": { + "description": "Missing clients", + "schema": { + "$ref": "#/definitions/ClientMismatch" + } + } + }, + "summary": "Broadcast an encrypted message to all team members and all contacts (accepts JSON or Protobuf)" + } + }, + "/broadcast/proteus/messages": { + "post": { + "consumes": [ + "application/x-protobuf" + ], + "description": "This endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo 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\nThe 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\nThis 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.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/QualifiedNewOtrMessage" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Message sent", + "schema": { + "$ref": "#/definitions/MessageSendingStatus" + } + }, + "400": { + "description": "Invalid `body`\n\nToo many users to fan out the broadcast event to (label: `too-many-users-to-broadcast`)" + }, + "403": { + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)", + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)\n\nNot a member of a binding team (label: `non-binding-team`)\n\nTeam not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "non-binding-team", + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "412": { + "description": "Missing clients", + "schema": { + "$ref": "#/definitions/MessageSendingStatus" + } + } + }, + "summary": "Post an encrypted message to all team members and all contacts (accepts only Protobuf)" + } + }, + "/calls/config": { + "get": { + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/RTCConfiguration" + } + } + }, + "summary": "[deprecated] Retrieve TURN server addresses and credentials for IP addresses, scheme `turn` and transport `udp` only" + } + }, + "/calls/config/v2": { + "get": { + "parameters": [ + { + "description": "Limit resulting list. Allowed values [1..10]", + "in": "query", + "maximum": 10, + "minimum": 1, + "name": "limit", + "required": false, + "type": "integer" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/RTCConfiguration" + } + }, + "400": { + "description": "Invalid `limit`" + } + }, + "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 " + } + }, + "/clients": { + "get": { + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "items": { + "$ref": "#/definitions/Client" + }, + "type": "array" + } + } + }, + "summary": "List the registered clients" + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "header", + "name": "X-Forwarded-For", + "required": false, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewClient" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "201": { + "description": "", + "headers": { + "Location": { + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Client" + } + }, + "400": { + "description": "Invalid `body` or `X-Forwarded-For`\n\nMalformed prekeys uploaded (label: `bad-request`)" + }, + "403": { + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nRe-authentication via password required (label: `missing-auth`)\n\nToo many clients (label: `too-many-clients`)", + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "missing-auth", + "too-many-clients" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Register a new client", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "on-user-deleted-connections" + ] + ] + } + }, + "/clients/{cid}/access-token": { + "post": { + "description": "[implementation stub, not supported yet!] Create an JWT DPoP access token for the client CSR, given a JWT DPoP proof, specified in the `DPoP` header. The access token will be returned as JWT DPoP token in the `DPoP` header.", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "cid", + "required": true, + "type": "string" + }, + { + "in": "header", + "name": "DPoP", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Access token created", + "headers": { + "Cache-Control": { + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/DPoPAccessTokenResponse" + } + }, + "400": { + "description": "Invalid `DPoP` or `cid`" + } + }, + "summary": "Create a JWT DPoP access token" + } + }, + "/clients/{client}": { + "delete": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DeleteClient" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Client deleted" + }, + "400": { + "description": "Invalid `body` or `client`" + } + }, + "summary": "Delete an existing client" + }, + "get": { + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Client found", + "schema": { + "$ref": "#/definitions/Client" + } + }, + "400": { + "description": "Invalid `client`" + }, + "404": { + "description": "Client not found(**Note**: This error has an empty body for legacy reasons)" + } + }, + "summary": "Get a registered client by ID" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateClient" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Client updated" + }, + "400": { + "description": "Invalid `body` or `client`\n\nMalformed prekeys uploaded (label: `bad-request`)" + } + }, + "summary": "Update a registered client" + } + }, + "/clients/{client}/capabilities": { + "get": { + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ClientCapabilityList" + } + }, + "400": { + "description": "Invalid `client`" + } + }, + "summary": "Read back what the client has been posting about itself" + } + }, + "/clients/{client}/nonce": { + "get": { + "description": "Get a new nonce for a client CSR, specified in the response header `Replay-Nonce` as a uuidv4 in base64url encoding.", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "204": { + "description": "No Content", + "headers": { + "Cache-Control": { + "type": "string" + }, + "Replay-Nonce": { + "type": "string" + } + } + }, + "400": { + "description": "Invalid `client`" + } + }, + "summary": "Get a new nonce for a client CSR" + }, + "head": { + "description": "Get a new nonce for a client CSR, specified in the response header `Replay-Nonce` as a uuidv4 in base64url encoding.", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "No Content", + "headers": { + "Cache-Control": { + "type": "string" + }, + "Replay-Nonce": { + "type": "string" + } + } + }, + "400": { + "description": "Invalid `client`" + } + }, + "summary": "Get a new nonce for a client CSR" + } + }, + "/clients/{client}/prekeys": { + "get": { + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "items": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "type": "array" + } + }, + "400": { + "description": "Invalid `client`" + } + }, + "summary": "List the remaining prekey IDs of a client" + } + }, + "/connections/{uid_domain}/{uid}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Connection found", + "schema": { + "$ref": "#/definitions/UserConnection" + } + }, + "400": { + "description": "Invalid `uid` or `uid_domain`" + }, + "404": { + "description": "Connection not found(**Note**: This error has an empty body for legacy reasons)" + } + }, + "summary": "Get an existing connection to another user (local or remote)" + }, + "post": { + "description": "You can have no more than 1000 connections in accepted or sent state", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Connection existed", + "schema": { + "$ref": "#/definitions/UserConnection" + } + }, + "201": { + "description": "Connection was created", + "schema": { + "$ref": "#/definitions/UserConnection" + } + }, + "400": { + "description": "Invalid `uid` or `uid_domain`\n\nInvalid user (label: `invalid-user`)" + }, + "403": { + "description": "The user has no verified identity (email or phone number) (label: `no-identity`)\n\nToo many sent/accepted connections (label: `connection-limit`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)", + "schema": { + "example": { + "code": 403, + "label": "no-identity", + "message": "The user has no verified identity (email or phone number)" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-identity", + "connection-limit", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Create a connection to another user", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "send-connection-action" + ] + ] + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConnectionUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Connection updated", + "schema": { + "$ref": "#/definitions/UserConnection" + } + }, + "204": { + "description": "Connection unchanged" + }, + "400": { + "description": "Invalid `body` or `uid` or `uid_domain`\n\nInvalid user (label: `invalid-user`)" + }, + "403": { + "description": "The user has no verified identity (email or phone number) (label: `no-identity`)\n\nInvalid status transition (label: `bad-conn-update`)\n\nUsers are not connected (label: `not-connected`)\n\nToo many sent/accepted connections (label: `connection-limit`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)", + "schema": { + "example": { + "code": 403, + "label": "no-identity", + "message": "The user has no verified identity (email or phone number)" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-identity", + "bad-conn-update", + "not-connected", + "connection-limit", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update a connection to another user", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "send-connection-action" + ] + ] + } + }, + "/conversations": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": "This returns 201 when a new conversation is created, and 200 when the conversation already existed", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewConv" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation existed", + "headers": { + "Location": { + "description": "Conversation ID", + "format": "uuid", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Conversation" + } + }, + "201": { + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "format": "uuid", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Conversation" + } + }, + "400": { + "description": "Invalid `body`\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nAttempting to add group members outside MLS (label: `non-empty-member-list`)" + }, + "403": { + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nUsers are not connected (label: `not-connected`)\n\nConversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "operation-denied", + "no-team-member", + "not-connected", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Create a new conversation", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-created" + ] + ] + } + }, + "/conversations/code-check": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationCode" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Valid" + }, + "400": { + "description": "Invalid `body`" + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "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." + } + }, + "/conversations/join": { + "get": { + "parameters": [ + { + "in": "query", + "name": "key", + "required": true, + "type": "string" + }, + { + "in": "query", + "name": "code", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ConversationCoverView" + } + }, + "400": { + "description": "Invalid `code` or `key`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)", + "schema": { + "example": { + "code": 409, + "label": "guest-links-disabled", + "message": "The guest link feature is disabled and all guest links have been revoked" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get limited conversation information by key/code pair" + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationCode" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation joined", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Conversation unchanged" + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Maximum number of members per conversation reached (label: `too-many-members`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "too-many-members", + "message": "Maximum number of members per conversation reached" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-members", + "no-team-member", + "invalid-op", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)", + "schema": { + "example": { + "code": 409, + "label": "guest-links-disabled", + "message": "The guest link feature is disabled and all guest links have been revoked" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "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.", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-new-remote-conversation" + ] + ] + } + }, + "/conversations/list": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ListConversations" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ConversationsResponse" + } + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "Get conversation metadata for a list of conversation ids", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "get-conversations" + ] + ] + } + }, + "/conversations/list-ids": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": "The IDs returned by this endpoint are paginated. To get the first page, make a call with the `paging_state` field set to `null` (or omitted). Whenever the `has_more` field of the response is set to `true`, more results are available, and they can be obtained by calling the endpoint again, but this time passing the value of `paging_state` returned by the previous call. One can continue in this fashion until all results are returned, which is indicated by `has_more` being `false`. Note that `paging_state` should be considered an opaque token. It should not be inspected, or stored, or reused across multiple unrelated invocations of the endpoint.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/GetPaginated_ConversationIds" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ConversationIds_Page" + } + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "Get all conversation IDs." + } + }, + "/conversations/one2one": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewConv" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation existed", + "headers": { + "Location": { + "description": "Conversation ID", + "format": "uuid", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Conversation" + } + }, + "201": { + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "format": "uuid", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Conversation" + } + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nInsufficient permissions (label: `operation-denied`)\n\nUsers are not connected (label: `not-connected`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nBoth users must be members of the same binding team (label: `non-binding-team-members`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "operation-denied", + "not-connected", + "no-team-member", + "non-binding-team-members", + "invalid-op", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)\n\nNot a member of a binding team (label: `non-binding-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "non-binding-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Create a 1:1 conversation", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-created" + ] + ] + } + }, + "/conversations/self": { + "post": { + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation existed", + "headers": { + "Location": { + "description": "Conversation ID", + "format": "uuid", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Conversation" + } + }, + "201": { + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "format": "uuid", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Conversation" + } + } + }, + "summary": "Create a self-conversation" + } + }, + "/conversations/{cnv_domain}/{cnv}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/Conversation" + } + }, + "400": { + "description": "Invalid `cnv` or `cnv_domain`" + }, + "403": { + "description": "Conversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get a conversation by ID", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "get-conversations" + ] + ] + } + }, + "/conversations/{cnv_domain}/{cnv}/access": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationAccessData" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Access updated", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Access unchanged" + }, + "400": { + "description": "Invalid `body` or `cnv` or `cnv_domain`" + }, + "403": { + "description": "Invalid target access (label: `invalid-op`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nInsufficient authorization (missing modify_conversation_access) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid target access" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update access modes for a conversation", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "galley", + "on-new-remote-conversation" + ] + ] + } + }, + "/conversations/{cnv_domain}/{cnv}/members": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/InviteQualified" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation updated", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Conversation unchanged" + }, + "400": { + "description": "Invalid `body` or `cnv` or `cnv_domain`" + }, + "403": { + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nUsers are not connected (label: `not-connected`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nMaximum number of members per conversation reached (label: `too-many-members`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing leave_conversation) (label: `action-denied`)\n\nInsufficient authorization (missing add_conversation_member) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "not-connected", + "no-team-member", + "access-denied", + "too-many-members", + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Add qualified members to an existing conversation.", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "galley", + "on-new-remote-conversation" + ] + ] + } + }, + "/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "usr_domain", + "required": true, + "type": "string" + }, + { + "description": "Target User ID", + "format": "uuid", + "in": "path", + "name": "usr", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Member removed", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "No change" + }, + "400": { + "description": "Invalid `usr` or `usr_domain` or `cnv` or `cnv_domain`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Remove a member from a conversation", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "leave-conversation" + ], + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "galley", + "on-new-remote-conversation" + ] + ] + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": "**Note**: at least one field has to be provided.", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "usr_domain", + "required": true, + "type": "string" + }, + { + "description": "Target User ID", + "format": "uuid", + "in": "path", + "name": "usr", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/OtherMemberUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Membership updated" + }, + "400": { + "description": "Invalid `body` or `usr` or `usr_domain` or `cnv` or `cnv_domain`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nInvalid target (label: `invalid-op`)\n\nInsufficient authorization (missing modify_other_conversation_member) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation member not found (label: `no-conversation-member`)\n\nConversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation-member", + "message": "Conversation member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation-member", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update membership of the specified user", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "galley", + "on-new-remote-conversation" + ] + ] + } + }, + "/conversations/{cnv_domain}/{cnv}/message-timer": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationMessageTimerUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Message timer updated", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Message timer unchanged" + }, + "400": { + "description": "Invalid `body` or `cnv` or `cnv_domain`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_message_timer) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update the message timer for a conversation", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "galley", + "on-new-remote-conversation" + ] + ] + } + }, + "/conversations/{cnv_domain}/{cnv}/name": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationRename" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Name unchanged", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Name updated" + }, + "400": { + "description": "Invalid `body` or `cnv` or `cnv_domain`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing modify_conversation_name) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update conversation name", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "galley", + "on-new-remote-conversation" + ] + ] + } + }, + "/conversations/{cnv_domain}/{cnv}/proteus/messages": { + "post": { + "consumes": [ + "application/x-protobuf" + ], + "description": "This endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo 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\nThe 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\nThis 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.", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/QualifiedNewOtrMessage" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Message sent", + "schema": { + "$ref": "#/definitions/MessageSendingStatus" + } + }, + "400": { + "description": "Invalid `body` or `cnv` or `cnv_domain`" + }, + "403": { + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)", + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "412": { + "description": "Missing clients", + "schema": { + "$ref": "#/definitions/MessageSendingStatus" + } + } + }, + "summary": "Post an encrypted message to a conversation (accepts only Protobuf)", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "get-user-clients" + ], + [ + "galley", + "on-message-sent" + ], + [ + "galley", + "send-message" + ] + ] + } + }, + "/conversations/{cnv_domain}/{cnv}/receipt-mode": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationReceiptModeUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Receipt mode updated", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Receipt mode unchanged" + }, + "400": { + "description": "Invalid `body` or `cnv` or `cnv_domain`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_receipt_mode) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update receipt mode for a conversation", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "galley", + "on-new-remote-conversation" + ], + [ + "galley", + "update-conversation" + ] + ] + } + }, + "/conversations/{cnv_domain}/{cnv}/self": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": "**Note**: at least one field has to be provided.", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/MemberUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Update successful" + }, + "400": { + "description": "Invalid `body` or `cnv` or `cnv_domain`" + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update self membership properties" + } + }, + "/conversations/{cnv_domain}/{cnv}/typing": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "type": "string" + }, + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/TypingData" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Notification sent" + }, + "400": { + "description": "Invalid `body` or `cnv` or `cnv_domain`" + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Sending typing notifications", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-typing-indicator-updated" + ] + ] + } + }, + "/conversations/{cnv}": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": "Use `/conversations/:domain/:conv/name` instead.", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationRename" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Name updated", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Name unchanged" + }, + "400": { + "description": "Invalid `body` or `cnv`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing modify_conversation_name) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update conversation name (deprecated)", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "galley", + "on-new-remote-conversation" + ] + ] + } + }, + "/conversations/{cnv}/code": { + "delete": { + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation code deleted.", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "400": { + "description": "Invalid `cnv`" + }, + "403": { + "description": "Conversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Delete conversation code" + }, + "get": { + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation Code", + "schema": { + "$ref": "#/definitions/ConversationCode" + } + }, + "400": { + "description": "Invalid `cnv`" + }, + "403": { + "description": "Conversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)", + "schema": { + "example": { + "code": 409, + "label": "guest-links-disabled", + "message": "The guest link feature is disabled and all guest links have been revoked" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get existing conversation code" + }, + "post": { + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation code already exists.", + "schema": { + "$ref": "#/definitions/ConversationCode" + } + }, + "201": { + "description": "Conversation code created.", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "400": { + "description": "Invalid `cnv`" + }, + "403": { + "description": "Conversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)", + "schema": { + "example": { + "code": 409, + "label": "guest-links-disabled", + "message": "The guest link feature is disabled and all guest links have been revoked" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Create or recreate a conversation code" + } + }, + "/conversations/{cnv}/features/conversationGuestLinks": { + "get": { + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/GuestLinksConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `cnv`" + }, + "403": { + "description": "Conversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get the status of the guest links feature for a conversation that potentially has been created by someone from another team." + } + }, + "/conversations/{cnv}/join": { + "post": { + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation joined", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Conversation unchanged" + }, + "400": { + "description": "Invalid `cnv`" + }, + "403": { + "description": "Maximum number of members per conversation reached (label: `too-many-members`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "too-many-members", + "message": "Maximum number of members per conversation reached" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-members", + "no-team-member", + "invalid-op", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Join a conversation by its ID (if link access enabled)", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-new-remote-conversation" + ] + ] + } + }, + "/conversations/{cnv}/members/{usr}": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": "Use `PUT /conversations/:cnv_domain/:cnv/members/:usr_domain/:usr` instead", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "description": "Target User ID", + "format": "uuid", + "in": "path", + "name": "usr", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/OtherMemberUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Membership updated" + }, + "400": { + "description": "Invalid `body` or `usr` or `cnv`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nInvalid target (label: `invalid-op`)\n\nInsufficient authorization (missing modify_other_conversation_member) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation member not found (label: `no-conversation-member`)\n\nConversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation-member", + "message": "Conversation member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation-member", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update membership of the specified user (deprecated)", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "galley", + "on-new-remote-conversation" + ] + ] + } + }, + "/conversations/{cnv}/message-timer": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": "Use `/conversations/:domain/:cnv/message-timer` instead.", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationMessageTimerUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Message timer updated", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Message timer unchanged" + }, + "400": { + "description": "Invalid `body` or `cnv`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_message_timer) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update the message timer for a conversation (deprecated)", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "galley", + "on-new-remote-conversation" + ] + ] + } + }, + "/conversations/{cnv}/name": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": "Use `/conversations/:domain/:conv/name` instead.", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationRename" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Name updated", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Name unchanged" + }, + "400": { + "description": "Invalid `body` or `cnv`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing modify_conversation_name) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update conversation name (deprecated)", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "galley", + "on-new-remote-conversation" + ] + ] + } + }, + "/conversations/{cnv}/otr/messages": { + "post": { + "consumes": [ + "application/json;charset=utf-8", + "application/x-protobuf" + ], + "description": "This endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo 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\nApart 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\nAll 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\nThis 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.", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "query", + "name": "ignore_missing", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "report_missing", + "required": false, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/new-otr-message" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Message sent", + "schema": { + "$ref": "#/definitions/ClientMismatch" + } + }, + "400": { + "description": "Invalid `body` or `report_missing` or `ignore_missing` or `cnv`" + }, + "403": { + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)", + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "412": { + "description": "Missing clients", + "schema": { + "$ref": "#/definitions/ClientMismatch" + } + } + }, + "summary": "Post an encrypted message to a conversation (accepts JSON or Protobuf)", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-message-sent" + ], + [ + "brig", + "get-user-clients" + ] + ] + } + }, + "/conversations/{cnv}/receipt-mode": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": "Use `PUT /conversations/:domain/:cnv/receipt-mode` instead.", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ConversationReceiptModeUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Receipt mode updated", + "schema": { + "$ref": "#/definitions/Event" + } + }, + "204": { + "description": "Receipt mode unchanged" + }, + "400": { + "description": "Invalid `body` or `cnv`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_receipt_mode) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update receipt mode for a conversation (deprecated)", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "galley", + "on-new-remote-conversation" + ], + [ + "galley", + "update-conversation" + ] + ] + } + }, + "/conversations/{cnv}/roles": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ConversationRolesList" + } + }, + "400": { + "description": "Invalid `cnv`" + }, + "403": { + "description": "Conversation access denied (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get existing roles available for the given conversation" + } + }, + "/conversations/{cnv}/self": { + "get": { + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/Member" + } + }, + "400": { + "description": "Invalid `cnv`" + } + }, + "summary": "Get self membership properties (deprecated)" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": "Use `/conversations/:domain/:conv/self` instead.", + "parameters": [ + { + "description": "Conversation ID", + "format": "uuid", + "in": "path", + "name": "cnv", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/MemberUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Update successful" + }, + "400": { + "description": "Invalid `body` or `cnv`" + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update self membership properties (deprecated)" + } + }, + "/cookies": { + "get": { + "parameters": [ + { + "description": "Filter by label (comma-separated list)", + "in": "query", + "name": "labels", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "List of cookies", + "schema": { + "$ref": "#/definitions/CookieList" + } + }, + "400": { + "description": "Invalid `labels`" + } + }, + "summary": "Retrieve the list of cookies currently stored for the user" + } + }, + "/cookies/remove": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RemoveCookies" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Cookies revoked" + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Authentication failed (label: `invalid-credentials`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Revoke stored cookies" + } + }, + "/custom-backend/by-domain/{domain}": { + "get": { + "parameters": [ + { + "description": "URL-encoded email domain", + "in": "path", + "name": "domain", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/CustomBackend" + } + }, + "400": { + "description": "Invalid `domain`" + }, + "404": { + "description": "Custom backend not found (label: `custom-backend-not-found`)", + "schema": { + "example": { + "code": 404, + "label": "custom-backend-not-found", + "message": "Custom backend not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "custom-backend-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Shows information about custom backends related to a given email domain" + } + }, + "/delete": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/VerifyDeleteUser" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Deletion is initiated." + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Invalid verification code (label: `invalid-code`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-code", + "message": "Invalid verification code" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Verify account deletion with a code.", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "on-user-deleted-connections" + ] + ] + } + }, + "/feature-configs": { + "get": { + "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).", + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/AllFeatureConfigs" + } + }, + "403": { + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Gets feature configs for a user" + } + }, + "/identity-providers": { + "get": { + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/IdPList" + } + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/IdPMetadataInfo" + } + }, + { + "format": "uuid", + "in": "query", + "name": "replaces", + "required": false, + "type": "string" + }, + { + "default": "v2", + "enum": [ + "v1", + "v2" + ], + "in": "query", + "name": "api_version", + "required": false, + "type": "string" + }, + { + "in": "query", + "maxLength": 1, + "minLength": 32, + "name": "handle", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "201": { + "description": "", + "schema": { + "$ref": "#/definitions/IdPConfig" + } + }, + "400": { + "description": "Invalid `handle` or `api_version` or `replaces` or `body`" + } + } + } + }, + "/identity-providers/{id}": { + "delete": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "id", + "required": true, + "type": "string" + }, + { + "in": "query", + "name": "purge", + "required": false, + "type": "boolean" + } + ], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "Invalid `purge` or `id`" + } + } + }, + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "id", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/IdPConfig" + } + }, + "400": { + "description": "Invalid `id`" + } + } + }, + "put": { + "consumes": [ + "application/xml", + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/IdPMetadataInfo" + } + }, + { + "format": "uuid", + "in": "path", + "name": "id", + "required": true, + "type": "string" + }, + { + "in": "query", + "maxLength": 1, + "minLength": 32, + "name": "handle", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/IdPConfig" + } + }, + "400": { + "description": "Invalid `handle` or `id` or `body`" + } + } + } + }, + "/identity-providers/{id}/raw": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "id", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/xml" + ], + "responses": { + "200": { + "description": "", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid `id`" + } + } + } + }, + "/list-connections": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": "The IDs returned by this endpoint are paginated. To get the first page, make a call with the `paging_state` field set to `null` (or omitted). Whenever the `has_more` field of the response is set to `true`, more results are available, and they can be obtained by calling the endpoint again, but this time passing the value of `paging_state` returned by the previous call. One can continue in this fashion until all results are returned, which is indicated by `has_more` being `false`. Note that `paging_state` should be considered an opaque token. It should not be inspected, or stored, or reused across multiple unrelated invocations of the endpoint.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/GetPaginated_Connections" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/Connections_Page" + } + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "List the connections to other users, including remote users" + } + }, + "/list-users": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": "The 'qualified_ids' and 'qualified_handles' parameters are mutually exclusive.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ListUsersQuery" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "items": { + "$ref": "#/definitions/UserProfile" + }, + "type": "array" + } + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "List users", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "get-users-by-ids" + ] + ] + } + }, + "/login": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": "Logins are throttled at the server's discretion", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Login" + } + }, + { + "description": "Request a persistent cookie instead of a session cookie", + "in": "query", + "name": "persist", + "required": false, + "type": "boolean" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "headers": { + "Set-Cookie": { + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/AccessToken" + } + }, + "400": { + "description": "Invalid `persist` or `body`" + }, + "403": { + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nAccount pending activation (label: `pending-activation`)\n\nAccount suspended (label: `suspended`)\n\nAuthentication failed (label: `invalid-credentials`)", + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "pending-activation", + "suspended", + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Authenticate a user to obtain a cookie and first access token", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "on-user-deleted-connections" + ] + ] + } + }, + "/login/send": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": "This operation generates and sends a login code via sms for phone login. A login code can be used only once and times out after 10 minutes. Only one login code may be pending at a time. For 2nd factor authentication login with email and password, use the `/verification-code/send` endpoint.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SendLoginCode" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/LoginCodeTimeout" + } + }, + "400": { + "description": "Invalid mobile phone number (label: `invalid-phone`) or `body`", + "schema": { + "example": { + "code": 400, + "label": "invalid-phone", + "message": "Invalid mobile phone number" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-phone" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "403": { + "description": "The operation is not permitted because the user has a password set (label: `password-exists`)", + "schema": { + "example": { + "code": 403, + "label": "password-exists", + "message": "The operation is not permitted because the user has a password set" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Send a login code to a verified phone number" + } + }, + "/mls/key-packages/claim/{user_domain}/{user}": { + "post": { + "parameters": [ + { + "in": "path", + "name": "user_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "user", + "required": true, + "type": "string" + }, + { + "description": "Do not claim a key package for the given own client", + "in": "query", + "name": "skip_own", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Claimed key packages", + "schema": { + "$ref": "#/definitions/KeyPackageBundle" + } + }, + "400": { + "description": "Invalid `skip_own` or `user` or `user_domain`" + } + }, + "summary": "Claim one key package for each client of the given user", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "claim-key-packages" + ] + ] + } + }, + "/mls/key-packages/self/{client}": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": "The request body should be a json object containing a list of base64-encoded key packages.", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/KeyPackageUpload" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json", + "message/mls" + ], + "responses": { + "201": { + "description": "Key packages uploaded" + }, + "400": { + "description": "Invalid `body` or `client`\n\nMLS protocol error (label: `mls-protocol-error`)" + }, + "403": { + "description": "Key package credential does not match qualified client ID (label: `mls-identity-mismatch`)", + "schema": { + "example": { + "code": 403, + "label": "mls-identity-mismatch", + "message": "Key package credential does not match qualified client ID" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-identity-mismatch" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Upload a fresh batch of key packages" + } + }, + "/mls/key-packages/self/{client}/count": { + "get": { + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Number of key packages", + "schema": { + "$ref": "#/definitions/OwnKeyPackages" + } + }, + "400": { + "description": "Invalid `client`" + } + }, + "summary": "Return the number of unused key packages for the given client" + } + }, + "/notifications": { + "get": { + "parameters": [ + { + "description": "Only return notifications more recent than this", + "format": "uuid", + "in": "query", + "name": "since", + "required": false, + "type": "string" + }, + { + "description": "Only return notifications targeted at the given client", + "in": "query", + "name": "client", + "required": false, + "type": "string" + }, + { + "description": "Maximum number of notifications to return", + "format": "int32", + "in": "query", + "maximum": 10000, + "minimum": 100, + "name": "size", + "required": false, + "type": "integer" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Notification list", + "schema": { + "$ref": "#/definitions/QueuedNotificationList" + } + }, + "400": { + "description": "Invalid `size` or `client` or `since`" + }, + "404": { + "description": "Some notifications not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Fetch notifications" + } + }, + "/notifications/last": { + "get": { + "parameters": [ + { + "description": "Only return notifications targeted at the given client", + "in": "query", + "name": "client", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Notification found", + "schema": { + "$ref": "#/definitions/QueuedNotification" + } + }, + "400": { + "description": "Invalid `client`" + }, + "404": { + "description": "Some notifications not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Fetch the last notification" + } + }, + "/notifications/{id}": { + "get": { + "parameters": [ + { + "description": "Notification ID", + "format": "uuid", + "in": "path", + "name": "id", + "required": true, + "type": "string" + }, + { + "description": "Only return notifications targeted at the given client", + "in": "query", + "name": "client", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Notification found", + "schema": { + "$ref": "#/definitions/QueuedNotification" + } + }, + "400": { + "description": "Invalid `client` or `id`" + }, + "404": { + "description": "Some notifications not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Fetch a notification by ID" + } + }, + "/onboarding/v3": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": "DEPRECATED: the feature has been turned off, the end-point does nothing and always returns '{\"results\":[],\"auto-connects\":[]}'.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Body" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/DeprecatedMatchingResult" + } + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "Upload contacts and invoke matching." + } + }, + "/password-reset": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewPasswordReset" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Password reset code created and sent by email." + }, + "400": { + "description": "Invalid `body`\n\nInvalid email or mobile number for password reset. (label: `invalid-key`)" + }, + "409": { + "description": "A password reset is already in progress. (label: `code-exists`)", + "schema": { + "example": { + "code": 409, + "label": "code-exists", + "message": "A password reset is already in progress." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Initiate a password reset." + } + }, + "/password-reset/complete": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CompletePasswordReset" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Password reset successful." + }, + "400": { + "description": "Invalid `body`\n\nInvalid password reset code. (label: `invalid-code`)" + } + }, + "summary": "Complete a password reset." + } + }, + "/password-reset/{key}": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": "DEPRECATED: Use 'POST /password-reset/complete'.", + "parameters": [ + { + "description": "An opaque key for a pending password reset.", + "in": "path", + "name": "key", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PasswordReset" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Password reset successful." + }, + "400": { + "description": "Invalid `body` or `key`\n\nInvalid password reset code. (label: `invalid-code`)\n\nInvalid email or mobile number for password reset. (label: `invalid-key`)" + }, + "409": { + "description": "For password reset, new and old password must be different. (label: `password-must-differ`)\n\nA password reset is already in progress. (label: `code-exists`)", + "schema": { + "example": { + "code": 409, + "label": "password-must-differ", + "message": "For password reset, new and old password must be different." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-must-differ", + "code-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Complete a password reset." + } + }, + "/properties": { + "delete": { + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Properties cleared" + } + }, + "summary": "Clear all properties" + }, + "get": { + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "List of property keys", + "schema": { + "items": { + "$ref": "#/definitions/ASCII" + }, + "type": "array" + } + } + }, + "summary": "List all property keys" + } + }, + "/properties-values": { + "get": { + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/PropertyKeysAndValues" + } + } + }, + "summary": "List all properties with key and value" + } + }, + "/properties/{key}": { + "delete": { + "parameters": [ + { + "format": "printable", + "in": "path", + "name": "key", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Property deleted" + }, + "400": { + "description": "Invalid `key`" + } + }, + "summary": "Delete a property" + }, + "get": { + "parameters": [ + { + "format": "printable", + "in": "path", + "name": "key", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "The property value", + "schema": { + "$ref": "#/definitions/PropertyValue" + } + }, + "400": { + "description": "Invalid `key`" + }, + "404": { + "description": "Property not found(**Note**: This error has an empty body for legacy reasons)" + } + }, + "summary": "Get a property value" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "printable", + "in": "path", + "name": "key", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PropertyValue" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Property set" + }, + "400": { + "description": "Invalid `body` or `key`" + } + }, + "summary": "Set a user property" + } + }, + "/provider/assets": { + "post": { + "consumes": [ + "multipart/mixed" + ], + "parameters": [ + { + "description": "A body with content type `multipart/mixed body`. The first section's content type should be `application/json`. The second section's content type should be always be `application/octet-stream`. Other content types will be ignored by the server.", + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/AssetSource" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Asset posted", + "headers": { + "Location": { + "description": "Asset location", + "format": "url", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Asset" + } + }, + "400": { + "description": "Invalid `body`\n\nInvalid content length (label: `invalid-length`)" + }, + "413": { + "description": "Asset too large (label: `client-error`)", + "schema": { + "example": { + "code": 413, + "label": "client-error", + "message": "Asset too large" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Upload an asset" + } + }, + "/provider/assets/{key}": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Asset deleted" + }, + "400": { + "description": "Invalid `key`" + }, + "403": { + "description": "Unauthorised operation (label: `unauthorised`)", + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Asset not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Delete an asset" + }, + "get": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "type": "string" + }, + { + "in": "header", + "name": "Asset-Token", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "asset_token", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "302": { + "description": "Asset found", + "headers": { + "Location": { + "description": "Asset location", + "format": "url", + "type": "string" + } + } + }, + "400": { + "description": "Invalid `asset_token` or `Asset-Token` or `key`" + }, + "404": { + "description": "Asset not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Download an asset" + } + }, + "/proxy/giphy/v1/gifs": {}, + "/proxy/googlemaps/api/staticmap": {}, + "/proxy/googlemaps/maps/api/geocode": {}, + "/proxy/youtube/v3": {}, + "/push/tokens": { + "get": { + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/PushTokenList" + } + } + }, + "summary": "List the user's registered push tokens" + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PushToken" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Push token registered", + "headers": { + "Location": { + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/PushToken" + } + }, + "400": { + "description": "Invalid `body`" + }, + "404": { + "description": "App does not exist (label: `app-not-found`)\n\nInvalid push token (label: `invalid-token`)", + "schema": { + "example": { + "code": 404, + "label": "app-not-found", + "message": "App does not exist" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "app-not-found", + "invalid-token" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "413": { + "description": "Too many concurrent calls to SNS; is SNS down? (label: `sns-thread-budget-reached`)\n\nPush token length must be < 8192 for GCM or 400 for APNS (label: `token-too-long`)\n\nTried to add token to endpoint resulting in metadata length > 2048 (label: `metadata-too-long`)", + "schema": { + "example": { + "code": 413, + "label": "sns-thread-budget-reached", + "message": "Too many concurrent calls to SNS; is SNS down?" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "sns-thread-budget-reached", + "token-too-long", + "metadata-too-long" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Register a native push token" + } + }, + "/push/tokens/{pid}": { + "delete": { + "parameters": [ + { + "description": "The push token to delete", + "in": "path", + "name": "pid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "204": { + "description": "Push token unregistered" + }, + "400": { + "description": "Invalid `pid`" + }, + "404": { + "description": "Push token not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Push token not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Unregister a native push token" + } + }, + "/register": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": "If the environment where the registration takes place is private and a registered email address or phone number is not whitelisted, a 403 error is returned.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewUser" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "User created and pending activation", + "headers": { + "Location": { + "description": "UserId", + "format": "uuid", + "type": "string" + }, + "Set-Cookie": { + "description": "Cookie", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/User" + } + }, + "400": { + "description": "Invalid invitation code. (label: `invalid-invitation-code`)\n\nInvalid e-mail address. (label: `invalid-email`)\n\nInvalid mobile phone number (label: `invalid-phone`) or `body`", + "schema": { + "example": { + "code": 400, + "label": "invalid-invitation-code", + "message": "Invalid invitation code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-invitation-code", + "invalid-email", + "invalid-phone" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "403": { + "description": "Unauthorized e-mail address or phone number. (label: `unauthorized`)\n\nUsing an invitation code requires registering the given email and/or phone. (label: `missing-identity`)\n\nThe given phone number has been blacklisted due to suspected abuse or a complaint (label: `blacklisted-phone`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)\n\nToo many members in this team. (label: `too-many-team-members`)\n\nThis instance does not allow creation of personal users or teams. (label: `user-creation-restricted`)", + "schema": { + "example": { + "code": 403, + "label": "unauthorized", + "message": "Unauthorized e-mail address or phone number." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorized", + "missing-identity", + "blacklisted-phone", + "blacklisted-email", + "too-many-team-members", + "user-creation-restricted" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "User does not exist (label: `invalid-code`)\n\nInvalid activation code (label: `invalid-code`)", + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "User does not exist" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The given e-mail address or phone number is in use. (label: `key-exists`)", + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address or phone number is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Register a new user.", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "on-user-deleted-connections" + ] + ] + } + }, + "/scim/auth-tokens": { + "delete": { + "parameters": [ + { + "format": "uuid", + "in": "query", + "name": "id", + "required": true, + "type": "string" + } + ], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "Invalid `id`" + }, + "403": { + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nPassword authentication failed. (label: `password-authentication-failed`)", + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "password-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + } + }, + "get": { + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ScimTokenList" + } + }, + "403": { + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nPassword authentication failed. (label: `password-authentication-failed`)", + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "password-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + } + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateScimToken" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/CreateScimTokenResponse" + } + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nPassword authentication failed. (label: `password-authentication-failed`)", + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "password-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + } + } + }, + "/search/contacts": { + "get": { + "parameters": [ + { + "description": "Search query", + "in": "query", + "name": "q", + "required": true, + "type": "string" + }, + { + "description": "Searched domain. Note: This is optional only for backwards compatibility, future versions will mandate this.", + "in": "query", + "name": "domain", + "required": false, + "type": "string" + }, + { + "description": "Number of results to return (min: 1, max: 500, default 15)", + "format": "int32", + "in": "query", + "maximum": 500, + "minimum": 1, + "name": "size", + "required": false, + "type": "integer" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SearchResult" + } + }, + "400": { + "description": "Invalid `size` or `domain` or `q`" + } + }, + "summary": "Search for users", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "get-users-by-ids" + ], + [ + "brig", + "search-users" + ] + ] + } + }, + "/self": { + "delete": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": "if the account has a verified identity, a verification code is sent and needs to be confirmed to authorise the deletion. if the account has no verified identity but a password, it must be provided. if password is correct, or if neither a verified identity nor a password exists, account deletion is scheduled immediately.", + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DeleteUser" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Deletion is initiated." + }, + "202": { + "description": "Deletion is pending verification with a code.", + "schema": { + "$ref": "#/definitions/DeletionCodeTimeout" + } + }, + "400": { + "description": "Invalid `body`\n\nInvalid user (label: `invalid-user`)" + }, + "403": { + "description": "Team owners are not allowed to delete themselves; ask a fellow owner (label: `no-self-delete-for-team-owner`)\n\nA verification code for account deletion is still pending (label: `pending-delete`)\n\nRe-authentication via password required (label: `missing-auth`)\n\nAuthentication failed (label: `invalid-credentials`)\n\nInvalid verification code (label: `invalid-code`)", + "schema": { + "example": { + "code": 403, + "label": "no-self-delete-for-team-owner", + "message": "Team owners are not allowed to delete themselves; ask a fellow owner" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-self-delete-for-team-owner", + "pending-delete", + "missing-auth", + "invalid-credentials", + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Initiate account deletion.", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "on-user-deleted-connections" + ] + ] + }, + "get": { + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/User" + } + } + }, + "summary": "Get your own profile" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UserUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "User updated" + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Updating name is not allowed, because it is managed by SCIM (label: `managed-by-scim`)", + "schema": { + "example": { + "code": 403, + "label": "managed-by-scim", + "message": "Updating name is not allowed, because it is managed by SCIM" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "managed-by-scim" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "User not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "User not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update your profile.", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "on-user-deleted-connections" + ] + ] + } + }, + "/self/email": { + "delete": { + "description": "Your email address can only be removed if you also have a phone number.", + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Identity Removed" + }, + "403": { + "description": "The last user identity (email or phone number) cannot be removed. (label: `last-identity`)\n\nThe user has no password. (label: `no-password`)\n\nThe user has no verified identity (email or phone number) (label: `no-identity`)", + "schema": { + "example": { + "code": 403, + "label": "last-identity", + "message": "The last user identity (email or phone number) cannot be removed." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "last-identity", + "no-password", + "no-identity" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Remove your email address.", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "on-user-deleted-connections" + ] + ] + } + }, + "/self/handle": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/HandleUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Handle Changed" + }, + "400": { + "description": "The given handle is invalid (label: `invalid-handle`) or `body`", + "schema": { + "example": { + "code": 400, + "label": "invalid-handle", + "message": "The given handle is invalid" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-handle" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "403": { + "description": "The user has no verified identity (email or phone number) (label: `no-identity`)\n\nUpdating handle is not allowed, because it is managed by SCIM (label: `managed-by-scim`)", + "schema": { + "example": { + "code": 403, + "label": "no-identity", + "message": "The user has no verified identity (email or phone number)" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-identity", + "managed-by-scim" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The given handle is already taken (label: `handle-exists`)", + "schema": { + "example": { + "code": 409, + "label": "handle-exists", + "message": "The given handle is already taken" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "handle-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Change your handle.", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "on-user-deleted-connections" + ] + ] + } + }, + "/self/locale": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LocaleUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Local Changed" + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "Change your locale.", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "on-user-deleted-connections" + ] + ] + } + }, + "/self/password": { + "head": { + "responses": { + "200": { + "description": "Password is set" + }, + "404": { + "description": "Password is not set" + } + }, + "summary": "Check that your password is set." + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PasswordChange" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Password Changed" + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "Authentication failed (label: `invalid-credentials`)\n\nThe user has no verified identity (email or phone number) (label: `no-identity`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "no-identity" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "For password change, new and old password must be different. (label: `password-must-differ`)", + "schema": { + "example": { + "code": 409, + "label": "password-must-differ", + "message": "For password change, new and old password must be different." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-must-differ" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Change your password." + } + }, + "/self/phone": { + "delete": { + "description": "Your phone number can only be removed if you also have an email address and a password.", + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Identity Removed" + }, + "403": { + "description": "The last user identity (email or phone number) cannot be removed. (label: `last-identity`)\n\nThe user has no password. (label: `no-password`)\n\nThe user has no verified identity (email or phone number) (label: `no-identity`)", + "schema": { + "example": { + "code": 403, + "label": "last-identity", + "message": "The last user identity (email or phone number) cannot be removed." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "last-identity", + "no-password", + "no-identity" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Remove your phone number.", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "on-user-deleted-connections" + ] + ] + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PhoneUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "202": { + "description": "Phone updated" + }, + "400": { + "description": "Invalid mobile phone number (label: `invalid-phone`) or `body`", + "schema": { + "example": { + "code": 400, + "label": "invalid-phone", + "message": "Invalid mobile phone number" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-phone" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "403": { + "description": "The given phone number has been blacklisted due to suspected abuse or a complaint (label: `blacklisted-phone`)", + "schema": { + "example": { + "code": 403, + "label": "blacklisted-phone", + "message": "The given phone number has been blacklisted due to suspected abuse or a complaint" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "blacklisted-phone" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "The given e-mail address or phone number is in use. (label: `key-exists`)", + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address or phone number is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Change your phone number." + } + }, + "/sso/finalize-login": { + "post": { + "description": "DEPRECATED! use /sso/metadata/:tid instead! Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams", + "produces": [ + "text/plain;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "type": "string" + } + } + } + } + }, + "/sso/finalize-login/{team}": { + "post": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "team", + "required": true, + "type": "string" + } + ], + "produces": [ + "text/plain;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid `team`" + } + } + } + }, + "/sso/initiate-login/{idp}": { + "get": { + "parameters": [ + { + "in": "query", + "name": "success_redirect", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "error_redirect", + "required": false, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "idp", + "required": true, + "type": "string" + } + ], + "produces": [ + "text/html" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/FormRedirect" + } + }, + "400": { + "description": "Invalid `idp` or `error_redirect` or `success_redirect`" + } + } + }, + "head": { + "parameters": [ + { + "in": "query", + "name": "success_redirect", + "required": false, + "type": "string" + }, + { + "in": "query", + "name": "error_redirect", + "required": false, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "idp", + "required": true, + "type": "string" + } + ], + "produces": [ + "text/plain;charset=utf-8" + ], + "responses": { + "200": { + "description": "" + }, + "400": { + "description": "Invalid `idp` or `error_redirect` or `success_redirect`" + } + } + } + }, + "/sso/metadata": { + "get": { + "description": "DEPRECATED! use /sso/metadata/:tid instead! Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams", + "produces": [ + "application/xml" + ], + "responses": { + "200": { + "description": "", + "schema": { + "type": "string" + } + } + } + } + }, + "/sso/metadata/{team}": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "team", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/xml" + ], + "responses": { + "200": { + "description": "", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid `team`" + } + } + } + }, + "/sso/settings": { + "get": { + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SsoSettings" + } + } + } + } + }, + "/system/settings": { + "get": { + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SystemSettings" + } + } + }, + "summary": "Returns a curated set of system configuration settings for authorized users." + } + }, + "/system/settings/unauthorized": { + "get": { + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SystemSettingsPublic" + } + } + }, + "summary": "Returns a curated set of system configuration settings." + } + }, + "/teams": { + "get": { + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/TeamList" + } + } + }, + "summary": "Get teams (deprecated); use `GET /teams/:tid`" + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NonBindingNewTeam" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Team ID as `Location` header value", + "headers": { + "Location": { + "description": "Team ID", + "format": "uuid", + "type": "string" + } + } + }, + "400": { + "description": "Invalid `body`" + }, + "403": { + "description": "User already bound to a different team (label: `binding-exists`)\n\nUsers are not connected (label: `not-connected`)", + "schema": { + "example": { + "code": 403, + "label": "binding-exists", + "message": "User already bound to a different team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "binding-exists", + "not-connected" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Create a new non binding team" + } + }, + "/teams/invitations/by-email": { + "head": { + "parameters": [ + { + "description": "Email address", + "in": "query", + "name": "email", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Pending invitation exists." + }, + "400": { + "description": "Invalid `email`" + }, + "404": { + "description": "No pending invitations exists. (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "No pending invitations exists." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "Multiple conflicting invitations to different teams exists. (label: `conflicting-invitations`)", + "schema": { + "example": { + "code": 409, + "label": "conflicting-invitations", + "message": "Multiple conflicting invitations to different teams exists." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "conflicting-invitations" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Check if there is an invitation pending given an email address." + } + }, + "/teams/invitations/info": { + "get": { + "parameters": [ + { + "description": "Invitation code", + "in": "query", + "name": "code", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Invitation info", + "schema": { + "$ref": "#/definitions/Invitation" + } + }, + "400": { + "description": "Invalid `code`\n\nInvalid invitation code. (label: `invalid-invitation-code`)" + } + }, + "summary": "Get invitation info given a code." + } + }, + "/teams/notifications": { + "get": { + "description": "This is a work-around for scalability issues with gundeck user event fan-out. It does not track all team-wide events, but only `member-join`.\nNote that `/teams/notifications` behaves differently from `/notifications`:\n- If there is a gap between the notification id requested with `since` and the available data, team queues respond with 200 and the data that could be found. They do NOT respond with status 404, but valid data in the body.\n- The notification with the id given via `since` is included in the response if it exists. You should remove this and only use it to decide whether there was a gap between your last request and this one.\n- If the notification id does *not* exist, you get the more recent events from the queue (instead of all of them). This can be done because a notification id is a UUIDv1, which is essentially a time stamp.\n- There is no corresponding `/last` end-point to get only the most recent event. That end-point was only useful to avoid having to pull the entire queue. In team queues, if you have never requested the queue before and have no prior notification id, just pull with timestamp 'now'.", + "parameters": [ + { + "description": "Notification id to start with in the response (UUIDv1)", + "format": "uuid", + "in": "query", + "name": "since", + "required": false, + "type": "string" + }, + { + "description": "Maximum number of events to return (1..10000; default: 1000)", + "format": "int32", + "in": "query", + "maximum": 10000, + "minimum": 1, + "name": "size", + "required": false, + "type": "integer" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/QueuedNotificationList" + } + }, + "400": { + "description": "Invalid `size` or `since`\n\nCould not parse notification id (must be UUIDv1). (label: `invalid-notification-id`)" + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Read recently added team members from team queue" + } + }, + "/teams/{tid}": { + "delete": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/TeamDeleteData" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "202": { + "description": "Team is scheduled for removal" + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Verification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (missing DeleteTeam) (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Verification code required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "access-denied", + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "503": { + "description": "The delete queue is full; no further delete requests can be processed at the moment (label: `queue-full`)", + "schema": { + "example": { + "code": 503, + "label": "queue-full", + "message": "The delete queue is full; no further delete requests can be processed at the moment" + }, + "properties": { + "code": { + "enum": [ + 503 + ], + "type": "integer" + }, + "label": { + "enum": [ + "queue-full" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Delete a team" + }, + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/Team" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get a team by ID" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/TeamUpdateData" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Team updated" + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Insufficient permissions (missing SetTeamData) (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions (missing SetTeamData)" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update team properties" + } + }, + "/teams/{tid}/conversations": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/TeamConversationList" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get team conversations" + } + }, + "/teams/{tid}/conversations/roles": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ConversationRolesList" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get existing roles available for the given team" + } + }, + "/teams/{tid}/conversations/{cid}": { + "delete": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "cid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Conversation deleted" + }, + "400": { + "description": "Invalid `cid` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing delete_conversation) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Remove a team conversation", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "galley", + "on-new-remote-conversation" + ] + ] + }, + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "cid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/TeamConversation" + } + }, + "400": { + "description": "Invalid `cid` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Conversation not found (label: `no-conversation`)", + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get one team conversation" + } + }, + "/teams/{tid}/features": { + "get": { + "description": "Gets feature configs for a team. User must be a member of the team and have permission to view team features.", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/AllFeatureConfigs" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Gets feature configs for a team" + } + }, + "/teams/{tid}/features/appLock": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/AppLockConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for appLock" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/AppLockConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/AppLockConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for appLock" + } + }, + "/teams/{tid}/features/classifiedDomains": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ClassifiedDomainsConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for classifiedDomains" + } + }, + "/teams/{tid}/features/conferenceCalling": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ConferenceCallingConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for conferenceCalling" + } + }, + "/teams/{tid}/features/conversationGuestLinks": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/GuestLinksConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for conversationGuestLinks" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/GuestLinksConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/GuestLinksConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for conversationGuestLinks" + } + }, + "/teams/{tid}/features/digitalSignatures": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/DigitalSignaturesConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for digitalSignatures" + } + }, + "/teams/{tid}/features/exposeInvitationURLsToTeamAdmin": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ExposeInvitationURLsToTeamAdminConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for exposeInvitationURLsToTeamAdmin" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ExposeInvitationURLsToTeamAdminConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ExposeInvitationURLsToTeamAdminConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for exposeInvitationURLsToTeamAdmin" + } + }, + "/teams/{tid}/features/fileSharing": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/FileSharingConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for fileSharing" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/FileSharingConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/FileSharingConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for fileSharing" + } + }, + "/teams/{tid}/features/legalhold": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/LegalholdConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for legalhold" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LegalholdConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/LegalholdConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "description": "legal hold cannot be disabled for whitelisted teams (label: `legalhold-disable-unimplemented`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nCannot enable legalhold on large teams (reason: for removing LH from team, we need to iterate over all members, which is only supported for teams with less than 2k members) (label: `too-large-team-for-legalhold`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "legalhold-disable-unimplemented", + "message": "legal hold cannot be disabled for whitelisted teams" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-disable-unimplemented", + "legalhold-not-enabled", + "too-large-team-for-legalhold", + "code-authentication-required", + "code-authentication-failed", + "access-denied", + "action-denied", + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "500": { + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)", + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for legalhold", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "galley", + "on-new-remote-conversation" + ] + ] + } + }, + "/teams/{tid}/features/mls": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/MLSConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for mls" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/MLSConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/MLSConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for mls" + } + }, + "/teams/{tid}/features/mlsE2EId": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/MlsE2EIdConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for mlsE2EId" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/MlsE2EIdConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/MlsE2EIdConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for mlsE2EId" + } + }, + "/teams/{tid}/features/outlookCalIntegration": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/OutlookCalIntegrationConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for outlookCalIntegration" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/OutlookCalIntegrationConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/OutlookCalIntegrationConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for outlookCalIntegration" + } + }, + "/teams/{tid}/features/searchVisibility": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SearchVisibilityAvailableConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for searchVisibility" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SearchVisibilityAvailableConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SearchVisibilityAvailableConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for searchVisibility" + } + }, + "/teams/{tid}/features/searchVisibilityInbound": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SearchVisibilityInboundConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for searchVisibilityInbound" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SearchVisibilityInboundConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SearchVisibilityInboundConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for searchVisibilityInbound" + } + }, + "/teams/{tid}/features/selfDeletingMessages": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SelfDeletingMessagesConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for selfDeletingMessages" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SelfDeletingMessagesConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SelfDeletingMessagesConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for selfDeletingMessages" + } + }, + "/teams/{tid}/features/sndFactorPasswordChallenge": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SndFactorPasswordChallengeConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for sndFactorPasswordChallenge" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SndFactorPasswordChallengeConfig.WithStatusNoLock" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SndFactorPasswordChallengeConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Put config for sndFactorPasswordChallenge" + } + }, + "/teams/{tid}/features/sso": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/SSOConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for sso" + } + }, + "/teams/{tid}/features/validateSAMLemails": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ValidateSAMLEmailsConfig.WithStatus" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get config for validateSAMLemails" + } + }, + "/teams/{tid}/get-members-by-ids-using-post": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": "The `has_more` field in the response body is always `false`.", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "description": "Maximum results to be returned", + "format": "int32", + "in": "query", + "maximum": 2000, + "minimum": 1, + "name": "maxResults", + "required": false, + "type": "integer" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UserIdList" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/TeamMemberList" + } + }, + "400": { + "description": "Invalid `body` or `maxResults` or `tid`\n\nCan only process 2000 user ids per request. (label: `too-many-uids`)" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get team members by user id list" + } + }, + "/teams/{tid}/invitations": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "description": "Invitation id to start from (ascending).", + "format": "uuid", + "in": "query", + "name": "start", + "required": false, + "type": "string" + }, + { + "description": "Number of results to return (default 100, max 500).", + "format": "int32", + "in": "query", + "maximum": 500, + "minimum": 1, + "name": "size", + "required": false, + "type": "integer" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "List of sent invitations", + "schema": { + "$ref": "#/definitions/InvitationList" + } + }, + "400": { + "description": "Invalid `size` or `start` or `tid`" + }, + "403": { + "description": "Insufficient team permissions (label: `insufficient-permissions`)", + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "List the sent team invitations" + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": "Invitations are sent by email. The maximum allowed number of pending team invitations is equal to the team size.", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/InvitationRequest" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Invitation was created and sent.", + "headers": { + "Location": { + "format": "url", + "type": "string" + } + }, + "schema": { + "$ref": "#/definitions/Invitation" + } + }, + "400": { + "description": "Invalid `body` or `tid`\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "403": { + "description": "Insufficient team permissions (label: `insufficient-permissions`)\n\nToo many team invitations for this team (label: `too-many-team-invitations`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)\n\nThe user has no verified identity (email or phone number) (label: `no-identity`)\n\nThis operation requires the user to have a verified email address. (label: `no-email`)", + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions", + "too-many-team-invitations", + "blacklisted-email", + "no-identity", + "no-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Create and send a new team invitation." + } + }, + "/teams/{tid}/invitations/{iid}": { + "delete": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "iid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Invitation deleted" + }, + "400": { + "description": "Invalid `iid` or `tid`" + }, + "403": { + "description": "Insufficient team permissions (label: `insufficient-permissions`)", + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Delete a pending team invitation by ID." + }, + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "iid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Invitation", + "schema": { + "$ref": "#/definitions/Invitation" + } + }, + "400": { + "description": "Invalid `iid` or `tid`" + }, + "403": { + "description": "Insufficient team permissions (label: `insufficient-permissions`)", + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Notification not found. (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Notification not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get a pending team invitation by ID." + } + }, + "/teams/{tid}/legalhold/consent": { + "post": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Grant consent successful" + }, + "204": { + "description": "Consent already granted" + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team member not found (label: `no-team-member`)", + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "500": { + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)", + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Consent to legal hold", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "galley", + "on-new-remote-conversation" + ] + ] + } + }, + "/teams/{tid}/legalhold/settings": { + "delete": { + "consumes": [ + "application/json;charset=utf-8" + ], + "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)", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RemoveLegalHoldSettingsRequest" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "204": { + "description": "Legal hold service settings deleted" + }, + "400": { + "description": "Invalid `body` or `tid`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "description": "legal hold cannot be disabled for whitelisted teams (label: `legalhold-disable-unimplemented`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "legalhold-disable-unimplemented", + "message": "legal hold cannot be disabled for whitelisted teams" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-disable-unimplemented", + "legalhold-not-enabled", + "invalid-op", + "action-denied", + "no-team-member", + "operation-denied", + "code-authentication-required", + "code-authentication-failed", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "500": { + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)", + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Delete legal hold service settings", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "galley", + "on-new-remote-conversation" + ] + ] + }, + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ViewLegalHoldService" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get legal hold service settings" + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewLegalHoldService" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Legal hold service settings created", + "schema": { + "$ref": "#/definitions/ViewLegalHoldService" + } + }, + "400": { + "description": "Invalid `body` or `tid`\n\nlegal hold service: invalid response (label: `legalhold-status-bad`)\n\nlegal hold service pubkey is invalid (label: `legalhold-invalid-key`)" + }, + "403": { + "description": "legal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "legalhold-not-enabled", + "message": "legal hold is not enabled for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-enabled", + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Create legal hold service settings" + } + }, + "/teams/{tid}/legalhold/{uid}": { + "delete": { + "consumes": [ + "application/json;charset=utf-8" + ], + "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)", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DisableLegalHoldForUserRequest" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Disable legal hold successful" + }, + "204": { + "description": "Legal hold was not enabled" + }, + "400": { + "description": "Invalid `body` or `uid` or `tid`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member", + "action-denied", + "code-authentication-required", + "code-authentication-failed", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "500": { + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)", + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Disable legal hold for user", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "galley", + "on-new-remote-conversation" + ] + ] + }, + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/UserLegalHoldStatusResponse" + } + }, + "400": { + "description": "Invalid `uid` or `tid`" + }, + "404": { + "description": "Team member not found (label: `no-team-member`)", + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get legal hold status" + }, + "post": { + "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)", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "201": { + "description": "Request device successful" + }, + "204": { + "description": "Request device already pending" + }, + "400": { + "description": "Invalid `uid` or `tid`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)\n\nlegal hold service: invalid response (label: `legalhold-status-bad`)" + }, + "403": { + "description": "legal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)", + "schema": { + "example": { + "code": 403, + "label": "legalhold-not-enabled", + "message": "legal hold is not enabled for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-enabled", + "operation-denied", + "no-team-member", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team member not found (label: `no-team-member`)", + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "user has not given consent to using legal hold (label: `legalhold-no-consent`)\n\nlegal hold is already enabled for this user (label: `legalhold-already-enabled`)", + "schema": { + "example": { + "code": 409, + "label": "legalhold-no-consent", + "message": "user has not given consent to using legal hold" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-no-consent", + "legalhold-already-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "500": { + "description": "internal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)\n\nlegal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)", + "schema": { + "example": { + "code": 500, + "label": "legalhold-illegal-op", + "message": "internal server error: inconsistent change of user's legalhold state" + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-illegal-op", + "legalhold-internal" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Request legal hold device", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "galley", + "on-new-remote-conversation" + ] + ] + } + }, + "/teams/{tid}/legalhold/{uid}/approve": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "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)", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ApproveLegalHoldForUserRequest" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Legal hold approved" + }, + "400": { + "description": "Invalid `body` or `uid` or `tid`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "description": "legal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nYou do not have permission to access this resource (label: `access-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "legalhold-not-enabled", + "message": "legal hold is not enabled for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-enabled", + "no-team-member", + "action-denied", + "access-denied", + "code-authentication-required", + "code-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "no legal hold device is registered for this user. POST /teams/:tid/legalhold/:uid/ to start the flow. (label: `legalhold-no-device-allocated`)", + "schema": { + "example": { + "code": 404, + "label": "legalhold-no-device-allocated", + "message": "no legal hold device is registered for this user. POST /teams/:tid/legalhold/:uid/ to start the flow." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-no-device-allocated" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "409": { + "description": "legal hold is already enabled for this user (label: `legalhold-already-enabled`)", + "schema": { + "example": { + "code": 409, + "label": "legalhold-already-enabled", + "message": "legal hold is already enabled for this user" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-already-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "412": { + "description": "legal hold cannot be approved without being in a pending state (label: `legalhold-not-pending`)", + "schema": { + "example": { + "code": 412, + "label": "legalhold-not-pending", + "message": "legal hold cannot be approved without being in a pending state" + }, + "properties": { + "code": { + "enum": [ + 412 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-pending" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "500": { + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)", + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Approve legal hold device", + "x-wire-makes-federated-call-to": [ + [ + "galley", + "on-conversation-updated" + ], + [ + "galley", + "on-mls-message-sent" + ], + [ + "galley", + "on-new-remote-conversation" + ] + ] + } + }, + "/teams/{tid}/members": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "description": "Maximum results to be returned", + "format": "int32", + "in": "query", + "maximum": 2000, + "minimum": 1, + "name": "maxResults", + "required": false, + "type": "integer" + }, + { + "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.", + "in": "query", + "name": "pagingState", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/TeamMembersPage" + } + }, + "400": { + "description": "Invalid `pagingState` or `maxResults` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get team members" + }, + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewTeamMember" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "" + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "cannot add more members to team when legalhold service is enabled. (label: `too-many-members-for-legalhold`)\n\nUser already bound to a different team (label: `binding-exists`)\n\nMaximum number of members per team reached (label: `too-many-team-members`)\n\nInsufficient permissions (label: `operation-denied`)\n\nUsers are not connected (label: `not-connected`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nCannot add users to binding teams, invite only (label: `binding-team`)\n\nThe specified permissions are invalid (label: `invalid-permissions`)", + "schema": { + "example": { + "code": 403, + "label": "too-many-members-for-legalhold", + "message": "cannot add more members to team when legalhold service is enabled." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-members-for-legalhold", + "binding-exists", + "too-many-team-members", + "operation-denied", + "not-connected", + "no-team-member", + "binding-team", + "invalid-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Add a new team member" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewTeamMember" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "" + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nThe specified permissions are invalid (label: `invalid-permissions`)\n\nYou do not have permission to access this resource (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member", + "invalid-permissions", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team member not found (label: `no-team-member`)\n\nTeam not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Update an existing team member" + } + }, + "/teams/{tid}/members/csv": { + "get": { + "description": "The endpoint returns data in chunked transfer encoding. Internal server errors might result in a failed transfer instead of a 500 response.", + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "text/csv" + ], + "responses": { + "200": { + "description": "CSV of team members" + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "You do not have permission to access this resource (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "You do not have permission to access this resource" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get all members of the team as a CSV file" + } + }, + "/teams/{tid}/members/{uid}": { + "delete": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "" + }, + "202": { + "description": "Team member scheduled for deletion" + }, + "400": { + "description": "Invalid `uid` or `tid`" + }, + "403": { + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nYou do not have permission to access this resource (label: `access-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)", + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member", + "access-denied", + "code-authentication-required", + "code-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)\n\nTeam member not found (label: `no-team-member`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Remove an existing team member" + }, + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/TeamMember" + } + }, + "400": { + "description": "Invalid `uid` or `tid`" + }, + "403": { + "description": "Requesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team member not found (label: `no-team-member`)", + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get single team member" + } + }, + "/teams/{tid}/search": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "description": "Search expression", + "in": "query", + "name": "q", + "required": false, + "type": "string" + }, + { + "collectionFormat": null, + "description": "Role filter, eg. `member,partner`. Empty list means do not filter.", + "in": "query", + "items": { + "enum": [ + "owner", + "admin", + "member", + "partner" + ], + "type": "string" + }, + "name": "frole", + "required": false, + "type": "array" + }, + { + "description": "Can be one of name, handle, email, saml_idp, managed_by, role, created_at.", + "enum": [ + "name", + "handle", + "email", + "saml_idp", + "managed_by", + "role", + "created_at" + ], + "in": "query", + "name": "sortby", + "required": false, + "type": "string" + }, + { + "description": "Can be one of asc, desc.", + "enum": [ + "asc", + "desc" + ], + "in": "query", + "name": "sortorder", + "required": false, + "type": "string" + }, + { + "description": "Number of results to return (min: 1, max: 500, default: 15)", + "format": "int32", + "in": "query", + "maximum": 500, + "minimum": 1, + "name": "size", + "required": false, + "type": "integer" + }, + { + "description": "Optional, when not specified, the first page will be returned. Every returned page contains a `paging_state`, this should be supplied to retrieve the next page.", + "in": "query", + "name": "pagingState", + "required": false, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Search results", + "schema": { + "$ref": "#/definitions/SearchResult" + } + }, + "400": { + "description": "Invalid `pagingState` or `size` or `sortorder` or `sortby` or `frole` or `q` or `tid`" + } + }, + "summary": "Browse team for members (requires add-user permission)" + } + }, + "/teams/{tid}/search-visibility": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/TeamSearchVisibilityView" + } + }, + "400": { + "description": "Invalid `tid`" + }, + "403": { + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Shows the value for search visibility" + }, + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/TeamSearchVisibilityView" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "204": { + "description": "Search visibility set" + }, + "400": { + "description": "Invalid `body` or `tid`" + }, + "403": { + "description": "Custom search is not available for this team (label: `team-search-visibility-not-enabled`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)", + "schema": { + "example": { + "code": 403, + "label": "team-search-visibility-not-enabled", + "message": "Custom search is not available for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "team-search-visibility-not-enabled", + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "404": { + "description": "Team not found (label: `no-team`)", + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Sets the search visibility for the whole team" + } + }, + "/teams/{tid}/size": { + "get": { + "parameters": [ + { + "format": "uuid", + "in": "path", + "name": "tid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Number of team members", + "schema": { + "$ref": "#/definitions/TeamSize" + } + }, + "400": { + "description": "Invalid `tid`\n\nInvalid invitation code. (label: `invalid-invitation-code`)" + } + }, + "summary": "Returns the number of team members as an integer. Can be out of sync by roughly the `refresh_interval` of the ES index." + } + }, + "/users/handles": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CheckHandles" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "List of free handles", + "schema": { + "items": { + "$ref": "#/definitions/Handle" + }, + "type": "array" + } + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "Check availability of user handles" + } + }, + "/users/handles/{handle}": { + "head": { + "parameters": [ + { + "in": "path", + "name": "handle", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Handle is taken", + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "400": { + "description": "Invalid `handle`\n\nThe given handle is invalid (label: `invalid-handle`)" + }, + "404": { + "description": "Handle not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Handle not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Check whether a user handle can be taken" + } + }, + "/users/list-clients": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LimitedQualifiedUserIdList" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "properties": { + "qualified_user_map": { + "$ref": "#/definitions/QualifiedUserMap_Set_PubClient" + } + }, + "type": "object" + } + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "List all clients for a set of user ids", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "get-user-clients" + ] + ] + } + }, + "/users/list-prekeys": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/QualifiedUserClients" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/QualifiedUserClientPrekeyMap" + } + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "Given a map of domain to (map of user IDs to client IDs) return a prekey for each one. You can't request information for more users than maximum conversation size.", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "claim-multi-prekey-bundle" + ] + ] + } + }, + "/users/{uid_domain}/{uid}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "User found", + "schema": { + "$ref": "#/definitions/UserProfile" + } + }, + "400": { + "description": "Invalid `uid` or `uid_domain`" + }, + "404": { + "description": "User not found (label: `not-found`)", + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "User not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get a user by Domain and UserId", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "get-users-by-ids" + ] + ] + } + }, + "/users/{uid_domain}/{uid}/clients": { + "get": { + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "items": { + "$ref": "#/definitions/PubClient" + }, + "type": "array" + } + }, + "400": { + "description": "Invalid `uid` or `uid_domain`" + } + }, + "summary": "Get all of a user's clients", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "get-user-clients" + ] + ] + } + }, + "/users/{uid_domain}/{uid}/clients/{client}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + }, + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/PubClient" + } + }, + "400": { + "description": "Invalid `client` or `uid` or `uid_domain`" + } + }, + "summary": "Get a specific client of a user", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "get-user-clients" + ] + ] + } + }, + "/users/{uid_domain}/{uid}/prekeys": { + "get": { + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/PrekeyBundle" + } + }, + "400": { + "description": "Invalid `uid` or `uid_domain`" + } + }, + "summary": "Get a prekey for each client of a user.", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "claim-prekey-bundle" + ] + ] + } + }, + "/users/{uid_domain}/{uid}/prekeys/{client}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "type": "string" + }, + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + }, + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/ClientPrekey" + } + }, + "400": { + "description": "Invalid `client` or `uid` or `uid_domain`" + } + }, + "summary": "Get a prekey for a specific client of a user.", + "x-wire-makes-federated-call-to": [ + [ + "brig", + "claim-prekey" + ] + ] + } + }, + "/users/{uid}/email": { + "put": { + "consumes": [ + "application/json;charset=utf-8" + ], + "description": "If the user has a pending email validation, the validation email will be resent.", + "parameters": [ + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/EmailUpdate" + } + } + ], + "produces": [ + "application/json;charset=utf-8" + ], + "responses": { + "200": { + "description": "", + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "400": { + "description": "Invalid `body` or `uid`" + } + }, + "summary": "Resend email address validation email." + } + }, + "/users/{uid}/rich-info": { + "get": { + "parameters": [ + { + "description": "User Id", + "format": "uuid", + "in": "path", + "name": "uid", + "required": true, + "type": "string" + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Rich info about the user", + "schema": { + "$ref": "#/definitions/RichInfoAssocList" + } + }, + "400": { + "description": "Invalid `uid`" + }, + "403": { + "description": "Insufficient team permissions (label: `insufficient-permissions`)", + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "summary": "Get a user's rich info" + } + }, + "/verification-code/send": { + "post": { + "consumes": [ + "application/json;charset=utf-8" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SendVerificationCode" + } + } + ], + "produces": [ + "application/json;charset=utf-8", + "application/json" + ], + "responses": { + "200": { + "description": "Verification code sent." + }, + "400": { + "description": "Invalid `body`" + } + }, + "summary": "Send a verification code to a given email address." + } + } + }, + "security": [ + { + "ZAuth": [] + } + ], + "securityDefinitions": { + "ZAuth": { + "description": "Must be a token retrieved by calling 'POST /login' or 'POST /access'. It must be presented in this format: 'Bearer \\'.", + "in": "header", + "name": "Authorization", + "type": "apiKey" + } + }, + "swagger": "2.0" +} diff --git a/services/brig/docs/swagger.md b/services/brig/docs/swagger.md index db6210b536..28f214eae9 100644 --- a/services/brig/docs/swagger.md +++ b/services/brig/docs/swagger.md @@ -1,10 +1,8 @@ ## General -**NOTE**: only a few endpoints are visible here at the moment, more will come as we migrate them to Swagger 2.0. In the meantime please also look at the old swagger docs link for the not-yet-migrated endpoints. See https://docs.wire.com/understand/api-client-perspective/swagger.html for the old endpoints. +### SSO Endpoints -## SSO Endpoints - -### Overview +#### Overview `/sso/metadata` will be requested by the IdPs to learn how to talk to wire. @@ -13,11 +11,11 @@ `/identity-providers` end-points are for use in the team settings page when IdPs are registered. They talk json. -### Configuring IdPs +#### Configuring IdPs IdPs usually allow you to copy the metadata into your clipboard. That should contain all the details you need to post the idp in your team under `/identity-providers`. (Team id is derived from the authorization credentials of the request.) -#### okta.com +##### okta.com Okta will ask you to provide two URLs when you set it up for talking to wireapp: @@ -25,7 +23,7 @@ Okta will ask you to provide two URLs when you set it up for talking to wireapp: 2. The `Audience URI`. You can find this in the metadata returned by the `/sso/metadata` end-point. It is the contents of the `md:OrganizationURL` element. -#### centrify.com +##### centrify.com Centrify allows you to upload the metadata xml document that you get from the `/sso/metadata` end-point. You can also enter the metadata url and have centrify retrieve the xml, but to guarantee integrity of the setup, the metadata should be copied from the team settings page and pasted into the centrify setup page without any URL indirections. @@ -37,6 +35,7 @@ For errors that are more likely to be transient, we suggest clients to retry wha **Note**: when a failure occurs as a result of making a federated RPC to another backend, the error response contains the following extra fields: + - `type`: "federation" (just the literal string in quotes, which can be used as an error type identifier when parsing errors) - `domain`: the target backend of the RPC that failed; - `path`: the path of the RPC that failed. diff --git a/services/brig/src/Main.hs b/services/brig/exec/Main.hs similarity index 100% rename from services/brig/src/Main.hs rename to services/brig/exec/Main.hs diff --git a/services/brig/schema/src/Main.hs b/services/brig/schema/src/Main.hs index db7ca577b4..e04a47faf6 100644 --- a/services/brig/schema/src/Main.hs +++ b/services/brig/schema/src/Main.hs @@ -22,36 +22,6 @@ import Control.Exception (finally) import Imports import qualified System.Logger.Extended as Log import Util.Options -import qualified V10 -import qualified V11 -import qualified V12 -import qualified V13 -import qualified V14 -import qualified V15 -import qualified V16 -import qualified V17 -import qualified V18 -import qualified V19 -import qualified V20 -import qualified V21 -import qualified V22 -import qualified V23 -import qualified V24 -import qualified V28 -import qualified V29 -import qualified V30 -import qualified V31 -import qualified V32 -import qualified V33 -import qualified V34 -import qualified V35 -import qualified V36 -import qualified V37 -import qualified V38 -import qualified V39 -import qualified V40 -import qualified V41 -import qualified V42 import qualified V43 import qualified V44 import qualified V45 @@ -83,7 +53,6 @@ import qualified V70_UserEmailUnvalidated import qualified V71_AddTableVCodesThrottle import qualified V72_AddNonceTable import qualified V73_ReplaceNonceTable -import qualified V9 main :: IO () main = do @@ -94,38 +63,7 @@ main = do migrateSchema l o - [ V9.migration, - V10.migration, - V11.migration, - V12.migration, - V13.migration, - V14.migration, - V15.migration, - V16.migration, - V17.migration, - V18.migration, - V19.migration, - V20.migration, - V21.migration, - V22.migration, - V23.migration, - V24.migration, - V28.migration, - V29.migration, - V30.migration, - V31.migration, - V32.migration, - V33.migration, - V34.migration, - V35.migration, - V36.migration, - V37.migration, - V38.migration, - V39.migration, - V40.migration, - V41.migration, - V42.migration, - V43.migration, + [ V43.migration, V44.migration, V45.migration, V46.migration, diff --git a/services/brig/schema/src/V10.hs b/services/brig/schema/src/V10.hs deleted file mode 100644 index ee254706c5..0000000000 --- a/services/brig/schema/src/V10.hs +++ /dev/null @@ -1,68 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V10 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = Migration 10 "Switch to leveled compaction" $ do - void $ - schema' - [r| - alter columnfamily user with compaction = { 'class' : 'LeveledCompactionStrategy' }; - |] - void $ - schema' - [r| - alter columnfamily user_keys with compaction = { 'class' : 'LeveledCompactionStrategy' }; - |] - void $ - schema' - [r| - alter columnfamily activation_keys with compaction = { 'class' : 'LeveledCompactionStrategy' }; - |] - void $ - schema' - [r| - alter columnfamily password_reset with compaction = { 'class' : 'LeveledCompactionStrategy' }; - |] - void $ - schema' - [r| - alter columnfamily push with compaction = { 'class' : 'LeveledCompactionStrategy' }; - |] - void $ - schema' - [r| - alter columnfamily connection with compaction = { 'class' : 'LeveledCompactionStrategy' }; - |] - void $ - schema' - [r| - alter columnfamily invitation with compaction = { 'class' : 'LeveledCompactionStrategy' }; - |] - void $ - schema' - [r| - alter columnfamily invitee_info with compaction = { 'class' : 'LeveledCompactionStrategy' }; - |] diff --git a/services/brig/schema/src/V11.hs b/services/brig/schema/src/V11.hs deleted file mode 100644 index 52d65e926f..0000000000 --- a/services/brig/schema/src/V11.hs +++ /dev/null @@ -1,33 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V11 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = Migration 11 "Add user.status column" $ do - void $ - schema' - [r| - alter columnfamily user add status int; - |] diff --git a/services/brig/schema/src/V12.hs b/services/brig/schema/src/V12.hs deleted file mode 100644 index c1195e9fdd..0000000000 --- a/services/brig/schema/src/V12.hs +++ /dev/null @@ -1,68 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V12 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = Migration 12 "Lower gc_grace_seconds on all CFs to 4 days" $ do - void $ - schema' - [r| - alter columnfamily user with gc_grace_seconds = 345600; - |] - void $ - schema' - [r| - alter columnfamily user_keys with gc_grace_seconds = 345600; - |] - void $ - schema' - [r| - alter columnfamily activation_keys with gc_grace_seconds = 345600; - |] - void $ - schema' - [r| - alter columnfamily password_reset with gc_grace_seconds = 345600; - |] - void $ - schema' - [r| - alter columnfamily push with gc_grace_seconds = 345600; - |] - void $ - schema' - [r| - alter columnfamily connection with gc_grace_seconds = 345600; - |] - void $ - schema' - [r| - alter columnfamily invitation with gc_grace_seconds = 345600; - |] - void $ - schema' - [r| - alter columnfamily invitee_info with gc_grace_seconds = 345600; - |] diff --git a/services/brig/schema/src/V13.hs b/services/brig/schema/src/V13.hs deleted file mode 100644 index ce17a5fda6..0000000000 --- a/services/brig/schema/src/V13.hs +++ /dev/null @@ -1,44 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V13 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = Migration 13 "Introduce reverse push CF (user_push) and remove index from push" $ do - void $ - schema' - [r| - create columnfamily if not exists user_push - ( usr uuid -- user id - , ptoken text -- token - , app text -- application - , transport int -- transport type (0 = GCM, 1 = APNS) - , primary key (usr, ptoken, app, transport) - ); - |] - void $ - schema' - [r| - drop index if exists push_usr_key; - |] diff --git a/services/brig/schema/src/V14.hs b/services/brig/schema/src/V14.hs deleted file mode 100644 index 40ada092cd..0000000000 --- a/services/brig/schema/src/V14.hs +++ /dev/null @@ -1,68 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V14 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = Migration 14 "Increase gc_grace_seconds back to 10 days" $ do - void $ - schema' - [r| - alter columnfamily user with gc_grace_seconds = 864000; - |] - void $ - schema' - [r| - alter columnfamily user_keys with gc_grace_seconds = 864000; - |] - void $ - schema' - [r| - alter columnfamily activation_keys with gc_grace_seconds = 864000; - |] - void $ - schema' - [r| - alter columnfamily password_reset with gc_grace_seconds = 864000; - |] - void $ - schema' - [r| - alter columnfamily push with gc_grace_seconds = 864000; - |] - void $ - schema' - [r| - alter columnfamily connection with gc_grace_seconds = 864000; - |] - void $ - schema' - [r| - alter columnfamily invitation with gc_grace_seconds = 864000; - |] - void $ - schema' - [r| - alter columnfamily invitee_info with gc_grace_seconds = 864000; - |] diff --git a/services/brig/schema/src/V16.hs b/services/brig/schema/src/V16.hs deleted file mode 100644 index 2601277699..0000000000 --- a/services/brig/schema/src/V16.hs +++ /dev/null @@ -1,33 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V16 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = Migration 16 "Drop push column family" $ do - void $ - schema' - [r| - drop columnfamily if exists push; - |] diff --git a/services/brig/schema/src/V17.hs b/services/brig/schema/src/V17.hs deleted file mode 100644 index 07b42e7f75..0000000000 --- a/services/brig/schema/src/V17.hs +++ /dev/null @@ -1,34 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V17 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = Migration 17 "Drop user_push column family" $ do - -- 'user_push' has been moved to gundeck - void $ - schema' - [r| - drop columnfamily if exists user_push; - |] diff --git a/services/brig/schema/src/V18.hs b/services/brig/schema/src/V18.hs deleted file mode 100644 index f82bb74cf1..0000000000 --- a/services/brig/schema/src/V18.hs +++ /dev/null @@ -1,51 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V18 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = Migration 18 "Add prekeys" $ do - void $ - schema' - [r| - create columnfamily if not exists clients - ( user uuid - , client text - , tstamp timestamp - , type int - , label text - , primary key (user, client) - ); - |] - void $ - schema' - [r| - create columnfamily if not exists prekeys - ( user uuid - , client text - , key int - , data text - , primary key (user, client, key) - ); - |] diff --git a/services/brig/schema/src/V19.hs b/services/brig/schema/src/V19.hs deleted file mode 100644 index 47aec633e4..0000000000 --- a/services/brig/schema/src/V19.hs +++ /dev/null @@ -1,38 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V19 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = Migration 19 "Add properties" $ do - void $ - schema' - [r| - create columnfamily if not exists properties - ( user uuid - , key ascii - , value blob - , primary key (user, key) - ); - |] diff --git a/services/brig/schema/src/V20.hs b/services/brig/schema/src/V20.hs deleted file mode 100644 index 1ab8756f69..0000000000 --- a/services/brig/schema/src/V20.hs +++ /dev/null @@ -1,33 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V20 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = Migration 20 "Add activation_keys.challenge" $ do - void $ - schema' - [r| - alter columnfamily activation_keys add challenge ascii; - |] diff --git a/services/brig/schema/src/V21.hs b/services/brig/schema/src/V21.hs deleted file mode 100644 index 0e3a9becf1..0000000000 --- a/services/brig/schema/src/V21.hs +++ /dev/null @@ -1,33 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V21 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = Migration 21 "Remove password_reset.email" $ do - void $ - schema' - [r| - alter columnfamily password_reset drop email; - |] diff --git a/services/brig/schema/src/V22.hs b/services/brig/schema/src/V22.hs deleted file mode 100644 index c9fc9a8cac..0000000000 --- a/services/brig/schema/src/V22.hs +++ /dev/null @@ -1,39 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V22 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = Migration 22 "Add login_codes" $ do - void $ - schema' - [r| - create columnfamily if not exists login_codes - ( user uuid - , code text - , retries int - , timeout timestamp - , primary key (user) - ); - |] diff --git a/services/brig/schema/src/V23.hs b/services/brig/schema/src/V23.hs deleted file mode 100644 index 2214512098..0000000000 --- a/services/brig/schema/src/V23.hs +++ /dev/null @@ -1,38 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V23 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = Migration 23 "Add password_reset.retries and timeout" $ do - void $ - schema' - [r| - alter columnfamily password_reset add retries int; - |] - void $ - schema' - [r| - alter columnfamily password_reset add timeout timestamp; - |] diff --git a/services/brig/schema/src/V24.hs b/services/brig/schema/src/V24.hs deleted file mode 100644 index a080397369..0000000000 --- a/services/brig/schema/src/V24.hs +++ /dev/null @@ -1,38 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V24 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = Migration 24 "Add user.language and user.country" $ do - void $ - schema' - [r| - alter columnfamily user add language ascii; - |] - void $ - schema' - [r| - alter columnfamily user add country ascii; - |] diff --git a/services/brig/schema/src/V25.hs b/services/brig/schema/src/V25.hs deleted file mode 100644 index bbafe532b9..0000000000 --- a/services/brig/schema/src/V25.hs +++ /dev/null @@ -1,30 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V25 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = Migration 25 "Change client IDs from ascii to text" $ do - schema' [r| alter columnfamily clients alter client type text; |] - schema' [r| alter columnfamily prekeys alter client type text; |] diff --git a/services/brig/schema/src/V29.hs b/services/brig/schema/src/V29.hs deleted file mode 100644 index 3a371af08a..0000000000 --- a/services/brig/schema/src/V29.hs +++ /dev/null @@ -1,56 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V29 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = Migration 29 "Create new invitations tables" $ do - void $ - schema' - [r| - drop columnfamily if exists invitation |] - void $ - schema' - [r| - create columnfamily if not exists invitation - ( inviter uuid -- user id that created the invitation - , id uuid -- invitation id reference (relevant for inviter) - , code ascii -- code of the invitation (known only by invitee) - , email text -- email of the user invited - , phone text -- phone of the user invited - , created_at timestamp -- time this invitation was created - , name text -- name of the invitee - , primary key (inviter, id) - ); - |] - void $ - schema' - [r| - create columnfamily if not exists invitation_info - ( code ascii -- code of the invitation (known only by invitee) - , inviter uuid -- user id that created the invitation - , id uuid -- invitation id reference (relevant for inviter) - , primary key (code) - ); - |] diff --git a/services/brig/schema/src/V30.hs b/services/brig/schema/src/V30.hs deleted file mode 100644 index f092709361..0000000000 --- a/services/brig/schema/src/V30.hs +++ /dev/null @@ -1,31 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V30 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = Migration 30 "Add even more client properties" $ do - schema' [r| alter columnfamily clients add ip inet; |] - schema' [r| alter columnfamily clients add lat double; |] - schema' [r| alter columnfamily clients add lon double; |] diff --git a/services/brig/schema/src/V31.hs b/services/brig/schema/src/V31.hs deleted file mode 100644 index 2ff9f63ffa..0000000000 --- a/services/brig/schema/src/V31.hs +++ /dev/null @@ -1,30 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V31 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = - Migration 31 "Add model to clients" $ - schema' [r| alter columnfamily clients add model text; |] diff --git a/services/brig/schema/src/V32.hs b/services/brig/schema/src/V32.hs deleted file mode 100644 index 83d31d9507..0000000000 --- a/services/brig/schema/src/V32.hs +++ /dev/null @@ -1,39 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V32 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = - Migration 32 "Add generic verification codes" $ - schema' - [r| - create columnfamily if not exists codes - ( user uuid - , scope int - , code text - , retries int - , primary key (user, scope) - ); - |] diff --git a/services/brig/schema/src/V34.hs b/services/brig/schema/src/V34.hs deleted file mode 100644 index 8fffe0e843..0000000000 --- a/services/brig/schema/src/V34.hs +++ /dev/null @@ -1,47 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V34 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = - Migration 34 "Add vcodes table" $ - -- Supposed to cover all existing use-cases for short-lived - -- verification codes sent either by e-mail, sms or voice call, - -- eventually superseding the 'activation_keys', 'login_codes', - -- 'password_reset' and 'codes' tables. - schema' - [r| - create table if not exists vcodes - ( key ascii -- opaque 'email' or 'phone' - , scope int - , value ascii -- secret value - , retries int -- attempts left - , email text -- email address (xor phone) - , phone text -- phone number (xor email) - , account uuid -- optional associated account ID - , primary key (key, scope) - ) with compaction = {'class': 'LeveledCompactionStrategy'} - and gc_grace_seconds = 0; - |] diff --git a/services/brig/schema/src/V35.hs b/services/brig/schema/src/V35.hs deleted file mode 100644 index 0a8a197f64..0000000000 --- a/services/brig/schema/src/V35.hs +++ /dev/null @@ -1,93 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V35 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = Migration 35 "Add service provider tables" $ do - schema' - [r| - create table if not exists provider - ( id uuid - , name text - , email text - , password blob - , url blob - , descr text - , primary key (id) - ) with compaction = {'class': 'LeveledCompactionStrategy'}; - |] - schema' - [r| - create table if not exists provider_keys - ( key text - , provider uuid - , primary key (key) - ) with compaction = {'class': 'LeveledCompactionStrategy'}; - |] - schema' - [r| - create type if not exists pubkey - ( typ int - , size int - , pem blob - ); - |] - schema' - [r| - create table if not exists service - ( provider uuid - , id uuid - , name text - , descr text - , base_url blob - , auth_tokens list - , pubkeys list> - , fingerprints list - , assets list> - , tags set - , enabled boolean - , primary key (provider, id) - ) with compaction = {'class': 'LeveledCompactionStrategy'}; - |] - schema' - [r| - create table if not exists service_tag - ( bucket int - , tag bigint - , name text - , service uuid - , provider uuid - , primary key ((bucket, tag), name, service) - ) with clustering order by (name asc, service asc) - and compaction = {'class': 'LeveledCompactionStrategy'}; - |] - schema' - [r| - alter table user add provider uuid; - |] - schema' - [r| - alter table user add service uuid; - |] diff --git a/services/brig/schema/src/V36.hs b/services/brig/schema/src/V36.hs deleted file mode 100644 index 97f71bfbbb..0000000000 --- a/services/brig/schema/src/V36.hs +++ /dev/null @@ -1,33 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V36 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = - Migration 36 "Add asset.size attribute" $ - schema' - [r| - alter type asset add size int; - |] diff --git a/services/brig/schema/src/V37.hs b/services/brig/schema/src/V37.hs deleted file mode 100644 index afc5c0032e..0000000000 --- a/services/brig/schema/src/V37.hs +++ /dev/null @@ -1,38 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V37 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = - Migration 37 "Add budget table" $ - schema' - [r| - create table if not exists budget - ( key text - , budget int - , primary key (key) - ) with compaction = {'class': 'LeveledCompactionStrategy'} - and gc_grace_seconds = 0; - |] diff --git a/services/brig/schema/src/V38.hs b/services/brig/schema/src/V38.hs deleted file mode 100644 index 6eca54ec75..0000000000 --- a/services/brig/schema/src/V38.hs +++ /dev/null @@ -1,49 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V38 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = Migration 38 "Add user handles" $ do - schema' - [r| - create table if not exists unique_claims - ( value text - , claims set - , primary key (value) - ) with compaction = {'class': 'LeveledCompactionStrategy'} - and gc_grace_seconds = 0; - |] - schema' - [r| - create table if not exists user_handle - ( handle text - , user uuid - , primary key (handle) - ) with compaction = {'class': 'LeveledCompactionStrategy'}; - |] - schema' - [r| - alter table user add handle text; - |] diff --git a/services/brig/schema/src/V39.hs b/services/brig/schema/src/V39.hs deleted file mode 100644 index 2a9ec5b7e2..0000000000 --- a/services/brig/schema/src/V39.hs +++ /dev/null @@ -1,42 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V39 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = - Migration 39 "Add user_cookies table" $ - schema' - [r| - create table if not exists user_cookies - ( user uuid - , expires timestamp - , id bigint - , label text - , type int - , created timestamp - , succ_id bigint - , primary key (user, expires, id) - ) with compaction = {'class': 'LeveledCompactionStrategy'}; - |] diff --git a/services/brig/schema/src/V40.hs b/services/brig/schema/src/V40.hs deleted file mode 100644 index 62b2f0fddd..0000000000 --- a/services/brig/schema/src/V40.hs +++ /dev/null @@ -1,38 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V40 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = - Migration 40 "Add hashed userkeys table" $ - schema' - [r| - create table if not exists user_keys_hash - ( key blob - , key_type int -- hash type (0 = PHONE, 1 = EMAIL) - , user uuid - , primary key (key) - ) with compaction = {'class': 'LeveledCompactionStrategy'}; - |] diff --git a/services/brig/schema/src/V41.hs b/services/brig/schema/src/V41.hs deleted file mode 100644 index 0d86c8fbe9..0000000000 --- a/services/brig/schema/src/V41.hs +++ /dev/null @@ -1,33 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V41 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = - Migration 41 "Add searchable field to user table" $ - schema' - [r| - alter table user add searchable boolean - |] diff --git a/services/brig/schema/src/V42.hs b/services/brig/schema/src/V42.hs deleted file mode 100644 index 708134320b..0000000000 --- a/services/brig/schema/src/V42.hs +++ /dev/null @@ -1,34 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V42 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = - Migration 42 "Remove user.tracking_id" $ - void $ - schema' - [r| - alter columnfamily user drop tracking_id; - |] diff --git a/services/brig/schema/src/V43.hs b/services/brig/schema/src/V43.hs index 842461bdb3..288ea41b2e 100644 --- a/services/brig/schema/src/V43.hs +++ b/services/brig/schema/src/V43.hs @@ -25,7 +25,368 @@ import Imports import Text.RawString.QQ migration :: Migration -migration = Migration 43 "Add team invitations" $ do +migration = Migration 43 "Initial brig schema at time of open-sourcing wire-server in 2017" $ do + void $ + schema' + [r| + create columnfamily if not exists user + ( id uuid + , accent list -- accent colour RGBA + , accent_id int -- accent colour ID + , name text -- user name + , picture list -- user picture(s) (asset metadata) + , email text + , phone text + , password blob -- pw hash + , activated boolean + , status int + , language ascii + , country ascii + , primary key (id) + ) with compaction = {'class': 'LeveledCompactionStrategy'}; + |] + void $ + schema' + [r| + -- verified, 'unique' user attributes + create columnfamily if not exists user_keys + ( key text -- email or phone number + , user uuid -- user ID + , primary key (key) + ) with compaction = {'class': 'LeveledCompactionStrategy'}; + |] + void $ + schema' + [r| + -- (temporary) activation keys + create columnfamily if not exists activation_keys + ( key ascii -- opaque version of key_text + , key_type ascii -- ("email" or "phone") + , key_text text -- the plain 'key' (phone or email) + , code ascii -- random code + , user uuid + , retries int -- # of remaining attempts + , challenge ascii + , primary key (key) + ) with compaction = {'class': 'LeveledCompactionStrategy'}; + |] + void $ + schema' + [r| + -- (temporary) password reset codes + create columnfamily if not exists password_reset + ( key ascii -- opaque version of the user ID + , code ascii -- random code + , user uuid + , primary key (key) + ) with compaction = {'class': 'LeveledCompactionStrategy'}; + |] + void $ + schema' + [r| + create columnfamily if not exists connection + ( left uuid -- user id "from" in the relation + , right uuid -- user id "to" in the relation + , status int -- relation type (0 = ACCEPTED, 1 = BLOCKED, 2 = PENDING, 3 = IGNORED) + , last_update timestamp -- last time this relation was updated + , message text -- message sent together with the request + , conv uuid -- conv id between the 2 (if needed) + , primary key (left, right) + ) with compaction = {'class': 'LeveledCompactionStrategy'}; + |] + void $ + schema' + [r| + create index if not exists conn_status on connection (status); + |] + void $ + schema' + [r| + create columnfamily if not exists invitee_info + ( invitee uuid -- user id generated with the invitation + , inviter uuid -- user id that created the invitation + , conv uuid -- conv id between the 2 + , primary key (invitee) + ) with compaction = {'class': 'LeveledCompactionStrategy'}; + |] + + -- Add prekeys + void $ + schema' + [r| + create columnfamily if not exists clients + ( user uuid + , client text + , tstamp timestamp + , type int + , label text + , primary key (user, client) + ); + |] + void $ + schema' + [r| + create columnfamily if not exists prekeys + ( user uuid + , client text + , key int + , data text + , primary key (user, client, key) + ); + |] + + -- Add properties + void $ + schema' + [r| + create columnfamily if not exists properties + ( user uuid + , key ascii + , value blob + , primary key (user, key) + ); + |] + + -- Add login_codes + void $ + schema' + [r| + create columnfamily if not exists login_codes + ( user uuid + , code text + , retries int + , timeout timestamp + , primary key (user) + ); + |] + + -- Add password_reset.retries and timeout + void $ + schema' + [r| + alter columnfamily password_reset add retries int; + |] + void $ + schema' + [r| + alter columnfamily password_reset add timeout timestamp; + |] + + -- Add additional client properties + schema' [r| alter columnfamily clients add class int; |] + schema' [r| alter columnfamily clients add cookie text; |] + + -- Create new invitations tables + void $ + schema' + [r| + create columnfamily if not exists invitation + ( inviter uuid -- user id that created the invitation + , id uuid -- invitation id reference (relevant for inviter) + , code ascii -- code of the invitation (known only by invitee) + , email text -- email of the user invited + , phone text -- phone of the user invited + , created_at timestamp -- time this invitation was created + , name text -- name of the invitee + , primary key (inviter, id) + ); + |] + void $ + schema' + [r| + create columnfamily if not exists invitation_info + ( code ascii -- code of the invitation (known only by invitee) + , inviter uuid -- user id that created the invitation + , id uuid -- invitation id reference (relevant for inviter) + , primary key (code) + ); + |] + + -- Add even more client properties + schema' [r| alter columnfamily clients add ip inet; |] + schema' [r| alter columnfamily clients add lat double; |] + schema' [r| alter columnfamily clients add lon double; |] + -- Add model to clients + schema' [r| alter columnfamily clients add model text; |] + -- Add generic verification codes + schema' + [r| + create columnfamily if not exists codes + ( user uuid + , scope int + , code text + , retries int + , primary key (user, scope) + ); + |] + + -- Add user.assets column + schema' + [r| + create type if not exists asset + ( typ int + , key text + ); + |] + schema' + [r| + alter columnfamily user add assets list>; + |] + -- Add vcodes table + -- Supposed to cover all existing use-cases for short-lived + -- verification codes sent either by e-mail, sms or voice call, + -- eventually superseding the 'activation_keys', 'login_codes', + -- 'password_reset' and 'codes' tables. + schema' + [r| + create table if not exists vcodes + ( key ascii -- opaque 'email' or 'phone' + , scope int + , value ascii -- secret value + , retries int -- attempts left + , email text -- email address (xor phone) + , phone text -- phone number (xor email) + , account uuid -- optional associated account ID + , primary key (key, scope) + ) with compaction = {'class': 'LeveledCompactionStrategy'} + and gc_grace_seconds = 0; + |] + + -- Add service provider tables + schema' + [r| + create table if not exists provider + ( id uuid + , name text + , email text + , password blob + , url blob + , descr text + , primary key (id) + ) with compaction = {'class': 'LeveledCompactionStrategy'}; + |] + schema' + [r| + create table if not exists provider_keys + ( key text + , provider uuid + , primary key (key) + ) with compaction = {'class': 'LeveledCompactionStrategy'}; + |] + schema' + [r| + create type if not exists pubkey + ( typ int + , size int + , pem blob + ); + |] + schema' + [r| + create table if not exists service + ( provider uuid + , id uuid + , name text + , descr text + , base_url blob + , auth_tokens list + , pubkeys list> + , fingerprints list + , assets list> + , tags set + , enabled boolean + , primary key (provider, id) + ) with compaction = {'class': 'LeveledCompactionStrategy'}; + |] + schema' + [r| + create table if not exists service_tag + ( bucket int + , tag bigint + , name text + , service uuid + , provider uuid + , primary key ((bucket, tag), name, service) + ) with clustering order by (name asc, service asc) + and compaction = {'class': 'LeveledCompactionStrategy'}; + |] + schema' + [r| + alter table user add provider uuid; + |] + schema' + [r| + alter table user add service uuid; + |] + -- Add asset.size attribute + schema' + [r| + alter type asset add size int; + |] + -- Add budget table + schema' + [r| + create table if not exists budget + ( key text + , budget int + , primary key (key) + ) with compaction = {'class': 'LeveledCompactionStrategy'} + and gc_grace_seconds = 0; + |] + + -- Add user handles + schema' + [r| + create table if not exists unique_claims + ( value text + , claims set + , primary key (value) + ) with compaction = {'class': 'LeveledCompactionStrategy'} + and gc_grace_seconds = 0; + |] + schema' + [r| + create table if not exists user_handle + ( handle text + , user uuid + , primary key (handle) + ) with compaction = {'class': 'LeveledCompactionStrategy'}; + |] + schema' + [r| + alter table user add handle text; + |] + -- Add user_cookies table + schema' + [r| + create table if not exists user_cookies + ( user uuid + , expires timestamp + , id bigint + , label text + , type int + , created timestamp + , succ_id bigint + , primary key (user, expires, id) + ) with compaction = {'class': 'LeveledCompactionStrategy'}; + |] + -- Add hashed userkeys table + schema' + [r| + create table if not exists user_keys_hash + ( key blob + , key_type int -- hash type (0 = PHONE, 1 = EMAIL) + , user uuid + , primary key (key) + ) with compaction = {'class': 'LeveledCompactionStrategy'}; + |] + -- Add searchable field to user table + schema' + [r| + alter table user add searchable boolean + |] + + -- Add team invitations schema' [r| create columnfamily if not exists team_invitation diff --git a/services/brig/schema/src/V9.hs b/services/brig/schema/src/V9.hs deleted file mode 100644 index e398e4b1e2..0000000000 --- a/services/brig/schema/src/V9.hs +++ /dev/null @@ -1,136 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module V9 - ( migration, - ) -where - -import Cassandra.Schema -import Imports -import Text.RawString.QQ - -migration :: Migration -migration = Migration 9 "Initial schema" $ do - void $ - schema' - [r| - create columnfamily if not exists user - ( id uuid - , accent list -- accent colour RGBA - , accent_id int -- accent colour ID - , name text -- user name - , picture list -- user picture(s) (asset metadata) - , email text - , phone text - , password blob -- pw hash - , activated boolean - , primary key (id) - ); - |] - void $ - schema' - [r| - -- verified, 'unique' user attributes - create columnfamily if not exists user_keys - ( key text -- email or phone number - , user uuid -- user ID - , primary key (key) - ); - |] - void $ - schema' - [r| - -- (temporary) activation keys - create columnfamily if not exists activation_keys - ( key ascii -- opaque version of key_text - , key_type ascii -- ("email" or "phone") - , key_text text -- the plain 'key' (phone or email) - , code ascii -- random code - , user uuid - , retries int -- # of remaining attempts - , primary key (key) - ); - |] - void $ - schema' - [r| - -- (temporary) password reset codes - create columnfamily if not exists password_reset - ( key ascii -- opaque version of the user ID - , code ascii -- random code - , user uuid - , email text - , primary key (key) - ); - |] - void $ - schema' - [r| - create columnfamily if not exists push - ( ptoken text -- token - , app text -- application - , transport int -- transport type (0 = GCM, 1 = APNS) - , usr uuid -- user id - , primary key (ptoken, app, transport) - ); - |] - void $ - schema' - [r| - -- TODO: usr is a really bad secondary index! - create index if not exists push_usr_key on push (usr); - |] - void $ - schema' - [r| - create columnfamily if not exists connection - ( left uuid -- user id "from" in the relation - , right uuid -- user id "to" in the relation - , status int -- relation type (0 = ACCEPTED, 1 = BLOCKED, 2 = PENDING, 3 = IGNORED) - , last_update timestamp -- last time this relation was updated - , message text -- message sent together with the request - , conv uuid -- conv id between the 2 (if needed) - , primary key (left, right) - ); - |] - void $ - schema' - [r| - create index if not exists conn_status on connection (status); - |] - void $ - schema' - [r| - create columnfamily if not exists invitation - ( inviter uuid -- user id that created the invitation - , invitee uuid -- user id generated with the invitation - , email text -- email of the user invited - , last_update timestamp -- last time this invitation was updated - , primary key (inviter, email) - ); - |] - void $ - schema' - [r| - create columnfamily if not exists invitee_info - ( invitee uuid -- user id generated with the invitation - , inviter uuid -- user id that created the invitation - , conv uuid -- conv id between the 2 - , primary key (invitee) - ); - |] diff --git a/services/brig/src/Brig/API.hs b/services/brig/src/Brig/API.hs index f9bc09e4ed..b477b7346d 100644 --- a/services/brig/src/Brig/API.hs +++ b/services/brig/src/Brig/API.hs @@ -29,26 +29,21 @@ import Brig.Effects.CodeStore import Brig.Effects.GalleyProvider (GalleyProvider) import Brig.Effects.PasswordResetStore (PasswordResetStore) import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) -import qualified Data.Swagger.Build.Api as Doc import Network.Wai.Routing (Routes) import Polysemy import Wire.Sem.Concurrency sitemap :: forall r p. - ( Members - '[ BlacklistPhonePrefixStore, - BlacklistStore, - GalleyProvider, - CodeStore, - Concurrency 'Unsafe, - PasswordResetStore, - UserPendingActivationStore p - ] - r + ( Member BlacklistPhonePrefixStore r, + Member BlacklistStore r, + Member GalleyProvider r, + Member CodeStore r, + Member (Concurrency 'Unsafe) r, + Member PasswordResetStore r, + Member (UserPendingActivationStore p) r ) => - Routes Doc.ApiBuilder (Handler r) () + Routes () (Handler r) () sitemap = do Public.sitemap - Public.apiDocs Internal.sitemap diff --git a/services/brig/src/Brig/API/Auth.hs b/services/brig/src/Brig/API/Auth.hs index b89733053e..b549fe9d5a 100644 --- a/services/brig/src/Brig/API/Auth.hs +++ b/services/brig/src/Brig/API/Auth.hs @@ -43,7 +43,6 @@ import Network.HTTP.Types import Network.Wai.Utilities ((!>>)) import qualified Network.Wai.Utilities.Error as Wai import Polysemy -import Wire.API.Federation.API import Wire.API.User import Wire.API.User.Auth hiding (access) import Wire.API.User.Auth.LegalHold @@ -51,7 +50,6 @@ import Wire.API.User.Auth.ReAuth import Wire.API.User.Auth.Sso accessH :: - CallsFed 'Brig "on-user-deleted-connections" => Maybe ClientId -> [Either Text SomeUserToken] -> Maybe (Either Text SomeAccessToken) -> @@ -63,7 +61,7 @@ accessH mcid ut' mat' = do >>= either (uncurry (access mcid)) (uncurry (access mcid)) access :: - (TokenPair u a, CallsFed 'Brig "on-user-deleted-connections") => + (TokenPair u a) => Maybe ClientId -> NonEmpty (Token u) -> Maybe (Token a) -> @@ -74,11 +72,11 @@ access mcid t mt = sendLoginCode :: SendLoginCode -> Handler r LoginCodeTimeout sendLoginCode (SendLoginCode phone call force) = do - checkWhitelist (Right phone) + checkAllowlist (Right phone) c <- wrapClientE (Auth.sendLoginCode phone call force) !>> sendLoginCodeError pure $ LoginCodeTimeout (pendingLoginTimeout c) -login :: (Member GalleyProvider r, CallsFed 'Brig "on-user-deleted-connections") => Login -> Maybe Bool -> Handler r SomeAccess +login :: (Member GalleyProvider r) => Login -> Maybe Bool -> Handler r SomeAccess login l (fromMaybe False -> persist) = do let typ = if persist then PersistentCookie else SessionCookie c <- Auth.login l typ !>> loginError @@ -130,13 +128,13 @@ removeCookies :: Local UserId -> RemoveCookies -> Handler r () removeCookies lusr (RemoveCookies pw lls ids) = wrapClientE (Auth.revokeAccess (tUnqualified lusr) pw ids lls) !>> authError -legalHoldLogin :: (Member GalleyProvider r, CallsFed 'Brig "on-user-deleted-connections") => LegalHoldLogin -> Handler r SomeAccess +legalHoldLogin :: (Member GalleyProvider r) => LegalHoldLogin -> Handler r SomeAccess legalHoldLogin lhl = do let typ = PersistentCookie -- Session cookie isn't a supported use case here c <- Auth.legalHoldLogin lhl typ !>> legalHoldLoginError traverse mkUserTokenCookie c -ssoLogin :: CallsFed 'Brig "on-user-deleted-connections" => SsoLogin -> Maybe Bool -> Handler r SomeAccess +ssoLogin :: SsoLogin -> Maybe Bool -> Handler r SomeAccess ssoLogin l (fromMaybe False -> persist) = do let typ = if persist then PersistentCookie else SessionCookie c <- wrapHttpClientE (Auth.ssoLogin l typ) !>> loginError diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 4c30599850..3ac4f7e255 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -89,11 +89,10 @@ import Data.String.Conversions (cs) import Imports import Network.HTTP.Types.Method (StdMethod) import Network.Wai.Utilities -import Polysemy (Member, Members) +import Polysemy (Member) import Servant (Link, ToHttpApiData (toUrlPiece)) import System.Logger.Class (field, msg, val, (~~)) import qualified System.Logger.Class as Log -import Wire.API.Federation.API import Wire.API.Federation.API.Brig (GetUserClients (GetUserClients)) import Wire.API.Federation.Error import Wire.API.MLS.Credential (ClientIdentity (..)) @@ -116,12 +115,12 @@ lookupLocalClient uid = wrapClient . Data.lookupClient uid lookupLocalClients :: UserId -> (AppT r) [Client] lookupLocalClients = wrapClient . Data.lookupClients -lookupPubClient :: CallsFed 'Brig "get-user-clients" => Qualified UserId -> ClientId -> ExceptT ClientError (AppT r) (Maybe PubClient) +lookupPubClient :: Qualified UserId -> ClientId -> ExceptT ClientError (AppT r) (Maybe PubClient) lookupPubClient qid cid = do clients <- lookupPubClients qid pure $ find ((== cid) . pubClientId) clients -lookupPubClients :: CallsFed 'Brig "get-user-clients" => Qualified UserId -> ExceptT ClientError (AppT r) [PubClient] +lookupPubClients :: Qualified UserId -> ExceptT ClientError (AppT r) [PubClient] lookupPubClients qid@(Qualified uid domain) = do getForUser <$> lookupPubClientsBulk [qid] where @@ -130,7 +129,7 @@ lookupPubClients qid@(Qualified uid domain) = do um <- userMap <$> Map.lookup domain (qualifiedUserMap qmap) Set.toList <$> Map.lookup uid um -lookupPubClientsBulk :: CallsFed 'Brig "get-user-clients" => [Qualified UserId] -> ExceptT ClientError (AppT r) (QualifiedUserMap (Set PubClient)) +lookupPubClientsBulk :: [Qualified UserId] -> ExceptT ClientError (AppT r) (QualifiedUserMap (Set PubClient)) lookupPubClientsBulk qualifiedUids = do loc <- qualifyLocal () let (localUsers, remoteUsers) = partitionQualified loc qualifiedUids @@ -146,7 +145,7 @@ lookupLocalPubClientsBulk :: [UserId] -> ExceptT ClientError (AppT r) (UserMap ( lookupLocalPubClientsBulk = lift . wrapClient . Data.lookupPubClientsBulk addClient :: - (Members '[GalleyProvider] r, CallsFed 'Brig "on-user-deleted-connections") => + (Member GalleyProvider r) => UserId -> Maybe ConnId -> Maybe IP -> @@ -158,7 +157,7 @@ addClient = addClientWithReAuthPolicy Data.reAuthForNewClients -- a superset of the clients known to galley. addClientWithReAuthPolicy :: forall r. - (Members '[GalleyProvider] r, CallsFed 'Brig "on-user-deleted-connections") => + (Member GalleyProvider r) => Data.ReAuthPolicy -> UserId -> Maybe ConnId -> @@ -239,7 +238,6 @@ rmClient u con clt pw = lift $ execDelete u (Just con) client claimPrekey :: - CallsFed 'Brig "claim-prekey" => LegalholdProtectee -> UserId -> Domain -> @@ -266,15 +264,14 @@ claimLocalPrekey protectee user client = do claimRemotePrekey :: ( MonadReader Env m, Log.MonadLogger m, - MonadClient m, - CallsFed 'Brig "claim-prekey" + MonadClient m ) => Qualified UserId -> ClientId -> ExceptT ClientError m (Maybe ClientPrekey) claimRemotePrekey quser client = fmapLT ClientFederationError $ Federation.claimPrekey quser client -claimPrekeyBundle :: CallsFed 'Brig "claim-prekey-bundle" => LegalholdProtectee -> Domain -> UserId -> ExceptT ClientError (AppT r) PrekeyBundle +claimPrekeyBundle :: LegalholdProtectee -> Domain -> UserId -> ExceptT ClientError (AppT r) PrekeyBundle claimPrekeyBundle protectee domain uid = do isLocalDomain <- (domain ==) <$> viewFederationDomain if isLocalDomain @@ -287,13 +284,13 @@ claimLocalPrekeyBundle protectee u = do guardLegalhold protectee (mkUserClients [(u, clients)]) PrekeyBundle u . catMaybes <$> lift (mapM (wrapHttp . Data.claimPrekey u) clients) -claimRemotePrekeyBundle :: CallsFed 'Brig "claim-prekey-bundle" => Qualified UserId -> ExceptT ClientError (AppT r) PrekeyBundle +claimRemotePrekeyBundle :: Qualified UserId -> ExceptT ClientError (AppT r) PrekeyBundle claimRemotePrekeyBundle quser = do Federation.claimPrekeyBundle quser !>> ClientFederationError claimMultiPrekeyBundles :: forall r. - (Members '[Concurrency 'Unsafe] r, CallsFed 'Brig "claim-multi-prekey-bundle") => + (Member (Concurrency 'Unsafe) r) => LegalholdProtectee -> QualifiedUserClients -> ExceptT ClientError (AppT r) QualifiedUserClientPrekeyMap @@ -333,7 +330,7 @@ claimMultiPrekeyBundles protectee quc = do claimLocalMultiPrekeyBundles :: forall r. - Members '[Concurrency 'Unsafe] r => + Member (Concurrency 'Unsafe) r => LegalholdProtectee -> UserClients -> ExceptT ClientError (AppT r) UserClientPrekeyMap @@ -413,7 +410,7 @@ pubClient c = pubClientClass = clientClass c } -legalHoldClientRequested :: CallsFed 'Brig "on-user-deleted-connections" => UserId -> LegalHoldClientRequest -> (AppT r) () +legalHoldClientRequested :: UserId -> LegalHoldClientRequest -> (AppT r) () legalHoldClientRequested targetUser (LegalHoldClientRequest _requester lastPrekey') = wrapHttpClient $ Intra.onUserEvent targetUser Nothing lhClientEvent where @@ -424,7 +421,7 @@ legalHoldClientRequested targetUser (LegalHoldClientRequest _requester lastPreke lhClientEvent :: UserEvent lhClientEvent = LegalHoldClientRequested eventData -removeLegalHoldClient :: CallsFed 'Brig "on-user-deleted-connections" => UserId -> (AppT r) () +removeLegalHoldClient :: UserId -> (AppT r) () removeLegalHoldClient uid = do clients <- wrapClient $ Data.lookupClients uid -- Should only be one; but just in case we'll treat it as a list diff --git a/services/brig/src/Brig/API/Connection.hs b/services/brig/src/Brig/API/Connection.hs index f1c54d08dc..9296a403c7 100644 --- a/services/brig/src/Brig/API/Connection.hs +++ b/services/brig/src/Brig/API/Connection.hs @@ -53,14 +53,13 @@ import Data.Qualified import Data.Range import qualified Data.UUID.V4 as UUID import Imports -import Polysemy (Members) +import Polysemy (Member) import qualified System.Logger.Class as Log import System.Logger.Message import Wire.API.Connection hiding (relationWithHistory) -import Wire.API.Conversation +import Wire.API.Conversation hiding (Member) import Wire.API.Error import qualified Wire.API.Error.Brig as E -import Wire.API.Federation.API import Wire.API.Routes.Public.Util (ResponseForExistedCreated (..)) ensureIsActivated :: Local UserId -> MaybeT (AppT r) () @@ -68,7 +67,7 @@ ensureIsActivated lusr = do active <- lift . wrapClient $ Data.isActivated (tUnqualified lusr) guard active -ensureNotSameTeam :: Members '[GalleyProvider] r => Local UserId -> Local UserId -> (ConnectionM r) () +ensureNotSameTeam :: Member GalleyProvider r => Local UserId -> Local UserId -> (ConnectionM r) () ensureNotSameTeam self target = do selfTeam <- lift $ liftSem $ GalleyProvider.getTeamId (tUnqualified self) targetTeam <- lift $ liftSem $ GalleyProvider.getTeamId (tUnqualified target) @@ -76,7 +75,7 @@ ensureNotSameTeam self target = do throwE ConnectSameBindingTeamUsers createConnection :: - (Members '[GalleyProvider] r, CallsFed 'Brig "send-connection-action") => + (Member GalleyProvider r) => Local UserId -> ConnId -> Qualified UserId -> @@ -96,7 +95,7 @@ createConnection self con target = do target createConnectionToLocalUser :: - Members '[GalleyProvider] r => + Member GalleyProvider r => Local UserId -> ConnId -> Local UserId -> @@ -185,7 +184,7 @@ createConnectionToLocalUser self conn target = do -- FUTUREWORK: we may want to move this to the LH application logic, so we can recycle it for -- group conv creation and possibly other situations. checkLegalholdPolicyConflict :: - Members '[GalleyProvider] r => + Member GalleyProvider r => UserId -> UserId -> ExceptT ConnectionError (AppT r) () @@ -211,7 +210,6 @@ checkLegalholdPolicyConflict uid1 uid2 = do oneway status2 status1 updateConnection :: - CallsFed 'Brig "send-connection-action" => Local UserId -> Qualified UserId -> Relation -> diff --git a/services/brig/src/Brig/API/Connection/Remote.hs b/services/brig/src/Brig/API/Connection/Remote.hs index 4567753e68..34e7eef34d 100644 --- a/services/brig/src/Brig/API/Connection/Remote.hs +++ b/services/brig/src/Brig/API/Connection/Remote.hs @@ -35,15 +35,14 @@ import Control.Error.Util ((??)) import Control.Monad.Trans.Except (runExceptT, throwE) import Data.Id as Id import Data.Qualified -import Galley.Types.Conversations.Intra (Actor (..), DesiredMembership (..), UpsertOne2OneConversationRequest (..), UpsertOne2OneConversationResponse (uuorConvId)) import Imports import Network.Wai.Utilities.Error import Wire.API.Connection -import Wire.API.Federation.API import Wire.API.Federation.API.Brig ( NewConnectionResponse (..), RemoteConnectionAction (..), ) +import Wire.API.Routes.Internal.Galley.ConversationsIntra (Actor (..), DesiredMembership (..), UpsertOne2OneConversationRequest (..), UpsertOne2OneConversationResponse (uuorConvId)) import Wire.API.Routes.Public.Util (ResponseForExistedCreated (..)) data LocalConnectionAction @@ -188,7 +187,6 @@ pushEvent self mzcon connection = do Intra.onConnectionEvent (tUnqualified self) mzcon event performLocalAction :: - CallsFed 'Brig "send-connection-action" => Local UserId -> Maybe ConnId -> Remote UserId -> @@ -253,7 +251,6 @@ performRemoteAction self other mconnection action = do reaction _ = Nothing createConnectionToRemoteUser :: - CallsFed 'Brig "send-connection-action" => Local UserId -> ConnId -> Remote UserId -> @@ -263,7 +260,6 @@ createConnectionToRemoteUser self zcon other = do fst <$> performLocalAction self (Just zcon) other mconnection LocalConnect updateConnectionToRemoteUser :: - CallsFed 'Brig "send-connection-action" => Local UserId -> Remote UserId -> Relation -> diff --git a/services/brig/src/Brig/API/Error.hs b/services/brig/src/Brig/API/Error.hs index 0f76c11ac2..7cd9ceb025 100644 --- a/services/brig/src/Brig/API/Error.hs +++ b/services/brig/src/Brig/API/Error.hs @@ -171,11 +171,14 @@ clientError ClientMissingLegalholdConsent = StdError (errorToWai @'E.MissingLega clientError ClientCodeAuthenticationFailed = StdError verificationCodeAuthFailed clientError ClientCodeAuthenticationRequired = StdError verificationCodeRequired +-- Note that UnknownError, FfiError, and ImplementationError semantically should rather be 500s than 400s. +-- However, the errors returned from the FFI are not always correct, +-- and we don't want to bombard our environments with 500 log messages, so we treat them as 400s, for now. certEnrollmentError :: CertEnrollmentError -> Error -certEnrollmentError (RustError NoError) = StdError $ Wai.mkError status500 "internal-error" "The server experienced an internal error during DPoP token generation. Unexpected NoError." -certEnrollmentError (RustError UnknownError) = StdError $ Wai.mkError status500 "internal-error" "The server experienced an internal error during DPoP token generation. Unknown error." -certEnrollmentError (RustError FfiError) = StdError $ Wai.mkError status500 "internal-error" "The server experienced an internal error during DPoP token generation" -certEnrollmentError (RustError ImplementationError) = StdError $ Wai.mkError status500 "internal-error" "The server experienced an internal error during DPoP token generation. Unexpected ImplementationError." +certEnrollmentError (RustError NoError) = StdError $ Wai.mkError status400 "internal-error" "The server experienced an internal error during DPoP token generation. Unexpected NoError." +certEnrollmentError (RustError UnknownError) = StdError $ Wai.mkError status400 "internal-error" "The server experienced an internal error during DPoP token generation. Unknown error." +certEnrollmentError (RustError FfiError) = StdError $ Wai.mkError status400 "internal-error" "The server experienced an internal error during DPoP token generation" +certEnrollmentError (RustError ImplementationError) = StdError $ Wai.mkError status400 "internal-error" "The server experienced an internal error during DPoP token generation. Unexpected ImplementationError." certEnrollmentError (RustError DpopSyntaxError) = StdError $ Wai.mkError status400 "client-token-parse-error" "The client JWT DPoP could not be parsed" certEnrollmentError (RustError DpopTypError) = StdError $ Wai.mkError status400 "client-token-type-error" "The client JWT DPoP 'typ' must be 'dpop+jwt'" certEnrollmentError (RustError DpopUnsupportedAlgorithmError) = StdError $ Wai.mkError status400 "client-token-unsupported-alg" "DPoP signature algorithm (alg) in JWT header is not a supported algorithm (ES256, ES384, Ed25519)" @@ -295,8 +298,8 @@ activationCodeNotFound = invalidActivationCode "Activation key/code not found or deletionCodePending :: Wai.Error deletionCodePending = Wai.mkError status403 "pending-delete" "A verification code for account deletion is still pending." -whitelistError :: Wai.Error -whitelistError = Wai.mkError status403 "unauthorized" "Unauthorized e-mail address or phone number." +allowlistError :: Wai.Error +allowlistError = Wai.mkError status403 "unauthorized" "Unauthorized e-mail address or phone number." blacklistedEmail :: Wai.Error blacklistedEmail = diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index 7fb1caebc8..61edef6ae9 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -70,11 +70,9 @@ import Wire.Sem.Concurrency type FederationAPI = "federation" :> BrigApi federationSitemap :: - Members - '[ GalleyProvider, - Concurrency 'Unsafe - ] - r => + ( Member GalleyProvider r, + Member (Concurrency 'Unsafe) r + ) => ServerT FederationAPI (Handler r) federationSitemap = Named @"api-version" (\_ _ -> pure versionInfo) @@ -103,10 +101,7 @@ sendConnectionAction originDomain NewConnectionRequest {..} = do else pure NewConnectionResponseUserNotActivated getUserByHandle :: - Members - '[ GalleyProvider - ] - r => + Member GalleyProvider r => Domain -> Handle -> ExceptT Error (AppT r) (Maybe UserProfile) @@ -129,10 +124,7 @@ getUserByHandle domain handle = do listToMaybe <$> API.lookupLocalProfiles Nothing [ownerId] getUsersByIds :: - Members - '[ GalleyProvider - ] - r => + Member GalleyProvider r => Domain -> [UserId] -> ExceptT Error (AppT r) [UserProfile] @@ -148,7 +140,7 @@ claimPrekeyBundle _ user = API.claimLocalPrekeyBundle LegalholdPlusFederationNotImplemented user !>> clientError claimMultiPrekeyBundle :: - Members '[Concurrency 'Unsafe] r => + Member (Concurrency 'Unsafe) r => Domain -> UserClients -> Handler r UserClientPrekeyMap @@ -169,7 +161,7 @@ fedClaimKeyPackages domain ckpr = -- (This decision may change in the future) searchUsers :: forall r. - Members '[GalleyProvider] r => + Member GalleyProvider r => Domain -> SearchRequest -> ExceptT Error (AppT r) SearchResponse diff --git a/services/brig/src/Brig/API/Handler.hs b/services/brig/src/Brig/API/Handler.hs index 5d29b9a279..2b74c1172a 100644 --- a/services/brig/src/Brig/API/Handler.hs +++ b/services/brig/src/Brig/API/Handler.hs @@ -24,22 +24,22 @@ module Brig.API.Handler -- * Utilities JSON, parseJsonBody, - checkWhitelist, - checkWhitelistWithError, - isWhiteListed, + checkAllowlist, + checkAllowlistWithError, + isAllowlisted, UserNotAllowedToJoinTeam (..), ) where -import Bilge (MonadHttp, RequestId (..)) +import Bilge (RequestId (..)) import Brig.API.Error import qualified Brig.AWS as AWS +import qualified Brig.Allowlists as Allowlists import Brig.App import Brig.CanonicalInterpreter (BrigCanonicalEffects, runBrigToIO) import Brig.Email (Email) -import Brig.Options (setWhitelist) +import Brig.Options (setAllowlistEmailDomains, setAllowlistPhonePrefixes) import Brig.Phone (Phone, PhoneException (..)) -import qualified Brig.Whitelist as Whitelist import Control.Error import Control.Exception (throwIO) import Control.Lens (set, view) @@ -167,18 +167,17 @@ type JSON = Media "application" "json" parseJsonBody :: (FromJSON a, MonadIO m) => JsonRequest a -> ExceptT Error m a parseJsonBody req = parseBody req !>> StdError . badRequest --- | If a whitelist is configured, consult it, otherwise a no-op. {#RefActivationWhitelist} -checkWhitelist :: Either Email Phone -> (Handler r) () -checkWhitelist = wrapHttpClientE . checkWhitelistWithError (StdError whitelistError) +-- | If an Allowlist is configured, consult it, otherwise a no-op. {#RefActivationAllowlist} +checkAllowlist :: Either Email Phone -> (Handler r) () +checkAllowlist = wrapHttpClientE . checkAllowlistWithError (StdError allowlistError) -checkWhitelistWithError :: (Monad m, MonadReader Env m, MonadIO m, Catch.MonadMask m, MonadHttp m, MonadError e m) => e -> Either Email Phone -> m () -checkWhitelistWithError e key = do - ok <- isWhiteListed key +-- checkAllowlistWithError :: (MonadReader Env m, MonadIO m, Catch.MonadMask m, MonadHttp m, MonadError e m) => e -> Either Email Phone -> m () +checkAllowlistWithError :: (MonadReader Env m, MonadError e m) => e -> Either Email Phone -> m () +checkAllowlistWithError e key = do + ok <- isAllowlisted key unless ok (throwError e) -isWhiteListed :: (Monad m, MonadReader Env m, MonadIO m, Catch.MonadMask m, MonadHttp m) => Either Email Phone -> m Bool -isWhiteListed key = do - eb <- setWhitelist <$> view settings - case eb of - Nothing -> pure True - Just b -> Whitelist.verify b key +isAllowlisted :: (MonadReader Env m) => Either Email Phone -> m Bool +isAllowlisted key = do + env <- view settings + pure $ Allowlists.verify (setAllowlistEmailDomains env) (setAllowlistPhonePrefixes env) key diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index d01481190b..f0e8eee1ef 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -17,9 +17,7 @@ module Brig.API.Internal ( sitemap, servantSitemap, - swaggerDocsAPI, BrigIRoutes.API, - BrigIRoutes.SwaggerDocsAPI, getMLSClients, ) where @@ -81,7 +79,6 @@ import Network.Wai.Utilities.ZAuth (zauthConnId, zauthUserId) import Polysemy import Servant hiding (Handler, JSON, addHeader, respond) import Servant.Swagger.Internal.Orphans () -import Servant.Swagger.UI import qualified System.Logger.Class as Log import UnliftIO.Async import Wire.API.Connection @@ -106,12 +103,9 @@ import Wire.API.User.RichInfo -- Sitemap (servant) servantSitemap :: - ( Members - '[ BlacklistStore, - GalleyProvider, - UserPendingActivationStore p - ] - r + ( Member BlacklistStore r, + Member GalleyProvider r, + Member (UserPendingActivationStore p) r ) => ServerT BrigIRoutes.API (Handler r) servantSitemap = @@ -124,10 +118,7 @@ servantSitemap = :<|> authAPI ejpdAPI :: - Members - '[ GalleyProvider - ] - r => + Member GalleyProvider r => ServerT BrigIRoutes.EJPD_API (Handler r) ejpdAPI = Brig.User.EJPD.ejpdRequest @@ -152,17 +143,14 @@ mlsAPI = :<|> Named @"put-key-package-add" upsertKeyPackage accountAPI :: - ( Members - '[ BlacklistStore, - GalleyProvider, - UserPendingActivationStore p - ] - r + ( Member BlacklistStore r, + Member GalleyProvider r, + Member (UserPendingActivationStore p) r ) => ServerT BrigIRoutes.AccountAPI (Handler r) accountAPI = - Named @"createUserNoVerify" (callsFed createUserNoVerify) - :<|> Named @"createUserNoVerifySpar" (callsFed createUserNoVerifySpar) + Named @"createUserNoVerify" (callsFed (exposeAnnotations createUserNoVerify)) + :<|> Named @"createUserNoVerifySpar" (callsFed (exposeAnnotations createUserNoVerifySpar)) teamsAPI :: ServerT BrigIRoutes.TeamsAPI (Handler r) teamsAPI = Named @"updateSearchVisibilityInbound" Index.updateSearchVisibilityInbound @@ -175,8 +163,8 @@ userAPI = authAPI :: (Member GalleyProvider r) => ServerT BrigIRoutes.AuthAPI (Handler r) authAPI = - Named @"legalhold-login" (callsFed legalHoldLogin) - :<|> Named @"sso-login" (callsFed ssoLogin) + Named @"legalhold-login" (callsFed (exposeAnnotations legalHoldLogin)) + :<|> Named @"sso-login" (callsFed (exposeAnnotations ssoLogin)) :<|> Named @"login-code" getLoginCode :<|> Named @"reauthenticate" reauthenticate @@ -280,22 +268,16 @@ getVerificationCode uid action = do code <- wrapClientE $ Code.lookup key (Code.scopeFromAction a) pure $ Code.codeValue <$> code -swaggerDocsAPI :: Servant.Server BrigIRoutes.SwaggerDocsAPI -swaggerDocsAPI = swaggerSchemaUIServer BrigIRoutes.swaggerDoc - --------------------------------------------------------------------------- -- Sitemap (wai-route) sitemap :: - ( Members - '[ CodeStore, - PasswordResetStore, - BlacklistStore, - BlacklistPhonePrefixStore, - GalleyProvider, - UserPendingActivationStore p - ] - r + ( Member CodeStore r, + Member PasswordResetStore r, + Member BlacklistStore r, + Member BlacklistPhonePrefixStore r, + Member GalleyProvider r, + Member (UserPendingActivationStore p) r ) => Routes a (Handler r) () sitemap = unsafeCallsFed @'Brig @"on-user-deleted-connections" $ do @@ -460,12 +442,7 @@ sitemap = unsafeCallsFed @'Brig @"on-user-deleted-connections" $ do -- | Add a client without authentication checks addClientInternalH :: - ( Members - '[ GalleyProvider - ] - r, - CallsFed 'Brig "on-user-deleted-connections" - ) => + (Member GalleyProvider r) => UserId ::: Maybe Bool ::: JsonRequest NewClient ::: Maybe ConnId ::: JSON -> (Handler r) Response addClientInternalH (usr ::: mSkipReAuth ::: req ::: connId ::: _) = do @@ -473,12 +450,7 @@ addClientInternalH (usr ::: mSkipReAuth ::: req ::: connId ::: _) = do setStatus status201 . json <$> addClientInternal usr mSkipReAuth new connId addClientInternal :: - ( Members - '[ GalleyProvider - ] - r, - CallsFed 'Brig "on-user-deleted-connections" - ) => + (Member GalleyProvider r) => UserId -> Maybe Bool -> NewClient -> @@ -490,13 +462,13 @@ addClientInternal usr mSkipReAuth new connId = do | otherwise = Data.reAuthForNewClients API.addClientWithReAuthPolicy policy usr connId Nothing new !>> clientError -legalHoldClientRequestedH :: (CallsFed 'Brig "on-user-deleted-connections") => UserId ::: JsonRequest LegalHoldClientRequest ::: JSON -> (Handler r) Response +legalHoldClientRequestedH :: UserId ::: JsonRequest LegalHoldClientRequest ::: JSON -> (Handler r) Response legalHoldClientRequestedH (targetUser ::: req ::: _) = do clientRequest <- parseJsonBody req lift $ API.legalHoldClientRequested targetUser clientRequest pure $ setStatus status200 empty -removeLegalHoldClientH :: (CallsFed 'Brig "on-user-deleted-connections") => UserId ::: JSON -> (Handler r) Response +removeLegalHoldClientH :: UserId ::: JSON -> (Handler r) Response removeLegalHoldClientH (uid ::: _) = do lift $ API.removeLegalHoldClient uid pure $ setStatus status200 empty @@ -519,13 +491,9 @@ internalListFullClients (UserSet usrs) = UserClientsFull <$> wrapClient (Data.lookupClientsBulk (Set.toList usrs)) createUserNoVerify :: - ( Members - '[ BlacklistStore, - GalleyProvider, - UserPendingActivationStore p - ] - r, - CallsFed 'Brig "on-user-deleted-connections" + ( Member BlacklistStore r, + Member GalleyProvider r, + Member (UserPendingActivationStore p) r ) => NewUser -> (Handler r) (Either RegisterError SelfProfile) @@ -543,12 +511,7 @@ createUserNoVerify uData = lift . runExceptT $ do pure . SelfProfile $ usr createUserNoVerifySpar :: - ( Members - '[ GalleyProvider - ] - r, - CallsFed 'Brig "on-user-deleted-connections" - ) => + (Member GalleyProvider r) => NewUserSpar -> (Handler r) (Either CreateUserSparError SelfProfile) createUserNoVerifySpar uData = @@ -565,7 +528,7 @@ createUserNoVerifySpar uData = in API.activate key code (Just uid) !>> CreateUserSparRegistrationError . activationErrorToRegisterError pure . SelfProfile $ usr -deleteUserNoAuthH :: (CallsFed 'Brig "on-user-deleted-connections") => UserId -> (Handler r) Response +deleteUserNoAuthH :: UserId -> (Handler r) Response deleteUserNoAuthH uid = do r <- lift $ wrapHttp $ API.ensureAccountDeleted uid case r of @@ -646,14 +609,18 @@ instance ToJSON GetActivationCodeResp where toJSON (GetActivationCodeResp (k, c)) = object ["key" .= k, "code" .= c] getPasswordResetCodeH :: - Members '[CodeStore, PasswordResetStore] r => + ( Member CodeStore r, + Member PasswordResetStore r + ) => JSON ::: Either Email Phone -> (Handler r) Response getPasswordResetCodeH (_ ::: emailOrPhone) = do maybe (throwStd (errorToWai @'E.InvalidPasswordResetKey)) (pure . json) =<< lift (getPasswordResetCode emailOrPhone) getPasswordResetCode :: - Members '[CodeStore, PasswordResetStore] r => + ( Member CodeStore r, + Member PasswordResetStore r + ) => Either Email Phone -> (AppT r) (Maybe GetPasswordResetCodeResp) getPasswordResetCode emailOrPhone = @@ -664,7 +631,7 @@ newtype GetPasswordResetCodeResp = GetPasswordResetCodeResp (PasswordResetKey, P instance ToJSON GetPasswordResetCodeResp where toJSON (GetPasswordResetCodeResp (k, c)) = object ["key" .= k, "code" .= c] -changeAccountStatusH :: (CallsFed 'Brig "on-user-deleted-connections") => UserId ::: JsonRequest AccountStatusUpdate -> (Handler r) Response +changeAccountStatusH :: UserId ::: JsonRequest AccountStatusUpdate -> (Handler r) Response changeAccountStatusH (usr ::: req) = do status <- suStatus <$> parseJsonBody req wrapHttpClientE (API.changeSingleAccountStatus usr status) !>> accountStatusError @@ -701,7 +668,7 @@ getConnectionsStatus (ConnectionsStatusRequestV2 froms mtos mrel) = do where filterByRelation l rel = filter ((== rel) . csv2Status) l -revokeIdentityH :: (CallsFed 'Brig "on-user-deleted-connections") => Either Email Phone -> (Handler r) Response +revokeIdentityH :: Either Email Phone -> (Handler r) Response revokeIdentityH emailOrPhone = do lift $ API.revokeIdentity emailOrPhone pure $ setStatus status200 empty @@ -748,7 +715,7 @@ addPhonePrefixH (_ ::: req) = do void . lift $ API.phonePrefixInsert prefix pure empty -updateSSOIdH :: (CallsFed 'Brig "on-user-deleted-connections") => UserId ::: JSON ::: JsonRequest UserSSOId -> (Handler r) Response +updateSSOIdH :: UserId ::: JSON ::: JsonRequest UserSSOId -> (Handler r) Response updateSSOIdH (uid ::: _ ::: req) = do ssoid :: UserSSOId <- parseJsonBody req success <- lift $ wrapClient $ Data.updateSSOId uid (Just ssoid) @@ -758,7 +725,7 @@ updateSSOIdH (uid ::: _ ::: req) = do pure empty else pure . setStatus status404 $ plain "User does not exist or has no team." -deleteSSOIdH :: (CallsFed 'Brig "on-user-deleted-connections") => UserId ::: JSON -> (Handler r) Response +deleteSSOIdH :: UserId ::: JSON -> (Handler r) Response deleteSSOIdH (uid ::: _) = do success <- lift $ wrapClient $ Data.updateSSOId uid Nothing if success @@ -814,18 +781,18 @@ getRichInfoMulti :: [UserId] -> (Handler r) [(UserId, RichInfo)] getRichInfoMulti uids = lift (wrapClient $ API.lookupRichInfoMultiUsers uids) -updateHandleH :: (CallsFed 'Brig "on-user-deleted-connections") => UserId ::: JSON ::: JsonRequest HandleUpdate -> (Handler r) Response +updateHandleH :: UserId ::: JSON ::: JsonRequest HandleUpdate -> (Handler r) Response updateHandleH (uid ::: _ ::: body) = empty <$ (updateHandle uid =<< parseJsonBody body) -updateHandle :: (CallsFed 'Brig "on-user-deleted-connections") => UserId -> HandleUpdate -> (Handler r) () +updateHandle :: UserId -> HandleUpdate -> (Handler r) () updateHandle uid (HandleUpdate handleUpd) = do handle <- validateHandle handleUpd API.changeHandle uid Nothing handle API.AllowSCIMUpdates !>> changeHandleError -updateUserNameH :: (CallsFed 'Brig "on-user-deleted-connections") => UserId ::: JSON ::: JsonRequest NameUpdate -> (Handler r) Response +updateUserNameH :: UserId ::: JSON ::: JsonRequest NameUpdate -> (Handler r) Response updateUserNameH (uid ::: _ ::: body) = empty <$ (updateUserName uid =<< parseJsonBody body) -updateUserName :: (CallsFed 'Brig "on-user-deleted-connections") => UserId -> NameUpdate -> (Handler r) () +updateUserName :: UserId -> NameUpdate -> (Handler r) () updateUserName uid (NameUpdate nameUpd) = do name <- either (const $ throwStd (errorToWai @'E.InvalidUser)) pure $ mkName nameUpd let uu = diff --git a/services/brig/src/Brig/API/MLS/KeyPackages.hs b/services/brig/src/Brig/API/MLS/KeyPackages.hs index 63379c4de8..74742fe176 100644 --- a/services/brig/src/Brig/API/MLS/KeyPackages.hs +++ b/services/brig/src/Brig/API/MLS/KeyPackages.hs @@ -55,7 +55,6 @@ uploadKeyPackages lusr cid (kpuKeyPackages -> kps) = do lift . wrapClient $ Data.insertKeyPackages (tUnqualified lusr) cid kps' claimKeyPackages :: - CallsFed 'Brig "claim-key-packages" => Local UserId -> Qualified UserId -> Maybe ClientId -> @@ -97,7 +96,6 @@ claimLocalKeyPackages qusr skipOwn target = do <$> wrapClientM (Data.claimKeyPackage target c) claimRemoteKeyPackages :: - CallsFed 'Brig "claim-key-packages" => Local UserId -> Remote UserId -> Handler r KeyPackageBundle diff --git a/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs b/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs index 186328a7f1..2ebed2e370 100644 --- a/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs +++ b/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs @@ -105,10 +105,14 @@ validateKeyPackage identity (RawMLS (KeyPackageData -> kpd) kp) = do validateCredential :: ClientIdentity -> Credential -> Handler r () validateCredential identity cred = do identity' <- - either mlsProtocolError pure $ + either credentialError pure $ decodeMLS' (bcIdentity cred) when (identity /= identity') $ throwStd (errorToWai @'MLSIdentityMismatch) + where + credentialError e = + mlsProtocolError $ + "Failed to parse identity: " <> e data RequiredExtensions f = RequiredExtensions { reLifetime :: f Lifetime, diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index bdcf685931..008ca4d5c2 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -20,7 +20,6 @@ module Brig.API.Public ( sitemap, - apiDocs, servantSitemap, docsAPI, DocsAPI, @@ -88,19 +87,16 @@ import Data.Nonce (Nonce, randomNonce) import Data.Qualified import Data.Range import qualified Data.Swagger as S -import qualified Data.Swagger.Build.Api as Doc import qualified Data.Text as Text import qualified Data.Text.Ascii as Ascii -import Data.Text.Encoding (decodeLatin1) import Data.Text.Lazy (pack) import qualified Data.ZAuth.Token as ZAuth import FileEmbedLzma import Galley.Types.Teams (HiddenPerm (..), hasPermission) import Imports hiding (head) -import Network.Wai.Predicate hiding (result, setStatus) +import Network.Socket (PortNumber) import Network.Wai.Routing import Network.Wai.Utilities as Utilities -import Network.Wai.Utilities.Swagger (mkSwaggerApi) import Polysemy import Servant hiding (Handler, JSON, addHeader, respond) import qualified Servant @@ -113,6 +109,11 @@ import Wire.API.Error import qualified Wire.API.Error.Brig as E import Wire.API.Federation.API import qualified Wire.API.Properties as Public +import qualified Wire.API.Routes.Internal.Brig as BrigInternalAPI +import qualified Wire.API.Routes.Internal.Cannon as CannonInternalAPI +import qualified Wire.API.Routes.Internal.Cargohold as CargoholdInternalAPI +import qualified Wire.API.Routes.Internal.Galley as GalleyInternalAPI +import qualified Wire.API.Routes.Internal.Spar as SparInternalAPI import qualified Wire.API.Routes.MultiTablePaging as Public import Wire.API.Routes.Named (Named (Named)) import Wire.API.Routes.Public.Brig @@ -124,12 +125,11 @@ import qualified Wire.API.Routes.Public.Proxy as ProxyAPI import qualified Wire.API.Routes.Public.Spar as SparAPI import qualified Wire.API.Routes.Public.Util as Public import Wire.API.Routes.Version -import qualified Wire.API.Swagger as Public.Swagger (models) import Wire.API.SwaggerHelper (cleanupSwagger) import Wire.API.SystemSettings import qualified Wire.API.Team as Public import Wire.API.Team.LegalHold (LegalholdProtectee (..)) -import Wire.API.User (RegisterError (RegisterErrorWhitelistError)) +import Wire.API.User (RegisterError (RegisterErrorAllowlistError)) import qualified Wire.API.User as Public import qualified Wire.API.User.Activation as Public import qualified Wire.API.User.Auth as Public @@ -147,10 +147,20 @@ import Wire.Sem.Now (Now) -- User API ----------------------------------------------------------- docsAPI :: Servant.Server DocsAPI -docsAPI = versionedSwaggerDocsAPI :<|> pure eventNotificationSchemas - +docsAPI = + versionedSwaggerDocsAPI + :<|> pure eventNotificationSchemas + :<|> internalEndpointsSwaggerDocsAPI "brig" 9082 BrigInternalAPI.swaggerDoc + :<|> internalEndpointsSwaggerDocsAPI "cannon" 9093 CannonInternalAPI.swaggerDoc + :<|> internalEndpointsSwaggerDocsAPI "cargohold" 9094 CargoholdInternalAPI.swaggerDoc + :<|> internalEndpointsSwaggerDocsAPI "galley" 9095 GalleyInternalAPI.swaggerDoc + :<|> internalEndpointsSwaggerDocsAPI "spar" 9098 SparInternalAPI.swaggerDoc + +-- | Serves Swagger docs for public endpoints +-- +-- Dual to `internalEndpointsSwaggerDocsAPI`. versionedSwaggerDocsAPI :: Servant.Server VersionedSwaggerDocsAPI -versionedSwaggerDocsAPI (Just V3) = +versionedSwaggerDocsAPI (Just V4) = swaggerSchemaUIServer $ ( brigSwagger <> versionSwagger @@ -167,24 +177,44 @@ versionedSwaggerDocsAPI (Just V3) = versionedSwaggerDocsAPI (Just V0) = swaggerPregenUIServer $(pregenSwagger V0) versionedSwaggerDocsAPI (Just V1) = swaggerPregenUIServer $(pregenSwagger V1) versionedSwaggerDocsAPI (Just V2) = swaggerPregenUIServer $(pregenSwagger V2) +versionedSwaggerDocsAPI (Just V3) = swaggerPregenUIServer $(pregenSwagger V3) versionedSwaggerDocsAPI Nothing = versionedSwaggerDocsAPI (Just maxBound) +-- | Serves Swagger docs for internal endpoints +-- +-- Dual to `versionedSwaggerDocsAPI`. Swagger docs for old versions are (almost) +-- empty. It would have been too tedious to create them. Please add +-- pre-generated docs on version increase as it's done in +-- `versionedSwaggerDocsAPI`. +internalEndpointsSwaggerDocsAPI :: + String -> + PortNumber -> + S.Swagger -> + Servant.Server (VersionedSwaggerDocsAPIBase service) +internalEndpointsSwaggerDocsAPI service examplePort swagger (Just V4) = + swaggerSchemaUIServer $ + swagger + & adjustSwaggerForInternalEndpoint service examplePort + & cleanupSwagger +internalEndpointsSwaggerDocsAPI _ _ _ (Just V0) = emptySwagger +internalEndpointsSwaggerDocsAPI _ _ _ (Just V1) = emptySwagger +internalEndpointsSwaggerDocsAPI _ _ _ (Just V2) = emptySwagger +internalEndpointsSwaggerDocsAPI _ _ _ (Just V3) = emptySwagger +internalEndpointsSwaggerDocsAPI service examplePort swagger Nothing = + internalEndpointsSwaggerDocsAPI service examplePort swagger (Just maxBound) + servantSitemap :: forall r p. - ( Members - '[ BlacklistPhonePrefixStore, - BlacklistStore, - CodeStore, - Concurrency 'Unsafe, - Concurrency 'Unsafe, - GalleyProvider, - JwtTools, - Now, - PasswordResetStore, - PublicKeyBundle, - UserPendingActivationStore p - ] - r + ( Member BlacklistPhonePrefixStore r, + Member BlacklistStore r, + Member CodeStore r, + Member (Concurrency 'Unsafe) r, + Member GalleyProvider r, + Member JwtTools r, + Member Now r, + Member PasswordResetStore r, + Member PublicKeyBundle r, + Member (UserPendingActivationStore p) r ) => ServerT BrigAPI (Handler r) servantSitemap = @@ -206,35 +236,35 @@ servantSitemap = where userAPI :: ServerT UserAPI (Handler r) userAPI = - Named @"get-user-unqualified" (callsFed getUserUnqualifiedH) - :<|> Named @"get-user-qualified" (callsFed getUser) + Named @"get-user-unqualified" (callsFed (exposeAnnotations getUserUnqualifiedH)) + :<|> Named @"get-user-qualified" (callsFed (exposeAnnotations getUser)) :<|> Named @"update-user-email" updateUserEmail - :<|> Named @"get-handle-info-unqualified" (callsFed getHandleInfoUnqualifiedH) - :<|> Named @"get-user-by-handle-qualified" (callsFed Handle.getHandleInfo) - :<|> Named @"list-users-by-unqualified-ids-or-handles" (callsFed listUsersByUnqualifiedIdsOrHandles) - :<|> Named @"list-users-by-ids-or-handles" (callsFed listUsersByIdsOrHandles) + :<|> Named @"get-handle-info-unqualified" (callsFed (exposeAnnotations getHandleInfoUnqualifiedH)) + :<|> Named @"get-user-by-handle-qualified" (callsFed (exposeAnnotations Handle.getHandleInfo)) + :<|> Named @"list-users-by-unqualified-ids-or-handles" (callsFed (exposeAnnotations listUsersByUnqualifiedIdsOrHandles)) + :<|> Named @"list-users-by-ids-or-handles" (callsFed (exposeAnnotations listUsersByIdsOrHandles)) :<|> Named @"send-verification-code" sendVerificationCode :<|> Named @"get-rich-info" getRichInfo selfAPI :: ServerT SelfAPI (Handler r) selfAPI = Named @"get-self" getSelf - :<|> Named @"delete-self" (callsFed deleteSelfUser) - :<|> Named @"put-self" (callsFed updateUser) + :<|> Named @"delete-self" (callsFed (exposeAnnotations deleteSelfUser)) + :<|> Named @"put-self" (callsFed (exposeAnnotations updateUser)) :<|> Named @"change-phone" changePhone - :<|> Named @"remove-phone" (callsFed removePhone) - :<|> Named @"remove-email" (callsFed removeEmail) + :<|> Named @"remove-phone" (callsFed (exposeAnnotations removePhone)) + :<|> Named @"remove-email" (callsFed (exposeAnnotations removeEmail)) :<|> Named @"check-password-exists" checkPasswordExists :<|> Named @"change-password" changePassword - :<|> Named @"change-locale" (callsFed changeLocale) - :<|> Named @"change-handle" (callsFed changeHandle) + :<|> Named @"change-locale" (callsFed (exposeAnnotations changeLocale)) + :<|> Named @"change-handle" (callsFed (exposeAnnotations changeHandle)) accountAPI :: ServerT AccountAPI (Handler r) accountAPI = - Named @"register" (callsFed createUser) - :<|> Named @"verify-delete" (callsFed verifyDeleteUser) - :<|> Named @"get-activate" (callsFed activate) - :<|> Named @"post-activate" (callsFed activateKey) + Named @"register" (callsFed (exposeAnnotations createUser)) + :<|> Named @"verify-delete" (callsFed (exposeAnnotations verifyDeleteUser)) + :<|> Named @"get-activate" (callsFed (exposeAnnotations activate)) + :<|> Named @"post-activate" (callsFed (exposeAnnotations activateKey)) :<|> Named @"post-activate-send" sendActivationCode :<|> Named @"post-password-reset" beginPasswordReset :<|> Named @"post-password-reset-complete" completePasswordReset @@ -243,27 +273,29 @@ servantSitemap = clientAPI :: ServerT ClientAPI (Handler r) clientAPI = - Named @"get-user-clients-unqualified" (callsFed getUserClientsUnqualified) - :<|> Named @"get-user-clients-qualified" (callsFed getUserClientsQualified) - :<|> Named @"get-user-client-unqualified" (callsFed getUserClientUnqualified) - :<|> Named @"get-user-client-qualified" (callsFed getUserClientQualified) - :<|> Named @"list-clients-bulk" (callsFed listClientsBulk) - :<|> Named @"list-clients-bulk-v2" (callsFed listClientsBulkV2) - :<|> Named @"list-clients-bulk@v2" (callsFed listClientsBulkV2) + Named @"get-user-clients-unqualified" (callsFed (exposeAnnotations getUserClientsUnqualified)) + :<|> Named @"get-user-clients-qualified" (callsFed (exposeAnnotations getUserClientsQualified)) + :<|> Named @"get-user-client-unqualified" (callsFed (exposeAnnotations getUserClientUnqualified)) + :<|> Named @"get-user-client-qualified" (callsFed (exposeAnnotations getUserClientQualified)) + :<|> Named @"list-clients-bulk" (callsFed (exposeAnnotations listClientsBulk)) + :<|> Named @"list-clients-bulk-v2" (callsFed (exposeAnnotations listClientsBulkV2)) + :<|> Named @"list-clients-bulk@v2" (callsFed (exposeAnnotations listClientsBulkV2)) prekeyAPI :: ServerT PrekeyAPI (Handler r) prekeyAPI = - Named @"get-users-prekeys-client-unqualified" (callsFed getPrekeyUnqualifiedH) - :<|> Named @"get-users-prekeys-client-qualified" (callsFed getPrekeyH) - :<|> Named @"get-users-prekey-bundle-unqualified" (callsFed getPrekeyBundleUnqualifiedH) - :<|> Named @"get-users-prekey-bundle-qualified" (callsFed getPrekeyBundleH) + Named @"get-users-prekeys-client-unqualified" (callsFed (exposeAnnotations getPrekeyUnqualifiedH)) + :<|> Named @"get-users-prekeys-client-qualified" (callsFed (exposeAnnotations getPrekeyH)) + :<|> Named @"get-users-prekey-bundle-unqualified" (callsFed (exposeAnnotations getPrekeyBundleUnqualifiedH)) + :<|> Named @"get-users-prekey-bundle-qualified" (callsFed (exposeAnnotations getPrekeyBundleH)) :<|> Named @"get-multi-user-prekey-bundle-unqualified" getMultiUserPrekeyBundleUnqualifiedH - :<|> Named @"get-multi-user-prekey-bundle-qualified" (callsFed getMultiUserPrekeyBundleH) + :<|> Named @"get-multi-user-prekey-bundle-qualified" (callsFed (exposeAnnotations getMultiUserPrekeyBundleH)) userClientAPI :: ServerT UserClientAPI (Handler r) userClientAPI = - Named @"add-client" (callsFed addClient) - :<|> Named @"update-client" updateClient + Named @"add-client" (callsFed (exposeAnnotations addClient)) + :<|> Named + @"update-client" + updateClient :<|> Named @"delete-client" deleteClient :<|> Named @"list-clients" listClients :<|> Named @"get-client" getClient @@ -275,15 +307,15 @@ servantSitemap = connectionAPI :: ServerT ConnectionAPI (Handler r) connectionAPI = - Named @"create-connection-unqualified" (callsFed createConnectionUnqualified) - :<|> Named @"create-connection" (callsFed createConnection) + Named @"create-connection-unqualified" (callsFed (exposeAnnotations createConnectionUnqualified)) + :<|> Named @"create-connection" (callsFed (exposeAnnotations createConnection)) :<|> Named @"list-local-connections" listLocalConnections :<|> Named @"list-connections" listConnections :<|> Named @"get-connection-unqualified" getLocalConnection :<|> Named @"get-connection" getConnection - :<|> Named @"update-connection-unqualified" (callsFed updateLocalConnection) - :<|> Named @"update-connection" (callsFed updateConnection) - :<|> Named @"search-contacts" (callsFed Search.search) + :<|> Named @"update-connection-unqualified" (callsFed (exposeAnnotations updateLocalConnection)) + :<|> Named @"update-connection" (callsFed (exposeAnnotations updateConnection)) + :<|> Named @"search-contacts" (callsFed (exposeAnnotations Search.search)) propertiesAPI :: ServerT PropertiesAPI (Handler r) propertiesAPI = @@ -298,7 +330,7 @@ servantSitemap = mlsAPI :: ServerT MLSAPI (Handler r) mlsAPI = Named @"mls-key-packages-upload" uploadKeyPackages - :<|> Named @"mls-key-packages-claim" (callsFed claimKeyPackages) + :<|> Named @"mls-key-packages-claim" (callsFed (exposeAnnotations claimKeyPackages)) :<|> Named @"mls-key-packages-count" countKeyPackages userHandleAPI :: ServerT UserHandleAPI (Handler r) @@ -312,9 +344,9 @@ servantSitemap = authAPI :: ServerT AuthAPI (Handler r) authAPI = - Named @"access" (callsFed accessH) + Named @"access" (callsFed (exposeAnnotations accessH)) :<|> Named @"send-login-code" sendLoginCode - :<|> Named @"login" (callsFed login) + :<|> Named @"login" (callsFed (exposeAnnotations login)) :<|> Named @"logout" logoutH :<|> Named @"change-self-email" changeSelfEmailH :<|> Named @"list-cookies" listCookies @@ -326,7 +358,9 @@ servantSitemap = :<|> Named @"get-calls-config-v2" Calling.getCallsConfigV2 systemSettingsAPI :: ServerT SystemSettingsAPI (Handler r) - systemSettingsAPI = Named @ "get-system-settings" getSystemSettings + systemSettingsAPI = + Named @"get-system-settings-unauthorized" getSystemSettings + :<|> Named @"get-system-settings" getSystemSettingsInternal -- Note [ephemeral user sideeffect] -- If the user is ephemeral and expired, it will be removed upon calling @@ -336,41 +370,13 @@ servantSitemap = -- - MemberLeave event to members for all conversations the user was in (via galley) sitemap :: - Members - '[ BlacklistPhonePrefixStore, - BlacklistStore, - CodeStore, - Concurrency 'Unsafe, - GalleyProvider, - PasswordResetStore - ] - r => - Routes Doc.ApiBuilder (Handler r) () + ( Member (Concurrency 'Unsafe) r, + Member GalleyProvider r + ) => + Routes () (Handler r) () sitemap = do Provider.routesPublic -apiDocs :: - forall r. - Members - '[ BlacklistPhonePrefixStore, - BlacklistStore, - CodeStore, - Concurrency 'Unsafe, - GalleyProvider, - PasswordResetStore - ] - r => - Routes Doc.ApiBuilder (Handler r) () -apiDocs = - get - "/users/api-docs" - ( \(_ ::: url) k -> - let doc = mkSwaggerApi (decodeLatin1 url) Public.Swagger.models (sitemap @r) - in k $ json doc - ) - $ accept "application" "json" - .&. query "base_url" - --------------------------------------------------------------------------- -- Handlers @@ -430,27 +436,27 @@ listPropertyKeysAndValues u = do keysAndVals <- fmap Map.fromList . lift $ wrapClient (API.lookupPropertyKeysAndValues u) Public.PropertyKeysAndValues <$> traverse parseStoredPropertyValue keysAndVals -getPrekeyUnqualifiedH :: (CallsFed 'Brig "claim-prekey") => UserId -> UserId -> ClientId -> (Handler r) Public.ClientPrekey +getPrekeyUnqualifiedH :: UserId -> UserId -> ClientId -> (Handler r) Public.ClientPrekey getPrekeyUnqualifiedH zusr user client = do domain <- viewFederationDomain getPrekeyH zusr (Qualified user domain) client -getPrekeyH :: (CallsFed 'Brig "claim-prekey") => UserId -> Qualified UserId -> ClientId -> (Handler r) Public.ClientPrekey +getPrekeyH :: UserId -> Qualified UserId -> ClientId -> (Handler r) Public.ClientPrekey getPrekeyH zusr (Qualified user domain) client = do mPrekey <- API.claimPrekey (ProtectedUser zusr) user domain client !>> clientError ifNothing (notFound "prekey not found") mPrekey -getPrekeyBundleUnqualifiedH :: (CallsFed 'Brig "claim-prekey-bundle") => UserId -> UserId -> (Handler r) Public.PrekeyBundle +getPrekeyBundleUnqualifiedH :: UserId -> UserId -> (Handler r) Public.PrekeyBundle getPrekeyBundleUnqualifiedH zusr uid = do domain <- viewFederationDomain API.claimPrekeyBundle (ProtectedUser zusr) domain uid !>> clientError -getPrekeyBundleH :: (CallsFed 'Brig "claim-prekey-bundle") => UserId -> Qualified UserId -> (Handler r) Public.PrekeyBundle +getPrekeyBundleH :: UserId -> Qualified UserId -> (Handler r) Public.PrekeyBundle getPrekeyBundleH zusr (Qualified uid domain) = API.claimPrekeyBundle (ProtectedUser zusr) domain uid !>> clientError getMultiUserPrekeyBundleUnqualifiedH :: - Members '[Concurrency 'Unsafe] r => + Member (Concurrency 'Unsafe) r => UserId -> Public.UserClients -> Handler r Public.UserClientPrekeyMap @@ -461,7 +467,7 @@ getMultiUserPrekeyBundleUnqualifiedH zusr userClients = do API.claimLocalMultiPrekeyBundles (ProtectedUser zusr) userClients !>> clientError getMultiUserPrekeyBundleH :: - (Members '[Concurrency 'Unsafe] r, CallsFed 'Brig "claim-multi-prekey-bundle") => + (Member (Concurrency 'Unsafe) r) => UserId -> Public.QualifiedUserClients -> (Handler r) Public.QualifiedUserClientPrekeyMap @@ -476,12 +482,7 @@ getMultiUserPrekeyBundleH zusr qualUserClients = do API.claimMultiPrekeyBundles (ProtectedUser zusr) qualUserClients !>> clientError addClient :: - ( Members - '[ GalleyProvider - ] - r, - CallsFed 'Brig "on-user-deleted-connections" - ) => + (Member GalleyProvider r) => UserId -> ConnId -> Maybe IpAddr -> @@ -512,28 +513,28 @@ listClients zusr = getClient :: UserId -> ClientId -> (Handler r) (Maybe Public.Client) getClient zusr clientId = lift $ API.lookupLocalClient zusr clientId -getUserClientsUnqualified :: (CallsFed 'Brig "get-user-clients") => UserId -> (Handler r) [Public.PubClient] +getUserClientsUnqualified :: UserId -> (Handler r) [Public.PubClient] getUserClientsUnqualified uid = do localdomain <- viewFederationDomain API.lookupPubClients (Qualified uid localdomain) !>> clientError -getUserClientsQualified :: (CallsFed 'Brig "get-user-clients") => Qualified UserId -> (Handler r) [Public.PubClient] +getUserClientsQualified :: Qualified UserId -> (Handler r) [Public.PubClient] getUserClientsQualified quid = API.lookupPubClients quid !>> clientError -getUserClientUnqualified :: (CallsFed 'Brig "get-user-clients") => UserId -> ClientId -> (Handler r) Public.PubClient +getUserClientUnqualified :: UserId -> ClientId -> (Handler r) Public.PubClient getUserClientUnqualified uid cid = do localdomain <- viewFederationDomain x <- API.lookupPubClient (Qualified uid localdomain) cid !>> clientError ifNothing (notFound "client not found") x -listClientsBulk :: (CallsFed 'Brig "get-user-clients") => UserId -> Range 1 MaxUsersForListClientsBulk [Qualified UserId] -> (Handler r) (Public.QualifiedUserMap (Set Public.PubClient)) +listClientsBulk :: UserId -> Range 1 MaxUsersForListClientsBulk [Qualified UserId] -> (Handler r) (Public.QualifiedUserMap (Set Public.PubClient)) listClientsBulk _zusr limitedUids = API.lookupPubClientsBulk (fromRange limitedUids) !>> clientError -listClientsBulkV2 :: (CallsFed 'Brig "get-user-clients") => UserId -> Public.LimitedQualifiedUserIdList MaxUsersForListClientsBulk -> (Handler r) (Public.WrappedQualifiedUserMap (Set Public.PubClient)) +listClientsBulkV2 :: UserId -> Public.LimitedQualifiedUserIdList MaxUsersForListClientsBulk -> (Handler r) (Public.WrappedQualifiedUserMap (Set Public.PubClient)) listClientsBulkV2 zusr userIds = Public.Wrapped <$> listClientsBulk zusr (Public.qualifiedUsers userIds) -getUserClientQualified :: (CallsFed 'Brig "get-user-clients") => Qualified UserId -> ClientId -> (Handler r) Public.PubClient +getUserClientQualified :: Qualified UserId -> ClientId -> (Handler r) Public.PubClient getUserClientQualified quid cid = do x <- API.lookupPubClient quid cid !>> clientError ifNothing (notFound "client not found") x @@ -589,20 +590,16 @@ createAccessToken method uid cid proof = do -- | docs/reference/user/registration.md {#RefRegistration} createUser :: - ( Members - '[ BlacklistStore, - GalleyProvider, - UserPendingActivationStore p - ] - r, - CallsFed 'Brig "on-user-deleted-connections" + ( Member BlacklistStore r, + Member GalleyProvider r, + Member (UserPendingActivationStore p) r ) => Public.NewUserPublic -> (Handler r) (Either Public.RegisterError Public.RegisterSuccess) createUser (Public.NewUserPublic new) = lift . runExceptT $ do API.checkRestrictedUserCreation new - for_ (Public.newUserEmail new) $ mapExceptT wrapHttp . checkWhitelistWithError RegisterErrorWhitelistError . Left - for_ (Public.newUserPhone new) $ mapExceptT wrapHttp . checkWhitelistWithError RegisterErrorWhitelistError . Right + for_ (Public.newUserEmail new) $ mapExceptT wrapHttp . checkAllowlistWithError RegisterErrorAllowlistError . Left + for_ (Public.newUserPhone new) $ mapExceptT wrapHttp . checkAllowlistWithError RegisterErrorAllowlistError . Right result <- API.createUser new let acc = createdAccount result @@ -673,12 +670,7 @@ getSelf self = >>= ifNothing (errorToWai @'E.UserNotFound) getUserUnqualifiedH :: - ( Members - '[ GalleyProvider - ] - r, - CallsFed 'Brig "get-users-by-ids" - ) => + (Member GalleyProvider r) => UserId -> UserId -> (Handler r) (Maybe Public.UserProfile) @@ -687,12 +679,7 @@ getUserUnqualifiedH self uid = do getUser self (Qualified uid domain) getUser :: - ( Members - '[ GalleyProvider - ] - r, - CallsFed 'Brig "get-users-by-ids" - ) => + (Member GalleyProvider r) => UserId -> Qualified UserId -> (Handler r) (Maybe Public.UserProfile) @@ -702,12 +689,8 @@ getUser self qualifiedUserId = do -- FUTUREWORK: Make servant understand that at least one of these is required listUsersByUnqualifiedIdsOrHandles :: - ( Members - '[ GalleyProvider, - Concurrency 'Unsafe - ] - r, - CallsFed 'Brig "get-users-by-ids" + ( Member GalleyProvider r, + Member (Concurrency 'Unsafe) r ) => UserId -> Maybe (CommaSeparatedList UserId) -> @@ -730,12 +713,8 @@ listUsersByUnqualifiedIdsOrHandles self mUids mHandles = do listUsersByIdsOrHandles :: forall r. - ( Members - '[ GalleyProvider, - Concurrency 'Unsafe - ] - r, - CallsFed 'Brig "get-users-by-ids" + ( Member GalleyProvider r, + Member (Concurrency 'Unsafe) r ) => UserId -> Public.ListUsersQuery -> @@ -767,17 +746,15 @@ newtype GetActivationCodeResp instance ToJSON GetActivationCodeResp where toJSON (GetActivationCodeResp (k, c)) = object ["key" .= k, "code" .= c] -updateUser :: (CallsFed 'Brig "on-user-deleted-connections") => UserId -> ConnId -> Public.UserUpdate -> (Handler r) (Maybe Public.UpdateProfileError) +updateUser :: UserId -> ConnId -> Public.UserUpdate -> (Handler r) (Maybe Public.UpdateProfileError) updateUser uid conn uu = do eithErr <- lift $ runExceptT $ API.updateUser uid (Just conn) uu API.ForbidSCIMUpdates pure $ either Just (const Nothing) eithErr changePhone :: - Members - '[ BlacklistStore, - BlacklistPhonePrefixStore - ] - r => + ( Member BlacklistStore r, + Member BlacklistPhonePrefixStore r + ) => UserId -> ConnId -> Public.PhoneUpdate -> @@ -788,11 +765,11 @@ changePhone u _ (Public.puPhone -> phone) = lift . exceptTToMaybe $ do let apair = (activationKey adata, activationCode adata) lift . wrapClient $ sendActivationSms pn apair loc -removePhone :: (CallsFed 'Brig "on-user-deleted-connections") => UserId -> ConnId -> (Handler r) (Maybe Public.RemoveIdentityError) +removePhone :: UserId -> ConnId -> (Handler r) (Maybe Public.RemoveIdentityError) removePhone self conn = lift . exceptTToMaybe $ API.removePhone self conn -removeEmail :: (CallsFed 'Brig "on-user-deleted-connections") => UserId -> ConnId -> (Handler r) (Maybe Public.RemoveIdentityError) +removeEmail :: UserId -> ConnId -> (Handler r) (Maybe Public.RemoveIdentityError) removeEmail self conn = lift . exceptTToMaybe $ API.removeEmail self conn @@ -802,7 +779,7 @@ checkPasswordExists = fmap isJust . lift . wrapClient . API.lookupPassword changePassword :: UserId -> Public.PasswordChange -> (Handler r) (Maybe Public.ChangePasswordError) changePassword u cp = lift . exceptTToMaybe $ API.changePassword u cp -changeLocale :: (CallsFed 'Brig "on-user-deleted-connections") => UserId -> ConnId -> Public.LocaleUpdate -> (Handler r) () +changeLocale :: UserId -> ConnId -> Public.LocaleUpdate -> (Handler r) () changeLocale u conn l = lift $ API.changeLocale u conn l -- | (zusr is ignored by this handler, ie. checking handles is allowed as long as you have @@ -826,12 +803,7 @@ checkHandles _ (Public.CheckHandles hs num) = do -- 'Handle.getHandleInfo') returns UserProfile to reduce traffic between backends -- in a federated scenario. getHandleInfoUnqualifiedH :: - ( Members - '[ GalleyProvider - ] - r, - CallsFed 'Brig "get-user-by-handle", - CallsFed 'Brig "get-users-by-ids" + ( Member GalleyProvider r ) => UserId -> Handle -> @@ -841,17 +813,17 @@ getHandleInfoUnqualifiedH self handle = do Public.UserHandleInfo . Public.profileQualifiedId <$$> Handle.getHandleInfo self (Qualified handle domain) -changeHandle :: (CallsFed 'Brig "on-user-deleted-connections") => UserId -> ConnId -> Public.HandleUpdate -> (Handler r) (Maybe Public.ChangeHandleError) +changeHandle :: UserId -> ConnId -> Public.HandleUpdate -> (Handler r) (Maybe Public.ChangeHandleError) changeHandle u conn (Public.HandleUpdate h) = lift . exceptTToMaybe $ do handle <- maybe (throwError Public.ChangeHandleInvalid) pure $ parseHandle h API.changeHandle u (Just conn) handle API.ForbidSCIMUpdates beginPasswordReset :: - Members '[PasswordResetStore] r => + Member PasswordResetStore r => Public.NewPasswordReset -> (Handler r) () beginPasswordReset (Public.NewPasswordReset target) = do - checkWhitelist target + checkAllowlist target (u, pair) <- API.beginPasswordReset target !>> pwResetError loc <- lift $ wrapClient $ API.lookupLocale u lift $ case target of @@ -859,7 +831,9 @@ beginPasswordReset (Public.NewPasswordReset target) = do Right phone -> wrapClient $ sendPasswordResetSms phone pair loc completePasswordReset :: - Members '[CodeStore, PasswordResetStore] r => + ( Member CodeStore r, + Member PasswordResetStore r + ) => Public.CompletePasswordReset -> (Handler r) () completePasswordReset req = do @@ -868,17 +842,15 @@ completePasswordReset req = do -- docs/reference/user/activation.md {#RefActivationRequest} -- docs/reference/user/registration.md {#RefRegistration} sendActivationCode :: - Members - '[ BlacklistStore, - BlacklistPhonePrefixStore, - GalleyProvider - ] - r => + ( Member BlacklistStore r, + Member BlacklistPhonePrefixStore r, + Member GalleyProvider r + ) => Public.SendActivationCode -> (Handler r) () sendActivationCode Public.SendActivationCode {..} = do either customerExtensionCheckBlockedDomains (const $ pure ()) saUserKey - checkWhitelist saUserKey + checkAllowlist saUserKey API.sendActivationCode saUserKey saLocale saCall !>> sendActCodeError -- | If the user presents an email address from a blocked domain, throw an error. @@ -898,12 +870,7 @@ customerExtensionCheckBlockedDomains email = do customerExtensionBlockedDomain domain createConnectionUnqualified :: - ( Members - '[ GalleyProvider - ] - r, - CallsFed 'Brig "send-connection-action" - ) => + (Member GalleyProvider r) => UserId -> ConnId -> Public.ConnectionRequest -> @@ -914,12 +881,7 @@ createConnectionUnqualified self conn cr = do API.createConnection lself conn (tUntagged target) !>> connError createConnection :: - ( Members - '[ GalleyProvider - ] - r, - CallsFed 'Brig "send-connection-action" - ) => + (Member GalleyProvider r) => UserId -> ConnId -> Qualified UserId -> @@ -928,12 +890,12 @@ createConnection self conn target = do lself <- qualifyLocal self API.createConnection lself conn target !>> connError -updateLocalConnection :: (CallsFed 'Brig "send-connection-action") => UserId -> ConnId -> UserId -> Public.ConnectionUpdate -> (Handler r) (Public.UpdateResult Public.UserConnection) +updateLocalConnection :: UserId -> ConnId -> UserId -> Public.ConnectionUpdate -> (Handler r) (Public.UpdateResult Public.UserConnection) updateLocalConnection self conn other update = do lother <- qualifyLocal other updateConnection self conn (tUntagged lother) update -updateConnection :: (CallsFed 'Brig "send-connection-action") => UserId -> ConnId -> Qualified UserId -> Public.ConnectionUpdate -> (Handler r) (Public.UpdateResult Public.UserConnection) +updateConnection :: UserId -> ConnId -> Qualified UserId -> Public.ConnectionUpdate -> (Handler r) (Public.UpdateResult Public.UserConnection) updateConnection self conn other update = do let newStatus = Public.cuStatus update lself <- qualifyLocal self @@ -999,28 +961,21 @@ getConnection self other = do lift . wrapClient $ Data.lookupConnection lself other deleteSelfUser :: - ( Members - '[ GalleyProvider - ] - r, - CallsFed 'Brig "on-user-deleted-connections" - ) => + (Member GalleyProvider r) => UserId -> Public.DeleteUser -> (Handler r) (Maybe Code.Timeout) deleteSelfUser u body = API.deleteSelfUser u (Public.deleteUserPassword body) !>> deleteUserError -verifyDeleteUser :: (CallsFed 'Brig "on-user-deleted-connections") => Public.VerifyDeleteUser -> Handler r () +verifyDeleteUser :: Public.VerifyDeleteUser -> Handler r () verifyDeleteUser body = API.verifyDeleteUser body !>> deleteUserError updateUserEmail :: forall r. - Members - '[ BlacklistStore, - GalleyProvider - ] - r => + ( Member BlacklistStore r, + Member GalleyProvider r + ) => UserId -> UserId -> Public.EmailUpdate -> @@ -1048,12 +1003,7 @@ updateUserEmail zuserId emailOwnerId (Public.EmailUpdate email) = do -- activation activate :: - ( Members - '[ GalleyProvider - ] - r, - CallsFed 'Brig "on-user-deleted-connections" - ) => + (Member GalleyProvider r) => Public.ActivationKey -> Public.ActivationCode -> (Handler r) ActivationRespWithStatus @@ -1063,12 +1013,7 @@ activate k c = do -- docs/reference/user/activation.md {#RefActivationSubmit} activateKey :: - ( Members - '[ GalleyProvider - ] - r, - CallsFed 'Brig "on-user-deleted-connections" - ) => + (Member GalleyProvider r) => Public.Activate -> (Handler r) ActivationRespWithStatus activateKey (Public.Activate tgt code dryrun) @@ -1086,10 +1031,7 @@ activateKey (Public.Activate tgt code dryrun) sendVerificationCode :: forall r. - Members - '[ GalleyProvider - ] - r => + Member GalleyProvider r => Public.SendVerificationCode -> (Handler r) () sendVerificationCode req = do @@ -1129,13 +1071,19 @@ sendVerificationCode req = do mbStatusEnabled <- lift $ liftSem $ GalleyProvider.getVerificationCodeEnabled `traverse` (Public.userTeam <$> accountUser =<< mbAccount) pure $ fromMaybe False mbStatusEnabled -getSystemSettings :: ExceptT Brig.API.Error.Error (AppT r) SystemSettings +getSystemSettings :: (Handler r) SystemSettingsPublic getSystemSettings = do optSettings <- view settings pure $ - SystemSettings - { systemSettingsSetRestrictUserCreation = fromMaybe False (setRestrictUserCreation optSettings) - } + SystemSettingsPublic $ + fromMaybe False (setRestrictUserCreation optSettings) + +getSystemSettingsInternal :: UserId -> (Handler r) SystemSettings +getSystemSettingsInternal _ = do + optSettings <- view settings + let pSettings = SystemSettingsPublic $ fromMaybe False (setRestrictUserCreation optSettings) + let iSettings = SystemSettingsInternal $ fromMaybe False (setEnableMLS optSettings) + pure $ SystemSettings pSettings iSettings -- Deprecated @@ -1143,7 +1091,9 @@ deprecatedOnboarding :: UserId -> JsonValue -> (Handler r) DeprecatedMatchingRes deprecatedOnboarding _ _ = pure DeprecatedMatchingResult deprecatedCompletePasswordReset :: - Members '[CodeStore, PasswordResetStore] r => + ( Member CodeStore r, + Member PasswordResetStore r + ) => Public.PasswordResetKey -> Public.PasswordReset -> (Handler r) () diff --git a/services/brig/src/Brig/API/Public/Swagger.hs b/services/brig/src/Brig/API/Public/Swagger.hs index 49c49e009c..0e75164817 100644 --- a/services/brig/src/Brig/API/Public/Swagger.hs +++ b/services/brig/src/Brig/API/Public/Swagger.hs @@ -1,9 +1,15 @@ module Brig.API.Public.Swagger ( VersionedSwaggerDocsAPI, + InternalEndpointsSwaggerDocsAPI, + VersionedSwaggerDocsAPIBase, + SwaggerDocsAPIBase, + ServiceSwaggerDocsAPIBase, DocsAPI, pregenSwagger, swaggerPregenUIServer, eventNotificationSchemas, + adjustSwaggerForInternalEndpoint, + emptySwagger, ) where @@ -11,12 +17,15 @@ import Control.Lens import qualified Data.Aeson as A import Data.FileEmbed import qualified Data.HashMap.Strict.InsOrd as HM +import qualified Data.HashSet.InsOrd as InsOrdSet import qualified Data.Swagger as S import qualified Data.Swagger.Declare as S import qualified Data.Text as T import FileEmbedLzma +import GHC.TypeLits import Imports hiding (head) import Language.Haskell.TH +import Network.Socket import Servant import Servant.Swagger.Internal.Orphans () import Servant.Swagger.UI @@ -25,13 +34,27 @@ import qualified Wire.API.Event.FeatureConfig import qualified Wire.API.Event.Team import Wire.API.Routes.Version -type VersionedSwaggerDocsAPIBase = SwaggerSchemaUI "swagger-ui" "swagger.json" +type SwaggerDocsAPIBase = SwaggerSchemaUI "swagger-ui" "swagger.json" -type VersionedSwaggerDocsAPI = "api" :> Header VersionHeader Version :> VersionedSwaggerDocsAPIBase +type VersionedSwaggerDocsAPI = "api" :> Header VersionHeader Version :> SwaggerDocsAPIBase + +type ServiceSwaggerDocsAPIBase service = SwaggerSchemaUI service (AppendSymbol service "-swagger.json") + +type VersionedSwaggerDocsAPIBase service = Header VersionHeader Version :> ServiceSwaggerDocsAPIBase service + +type InternalEndpointsSwaggerDocsAPI = + "api-internal" + :> "swagger-ui" + :> ( VersionedSwaggerDocsAPIBase "brig" + :<|> VersionedSwaggerDocsAPIBase "cannon" + :<|> VersionedSwaggerDocsAPIBase "cargohold" + :<|> VersionedSwaggerDocsAPIBase "galley" + :<|> VersionedSwaggerDocsAPIBase "spar" + ) type NotificationSchemasAPI = "api" :> "event-notification-schemas" :> Get '[JSON] [S.Definitions S.Schema] -type DocsAPI = VersionedSwaggerDocsAPI :<|> NotificationSchemasAPI +type DocsAPI = VersionedSwaggerDocsAPI :<|> NotificationSchemasAPI :<|> InternalEndpointsSwaggerDocsAPI pregenSwagger :: Version -> Q Exp pregenSwagger v = @@ -39,12 +62,50 @@ pregenSwagger v = =<< makeRelativeToProject ("docs/swagger-v" <> T.unpack (toUrlPiece v) <> ".json") -swaggerPregenUIServer :: LByteString -> Server VersionedSwaggerDocsAPIBase +swaggerPregenUIServer :: LByteString -> Server SwaggerDocsAPIBase swaggerPregenUIServer = swaggerSchemaUIServer . fromMaybe A.Null . A.decode +adjustSwaggerForInternalEndpoint :: String -> PortNumber -> S.Swagger -> S.Swagger +adjustSwaggerForInternalEndpoint service examplePort swagger = + swagger + & S.info . S.title .~ T.pack ("Wire-Server internal API (" ++ service ++ ")") + & S.info . S.description ?~ renderedDescription + & S.host ?~ S.Host "localhost" (Just examplePort) + & S.allOperations . S.tags <>~ tag + -- Enforce HTTP as the services themselves don't understand HTTPS + & S.schemes ?~ [S.Http] + & S.allOperations . S.schemes ?~ [S.Http] + where + tag :: InsOrdSet.InsOrdHashSet S.TagName + tag = InsOrdSet.singleton @S.TagName (T.pack service) + + renderedDescription :: Text + renderedDescription = + T.pack . Imports.unlines $ + [ "To have access to this *internal* endpoint, create a port forwarding to `" + ++ service + ++ "` into the Kubernetes cluster. E.g.:", + "```", + "kubectl port-forward -n wire service/" + ++ service + ++ " " + ++ show examplePort + ++ ":8080", + "```", + "**N.B.:** Execution via this UI won't work due to CORS issues." + ++ " But, the proposed `curl` commands will." + ] + +emptySwagger :: Servant.Server (ServiceSwaggerDocsAPIBase a) +emptySwagger = + swaggerSchemaUIServer $ + mempty @S.Swagger + & S.info . S.description + ?~ "There is no Swagger documentation for this version. Please refer to v3 or later." + {- FUTUREWORK(fisx): there are a few things that need to be fixed before this schema collection is of any practical use! diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index a58f31f713..d8944a4ea4 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -161,7 +161,6 @@ import Data.Qualified import Data.Time.Clock (addUTCTime, diffUTCTime) import Data.UUID.V4 (nextRandom) import qualified Galley.Types.Teams as Team -import qualified Galley.Types.Teams.Intra as Team import Imports import Network.Wai.Utilities import Polysemy @@ -172,9 +171,9 @@ import UnliftIO.Async import Wire.API.Connection import Wire.API.Error import qualified Wire.API.Error.Brig as E -import Wire.API.Federation.API import Wire.API.Federation.Error import Wire.API.Routes.Internal.Brig.Connection +import qualified Wire.API.Routes.Internal.Galley.TeamsIntra as Team import Wire.API.Team hiding (newTeam) import Wire.API.Team.Feature (forgetLock) import Wire.API.Team.Invitation @@ -228,12 +227,7 @@ verifyUniquenessAndCheckBlacklist uk = do createUserSpar :: forall r. - ( Members - '[ GalleyProvider - ] - r, - CallsFed 'Brig "on-user-deleted-connections" - ) => + (Member GalleyProvider r) => NewUserSpar -> ExceptT CreateUserSparError (AppT r) CreateUserResult createUserSpar new = do @@ -296,13 +290,9 @@ createUserSpar new = do -- docs/reference/user/registration.md {#RefRegistration} createUser :: forall r p. - ( Members - '[ BlacklistStore, - GalleyProvider, - UserPendingActivationStore p - ] - r, - CallsFed 'Brig "on-user-deleted-connections" + ( Member BlacklistStore r, + Member GalleyProvider r, + Member (UserPendingActivationStore p) r ) => NewUser -> ExceptT RegisterError (AppT r) CreateUserResult @@ -541,12 +531,9 @@ initAccountFeatureConfig uid = do -- all over the place there, we add a new function that handles just the one new flow where -- users are invited to the team via scim. createUserInviteViaScim :: - Members - '[ BlacklistStore, - UserPendingActivationStore p, - GalleyProvider - ] - r => + ( Member BlacklistStore r, + Member (UserPendingActivationStore p) r + ) => UserId -> NewUserScimInvitation -> ExceptT Error.Error (AppT r) UserAccount @@ -587,7 +574,7 @@ checkRestrictedUserCreation new = do ------------------------------------------------------------------------------- -- Update Profile -updateUser :: CallsFed 'Brig "on-user-deleted-connections" => UserId -> Maybe ConnId -> UserUpdate -> AllowSCIMUpdates -> ExceptT UpdateProfileError (AppT r) () +updateUser :: UserId -> Maybe ConnId -> UserUpdate -> AllowSCIMUpdates -> ExceptT UpdateProfileError (AppT r) () updateUser uid mconn uu allowScim = do for_ (uupName uu) $ \newName -> do mbUser <- lift . wrapClient $ Data.lookupUser WithPendingInvitations uid @@ -605,7 +592,7 @@ updateUser uid mconn uu allowScim = do ------------------------------------------------------------------------------- -- Update Locale -changeLocale :: CallsFed 'Brig "on-user-deleted-connections" => UserId -> ConnId -> LocaleUpdate -> (AppT r) () +changeLocale :: UserId -> ConnId -> LocaleUpdate -> (AppT r) () changeLocale uid conn (LocaleUpdate loc) = do wrapClient $ Data.updateLocale uid loc wrapHttpClient $ Intra.onUserEvent uid (Just conn) (localeUpdate uid loc) @@ -613,7 +600,7 @@ changeLocale uid conn (LocaleUpdate loc) = do ------------------------------------------------------------------------------- -- Update ManagedBy -changeManagedBy :: CallsFed 'Brig "on-user-deleted-connections" => UserId -> ConnId -> ManagedByUpdate -> (AppT r) () +changeManagedBy :: UserId -> ConnId -> ManagedByUpdate -> (AppT r) () changeManagedBy uid conn (ManagedByUpdate mb) = do wrapClient $ Data.updateManagedBy uid mb wrapHttpClient $ Intra.onUserEvent uid (Just conn) (managedByUpdate uid mb) @@ -621,7 +608,7 @@ changeManagedBy uid conn (ManagedByUpdate mb) = do -------------------------------------------------------------------------------- -- Change Handle -changeHandle :: CallsFed 'Brig "on-user-deleted-connections" => UserId -> Maybe ConnId -> Handle -> AllowSCIMUpdates -> ExceptT ChangeHandleError (AppT r) () +changeHandle :: UserId -> Maybe ConnId -> Handle -> AllowSCIMUpdates -> ExceptT ChangeHandleError (AppT r) () changeHandle uid mconn hdl allowScim = do when (isBlacklistedHandle hdl) $ throwE ChangeHandleInvalid @@ -747,11 +734,9 @@ changeEmail u email allowScim = do -- Change Phone changePhone :: - Members - '[ BlacklistStore, - BlacklistPhonePrefixStore - ] - r => + ( Member BlacklistStore r, + Member BlacklistPhonePrefixStore r + ) => UserId -> Phone -> ExceptT ChangePhoneError (AppT r) (Activation, Phone) @@ -779,7 +764,7 @@ changePhone u phone = do ------------------------------------------------------------------------------- -- Remove Email -removeEmail :: CallsFed 'Brig "on-user-deleted-connections" => UserId -> ConnId -> ExceptT RemoveIdentityError (AppT r) () +removeEmail :: UserId -> ConnId -> ExceptT RemoveIdentityError (AppT r) () removeEmail uid conn = do ident <- lift $ fetchUserIdentity uid case ident of @@ -793,7 +778,7 @@ removeEmail uid conn = do ------------------------------------------------------------------------------- -- Remove Phone -removePhone :: CallsFed 'Brig "on-user-deleted-connections" => UserId -> ConnId -> ExceptT RemoveIdentityError (AppT r) () +removePhone :: UserId -> ConnId -> ExceptT RemoveIdentityError (AppT r) () removePhone uid conn = do ident <- lift $ fetchUserIdentity uid case ident of @@ -811,7 +796,7 @@ removePhone uid conn = do ------------------------------------------------------------------------------- -- Forcefully revoke a verified identity -revokeIdentity :: CallsFed 'Brig "on-user-deleted-connections" => Either Email Phone -> AppT r () +revokeIdentity :: Either Email Phone -> AppT r () revokeIdentity key = do let uk = either userEmailKey userPhoneKey key mu <- wrapClient $ Data.lookupKey uk @@ -855,8 +840,7 @@ changeAccountStatus :: MonadMask m, MonadHttp m, HasRequestId m, - MonadUnliftIO m, - CallsFed 'Brig "on-user-deleted-connections" + MonadUnliftIO m ) => List1 UserId -> AccountStatus -> @@ -882,8 +866,7 @@ changeSingleAccountStatus :: MonadMask m, MonadHttp m, HasRequestId m, - MonadUnliftIO m, - CallsFed 'Brig "on-user-deleted-connections" + MonadUnliftIO m ) => UserId -> AccountStatus -> @@ -908,7 +891,7 @@ mkUserEvent usrs status = -- Activation activate :: - (Members '[GalleyProvider] r, CallsFed 'Brig "on-user-deleted-connections") => + (Member GalleyProvider r) => ActivationTarget -> ActivationCode -> -- | The user for whom to activate the key. @@ -917,7 +900,7 @@ activate :: activate tgt code usr = activateWithCurrency tgt code usr Nothing activateWithCurrency :: - (Members '[GalleyProvider] r, CallsFed 'Brig "on-user-deleted-connections") => + (Member GalleyProvider r) => ActivationTarget -> ActivationCode -> -- | The user for whom to activate the key. @@ -948,8 +931,7 @@ activateWithCurrency tgt code usr cur = do preverify :: ( MonadClient m, - MonadReader Env m, - CallsFed 'Brig "on-user-deleted-connections" + MonadReader Env m ) => ActivationTarget -> ActivationCode -> @@ -958,7 +940,7 @@ preverify tgt code = do key <- mkActivationKey tgt void $ Data.verifyCode key code -onActivated :: CallsFed 'Brig "on-user-deleted-connections" => ActivationEvent -> (AppT r) (UserId, Maybe UserIdentity, Bool) +onActivated :: ActivationEvent -> (AppT r) (UserId, Maybe UserIdentity, Bool) onActivated (AccountActivated account) = do let uid = userId (accountUser account) Log.debug $ field "user" (toByteString uid) . field "action" (Log.val "User.onActivated") @@ -975,12 +957,10 @@ onActivated (PhoneActivated uid phone) = do -- docs/reference/user/activation.md {#RefActivationRequest} sendActivationCode :: - Members - '[ BlacklistStore, - BlacklistPhonePrefixStore, - GalleyProvider - ] - r => + ( Member BlacklistStore r, + Member BlacklistPhonePrefixStore r, + Member GalleyProvider r + ) => Either Email Phone -> Maybe Locale -> Bool -> @@ -1104,7 +1084,7 @@ changePassword uid cp = do lift $ wrapClient (Data.updatePassword uid newpw) >> wrapClient (revokeAllCookies uid) beginPasswordReset :: - Members '[PasswordResetStore] r => + Member PasswordResetStore r => Either Email Phone -> ExceptT PasswordResetError (AppT r) (UserId, PasswordResetPair) beginPasswordReset target = do @@ -1120,7 +1100,9 @@ beginPasswordReset target = do (user,) <$> lift (liftSem $ E.createPasswordResetCode user target) completePasswordReset :: - Members '[CodeStore, PasswordResetStore] r => + ( Member CodeStore r, + Member PasswordResetStore r + ) => PasswordResetIdentity -> PasswordResetCode -> PlainTextPassword -> @@ -1148,7 +1130,7 @@ checkNewIsDifferent uid pw = do _ -> pure () mkPasswordResetKey :: - Members '[CodeStore] r => + Member CodeStore r => PasswordResetIdentity -> ExceptT PasswordResetError (AppT r) PasswordResetKey mkPasswordResetKey ident = case ident of @@ -1175,12 +1157,7 @@ mkPasswordResetKey ident = case ident of -- TODO: communicate deletions of SSO users to SSO service. deleteSelfUser :: forall r. - ( Members - '[ GalleyProvider - ] - r, - CallsFed 'Brig "on-user-deleted-connections" - ) => + (Member GalleyProvider r) => UserId -> Maybe PlainTextPassword -> ExceptT DeleteUserError (AppT r) (Maybe Timeout) @@ -1256,7 +1233,7 @@ deleteSelfUser uid pwd = do -- | Conclude validation and scheduling of user's deletion request that was initiated in -- 'deleteUser'. Called via @post /delete@. -verifyDeleteUser :: CallsFed 'Brig "on-user-deleted-connections" => VerifyDeleteUser -> ExceptT DeleteUserError (AppT r) () +verifyDeleteUser :: VerifyDeleteUser -> ExceptT DeleteUserError (AppT r) () verifyDeleteUser d = do let key = verifyDeleteUserKey d let code = verifyDeleteUserCode d @@ -1270,18 +1247,13 @@ verifyDeleteUser d = do -- Called via @delete /i/user/:uid@. ensureAccountDeleted :: ( MonadLogger m, - MonadCatch m, - MonadThrow m, MonadIndexIO m, - MonadReader Env m, - MonadIO m, MonadMask m, MonadHttp m, HasRequestId m, MonadUnliftIO m, MonadClient m, - MonadReader Env m, - CallsFed 'Brig "on-user-deleted-connections" + MonadReader Env m ) => UserId -> m DeleteUserResult @@ -1326,8 +1298,7 @@ deleteAccount :: MonadHttp m, HasRequestId m, MonadUnliftIO m, - MonadClient m, - CallsFed 'Brig "on-user-deleted-connections" + MonadClient m ) => UserAccount -> m () @@ -1372,7 +1343,7 @@ deleteAccount account@(accountUser -> user) = do -- Lookups lookupActivationCode :: - (MonadIO m, MonadClient m) => + MonadClient m => Either Email Phone -> m (Maybe ActivationPair) lookupActivationCode emailOrPhone = do @@ -1382,7 +1353,9 @@ lookupActivationCode emailOrPhone = do pure $ (k,) <$> c lookupPasswordResetCode :: - Members '[CodeStore, PasswordResetStore] r => + ( Member CodeStore r, + Member PasswordResetStore r + ) => Either Email Phone -> (AppT r) (Maybe PasswordResetPair) lookupPasswordResetCode emailOrPhone = do @@ -1434,7 +1407,7 @@ userGC u = case userExpire u of pure u lookupProfile :: - (Members '[GalleyProvider] r, CallsFed 'Brig "get-users-by-ids") => + (Member GalleyProvider r) => Local UserId -> Qualified UserId -> ExceptT FederationError (AppT r) (Maybe UserProfile) @@ -1450,12 +1423,8 @@ lookupProfile self other = -- Otherwise only the 'PublicProfile' is accessible for user 'self'. -- If 'self' is an unknown 'UserId', return '[]'. lookupProfiles :: - ( Members - '[ GalleyProvider, - Concurrency 'Unsafe - ] - r, - CallsFed 'Brig "get-users-by-ids" + ( Member GalleyProvider r, + Member (Concurrency 'Unsafe) r ) => -- | User 'self' on whose behalf the profiles are requested. Local UserId -> @@ -1469,7 +1438,7 @@ lookupProfiles self others = (bucketQualified others) lookupProfilesFromDomain :: - (Members '[GalleyProvider] r, CallsFed 'Brig "get-users-by-ids") => + (Member GalleyProvider r) => Local UserId -> Qualified [UserId] -> ExceptT FederationError (AppT r) [UserProfile] @@ -1482,8 +1451,7 @@ lookupProfilesFromDomain self = lookupRemoteProfiles :: ( MonadIO m, MonadReader Env m, - MonadLogger m, - CallsFed 'Brig "get-users-by-ids" + MonadLogger m ) => Remote [UserId] -> ExceptT FederationError m [UserProfile] @@ -1495,7 +1463,7 @@ lookupRemoteProfiles (tUntagged -> Qualified uids domain) = -- pure function and writing tests for that. lookupLocalProfiles :: forall r. - Members '[GalleyProvider] r => + Member GalleyProvider r => -- | This is present only when an authenticated user is requesting access. Maybe UserId -> -- | The users ('others') for which to obtain the profiles. @@ -1540,13 +1508,13 @@ lookupLocalProfiles requestingUser others = do in baseProfile {profileEmail = profileEmail'} getLegalHoldStatus :: - Members '[GalleyProvider] r => + Member GalleyProvider r => UserId -> AppT r (Maybe UserLegalHoldStatus) getLegalHoldStatus uid = traverse (liftSem . getLegalHoldStatus' . accountUser) =<< wrapHttpClient (lookupAccount uid) getLegalHoldStatus' :: - Members '[GalleyProvider] r => + Member GalleyProvider r => User -> Sem r UserLegalHoldStatus getLegalHoldStatus' user = diff --git a/services/brig/src/Brig/Allowlists.hs b/services/brig/src/Brig/Allowlists.hs new file mode 100644 index 0000000000..5b33b853a7 --- /dev/null +++ b/services/brig/src/Brig/Allowlists.hs @@ -0,0 +1,50 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +-- | > docs/reference/user/activation.md {#RefActivationAllowlist} +-- +-- Email/phone whitelist. +module Brig.Allowlists + ( AllowlistEmailDomains (..), + AllowlistPhonePrefixes (..), + verify, + ) +where + +import Data.Aeson +import qualified Data.Text as Text +import Imports +import Wire.API.User.Identity + +-- | A service providing a whitelist of allowed email addresses and phone numbers +data AllowlistEmailDomains = AllowlistEmailDomains [Text] + deriving (Show, Generic) + +instance FromJSON AllowlistEmailDomains + +data AllowlistPhonePrefixes = AllowlistPhonePrefixes [Text] + deriving (Show, Generic) + +instance FromJSON AllowlistPhonePrefixes + +-- | Consult the whitelist settings in brig's config file and verify that the provided +-- email/phone address is whitelisted. +verify :: Maybe AllowlistEmailDomains -> Maybe AllowlistPhonePrefixes -> Either Email Phone -> Bool +verify (Just (AllowlistEmailDomains allowed)) _ (Left email) = emailDomain email `elem` allowed +verify _ (Just (AllowlistPhonePrefixes allowed)) (Right phone) = any (`Text.isPrefixOf` fromPhone phone) allowed +verify Nothing _ (Left _) = True +verify _ Nothing (Right _) = True diff --git a/services/brig/src/Brig/Data/Activation.hs b/services/brig/src/Brig/Data/Activation.hs index 0bbb97b587..42f7e52222 100644 --- a/services/brig/src/Brig/Data/Activation.hs +++ b/services/brig/src/Brig/Data/Activation.hs @@ -143,7 +143,7 @@ activateKey k c u = verifyCode k c >>= pickUser >>= activate -- | Create a new pending activation for a given 'UserKey'. newActivation :: - (MonadIO m, MonadClient m) => + MonadClient m => UserKey -> -- | The timeout for the activation code. Timeout -> diff --git a/services/brig/src/Brig/Data/Nonce.hs b/services/brig/src/Brig/Data/Nonce.hs index 9653ea4313..83f11d1b78 100644 --- a/services/brig/src/Brig/Data/Nonce.hs +++ b/services/brig/src/Brig/Data/Nonce.hs @@ -21,7 +21,6 @@ module Brig.Data.Nonce ) where -import Brig.App (Env) import Brig.Data.Instances () import Cassandra import Control.Lens hiding (from) @@ -30,7 +29,7 @@ import Data.Nonce (Nonce, NonceTtlSecs) import Imports insertNonce :: - (MonadClient m, MonadReader Brig.App.Env m) => + MonadClient m => NonceTtlSecs -> UserId -> Text -> @@ -42,14 +41,14 @@ insertNonce ttl uid key nonce = retry x5 . write insert $ params LocalQuorum (ui insert = "INSERT INTO nonce (user, key, nonce) VALUES (?, ?, ?) USING TTL ?" lookupAndDeleteNonce :: - (MonadClient m, MonadReader Env m) => + MonadClient m => UserId -> Text -> m (Maybe Nonce) lookupAndDeleteNonce uid key = lookupNonce uid key <* deleteNonce uid key lookupNonce :: - (MonadClient m, MonadReader Env m) => + MonadClient m => UserId -> Text -> m (Maybe Nonce) @@ -59,7 +58,7 @@ lookupNonce uid key = (runIdentity <$$>) . retry x5 . query1 get $ params LocalQ get = "SELECT nonce FROM nonce WHERE user = ? AND key = ?" deleteNonce :: - (MonadClient m, MonadReader Env m) => + MonadClient m => UserId -> Text -> m () diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 7aa3adbb82..947eb5ee23 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -160,7 +160,7 @@ newAccount u inv tid mbHandle = do managedBy = fromMaybe defaultManagedBy (newUserManagedBy u) user uid domain l e = User uid (Qualified uid domain) ident name pict assets colour False l Nothing mbHandle e tid managedBy -newAccountInviteViaScim :: (MonadClient m, MonadReader Env m) => UserId -> TeamId -> Maybe Locale -> Name -> Email -> m UserAccount +newAccountInviteViaScim :: MonadReader Env m => UserId -> TeamId -> Maybe Locale -> Name -> Email -> m UserAccount newAccountInviteViaScim uid tid locale name email = do defLoc <- setDefaultUserLocale <$> view settings let loc = fromMaybe defLoc locale diff --git a/services/brig/src/Brig/Data/UserKey.hs b/services/brig/src/Brig/Data/UserKey.hs index 4afa9222c5..999acb043f 100644 --- a/services/brig/src/Brig/Data/UserKey.hs +++ b/services/brig/src/Brig/Data/UserKey.hs @@ -33,7 +33,6 @@ module Brig.Data.UserKey ) where -import Brig.App (Env) import Brig.Data.Instances () import qualified Brig.Data.User as User import Brig.Email @@ -83,7 +82,7 @@ keyTextOriginal (UserPhoneKey k) = fromPhone (phoneKeyOrig k) -- | Claim a 'UserKey' for a user. claimKey :: - (MonadClient m, MonadReader Env m) => + MonadClient m => -- | The key to claim. UserKey -> -- | The user claiming the key. @@ -116,11 +115,11 @@ lookupKey k = fmap runIdentity <$> retry x1 (query1 keySelect (params LocalQuorum (Identity $ keyText k))) -insertKey :: (MonadClient m, MonadReader Env m) => UserId -> UserKey -> m () +insertKey :: MonadClient m => UserId -> UserKey -> m () insertKey u k = do retry x5 $ write keyInsert (params LocalQuorum (keyText k, u)) -deleteKey :: (MonadClient m, MonadReader Env m) => UserKey -> m () +deleteKey :: MonadClient m => UserKey -> m () deleteKey k = do retry x5 $ write keyDelete (params LocalQuorum (Identity $ keyText k)) @@ -132,7 +131,7 @@ deleteKey k = do -- executed several times due to cassandra not supporting transactions) -- `deleteKeyForUser` does not fail for missing keys or keys that belong to -- another user: It always returns `()` as result. -deleteKeyForUser :: (MonadClient m, MonadReader Env m) => UserId -> UserKey -> m () +deleteKeyForUser :: MonadClient m => UserId -> UserKey -> m () deleteKeyForUser uid k = do mbKeyUid <- lookupKey k case mbKeyUid of diff --git a/services/brig/src/Brig/Effects/GalleyProvider.hs b/services/brig/src/Brig/Effects/GalleyProvider.hs index afbe6b5875..2457b026ef 100644 --- a/services/brig/src/Brig/Effects/GalleyProvider.hs +++ b/services/brig/src/Brig/Effects/GalleyProvider.hs @@ -1,3 +1,19 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 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 TemplateHaskell #-} module Brig.Effects.GalleyProvider where @@ -8,11 +24,11 @@ import qualified Data.Currency as Currency import Data.Id import Data.Json.Util (UTCTimeMillis) import Data.Qualified -import qualified Galley.Types.Teams.Intra as Team import Imports import qualified Network.Wai.Utilities.Error as Wai import Polysemy import Wire.API.Conversation +import qualified Wire.API.Routes.Internal.Galley.TeamsIntra as Team import Wire.API.Team import qualified Wire.API.Team.Conversation as Conv import Wire.API.Team.Feature diff --git a/services/brig/src/Brig/Effects/GalleyProvider/RPC.hs b/services/brig/src/Brig/Effects/GalleyProvider/RPC.hs index a3cb6c2e37..65ee53fead 100644 --- a/services/brig/src/Brig/Effects/GalleyProvider/RPC.hs +++ b/services/brig/src/Brig/Effects/GalleyProvider/RPC.hs @@ -1,3 +1,19 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 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-unused-matches #-} module Brig.Effects.GalleyProvider.RPC where @@ -21,7 +37,6 @@ import Data.Json.Util (UTCTimeMillis) import Data.Qualified import Data.Range import qualified Galley.Types.Teams as Team -import qualified Galley.Types.Teams.Intra as Team import Imports import Network.HTTP.Types.Method import Network.HTTP.Types.Status @@ -31,6 +46,7 @@ import Polysemy.Error import Servant.API (toHeader) import System.Logger (Msg, field, msg, val) import Wire.API.Conversation hiding (Member) +import qualified Wire.API.Routes.Internal.Galley.TeamsIntra as Team import Wire.API.Routes.Version import Wire.API.Team import qualified Wire.API.Team.Conversation as Conv @@ -42,12 +58,10 @@ import Wire.API.Team.SearchVisibility import Wire.Sem.Logger interpretGalleyProviderToRPC :: - Members - '[ Error ParseException, - ServiceRPC 'Galley, - Logger (Msg -> Msg) - ] - r => + ( Member (Error ParseException) r, + Member (ServiceRPC 'Galley) r, + Member (Logger (Msg -> Msg)) r + ) => Sem (GalleyProvider ': r) a -> Sem r a interpretGalleyProviderToRPC = interpret $ \case @@ -73,11 +87,9 @@ interpretGalleyProviderToRPC = interpret $ \case -- | Calls 'Galley.API.createSelfConversationH'. createSelfConv :: - Members - '[ ServiceRPC 'Galley, - Logger (Msg -> Msg) - ] - r => + ( Member (ServiceRPC 'Galley) r, + Member (Logger (Msg -> Msg)) r + ) => UserId -> Sem r () createSelfConv u = do @@ -93,12 +105,10 @@ createSelfConv u = do -- | Calls 'Galley.API.getConversationH'. getConv :: - Members - '[ Error ParseException, - ServiceRPC 'Galley, - Logger (Msg -> Msg) - ] - r => + ( Member (Error ParseException) r, + Member (ServiceRPC 'Galley) r, + Member (Logger (Msg -> Msg)) r + ) => UserId -> Local ConvId -> Sem r (Maybe Conversation) @@ -125,12 +135,10 @@ getConv usr lcnv = do -- | Calls 'Galley.API.getTeamConversationH'. getTeamConv :: - Members - '[ Error ParseException, - ServiceRPC 'Galley, - Logger (Msg -> Msg) - ] - r => + ( Member (Error ParseException) r, + Member (ServiceRPC 'Galley) r, + Member (Logger (Msg -> Msg)) r + ) => UserId -> TeamId -> ConvId -> @@ -158,11 +166,9 @@ getTeamConv usr tid cnv = do -- | Calls 'Galley.API.addClientH'. newClient :: - Members - '[ ServiceRPC 'Galley, - Logger (Msg -> Msg) - ] - r => + ( Member (ServiceRPC 'Galley) r, + Member (Logger (Msg -> Msg)) r + ) => UserId -> ClientId -> Sem r () @@ -177,11 +183,9 @@ newClient u c = do -- | Calls 'Galley.API.canUserJoinTeamH'. checkUserCanJoinTeam :: - Members - '[ ServiceRPC 'Galley, - Logger (Msg -> Msg) - ] - r => + ( Member (ServiceRPC 'Galley) r, + Member (Logger (Msg -> Msg)) r + ) => TeamId -> Sem r (Maybe Wai.Error) checkUserCanJoinTeam tid = do @@ -201,11 +205,9 @@ checkUserCanJoinTeam tid = do -- | Calls 'Galley.API.uncheckedAddTeamMemberH'. addTeamMember :: - Members - '[ ServiceRPC 'Galley, - Logger (Msg -> Msg) - ] - r => + ( Member (ServiceRPC 'Galley) r, + Member (Logger (Msg -> Msg)) r + ) => UserId -> TeamId -> (Maybe (UserId, UTCTimeMillis), Role) -> @@ -230,11 +232,9 @@ addTeamMember u tid (minvmeta, role) = do -- | Calls 'Galley.API.createBindingTeamH'. createTeam :: - Members - '[ ServiceRPC 'Galley, - Logger (Msg -> Msg) - ] - r => + ( Member (ServiceRPC 'Galley) r, + Member (Logger (Msg -> Msg)) r + ) => UserId -> BindingNewTeam -> TeamId -> @@ -259,12 +259,10 @@ createTeam u t@(BindingNewTeam bt) teamid = do -- | Calls 'Galley.API.uncheckedGetTeamMemberH'. getTeamMember :: - Members - '[ Error ParseException, - ServiceRPC 'Galley, - Logger (Msg -> Msg) - ] - r => + ( Member (Error ParseException) r, + Member (ServiceRPC 'Galley) r, + Member (Logger (Msg -> Msg)) r + ) => UserId -> TeamId -> Sem r (Maybe Team.TeamMember) @@ -288,12 +286,10 @@ getTeamMember u tid = do -- means that only the first 2000 members of a team (according to some arbitrary order) will -- be suspended, and the rest will remain active. getTeamMembers :: - Members - '[ Error ParseException, - ServiceRPC 'Galley, - Logger (Msg -> Msg) - ] - r => + ( Member (Error ParseException) r, + Member (ServiceRPC 'Galley) r, + Member (Logger (Msg -> Msg)) r + ) => TeamId -> Sem r Team.TeamMemberList getTeamMembers tid = do @@ -305,11 +301,7 @@ getTeamMembers tid = do . expect2xx memberIsTeamOwner :: - Members - '[ ServiceRPC 'Galley, - Logger (Msg -> Msg) - ] - r => + Member (ServiceRPC 'Galley) r => TeamId -> UserId -> Sem r Bool @@ -321,12 +313,10 @@ memberIsTeamOwner tid uid = do -- | Calls 'Galley.API.getBindingTeamIdH'. getTeamId :: - Members - '[ Error ParseException, - ServiceRPC 'Galley, - Logger (Msg -> Msg) - ] - r => + ( Member (Error ParseException) r, + Member (ServiceRPC 'Galley) r, + Member (Logger (Msg -> Msg)) r + ) => UserId -> Sem r (Maybe TeamId) getTeamId u = do @@ -342,12 +332,10 @@ getTeamId u = do -- | Calls 'Galley.API.getTeamInternalH'. getTeam :: - Members - '[ Error ParseException, - ServiceRPC 'Galley, - Logger (Msg -> Msg) - ] - r => + ( Member (Error ParseException) r, + Member (ServiceRPC 'Galley) r, + Member (Logger (Msg -> Msg)) r + ) => TeamId -> Sem r Team.TeamData getTeam tid = do @@ -360,12 +348,10 @@ getTeam tid = do -- | Calls 'Galley.API.getTeamInternalH'. getTeamName :: - Members - '[ Error ParseException, - ServiceRPC 'Galley, - Logger (Msg -> Msg) - ] - r => + ( Member (Error ParseException) r, + Member (ServiceRPC 'Galley) r, + Member (Logger (Msg -> Msg)) r + ) => TeamId -> Sem r Team.TeamName getTeamName tid = do @@ -378,12 +364,10 @@ getTeamName tid = do -- | Calls 'Galley.API.getTeamFeatureStatusH'. getTeamLegalHoldStatus :: - Members - '[ Error ParseException, - ServiceRPC 'Galley, - Logger (Msg -> Msg) - ] - r => + ( Member (Error ParseException) r, + Member (ServiceRPC 'Galley) r, + Member (Logger (Msg -> Msg)) r + ) => TeamId -> Sem r (WithStatus LegalholdConfig) getTeamLegalHoldStatus tid = do @@ -396,12 +380,10 @@ getTeamLegalHoldStatus tid = do -- | Calls 'Galley.API.getSearchVisibilityInternalH'. getTeamSearchVisibility :: - Members - '[ Error ParseException, - ServiceRPC 'Galley, - Logger (Msg -> Msg) - ] - r => + ( Member (Error ParseException) r, + Member (ServiceRPC 'Galley) r, + Member (Logger (Msg -> Msg)) r + ) => TeamId -> Sem r TeamSearchVisibility getTeamSearchVisibility tid = @@ -414,12 +396,10 @@ getTeamSearchVisibility tid = . expect2xx getVerificationCodeEnabled :: - Members - '[ Error ParseException, - ServiceRPC 'Galley, - Logger (Msg -> Msg) - ] - r => + ( Member (Error ParseException) r, + Member (ServiceRPC 'Galley) r, + Member (Logger (Msg -> Msg)) r + ) => TeamId -> Sem r Bool getVerificationCodeEnabled tid = do @@ -447,11 +427,7 @@ decodeBodyMaybe :: (Typeable a, FromJSON a) => Text -> Response (Maybe BL.ByteSt decodeBodyMaybe t r = hush $ decodeBody t r getAllFeatureConfigsForUser :: - Members - '[ ServiceRPC 'Galley, - Logger (Msg -> Msg) - ] - r => + Member (ServiceRPC 'Galley) r => Maybe UserId -> Sem r AllFeatureConfigs getAllFeatureConfigsForUser mbUserId = @@ -464,11 +440,9 @@ getAllFeatureConfigsForUser mbUserId = -- | Calls 'Galley.API.updateTeamStatusH'. changeTeamStatus :: - Members - '[ ServiceRPC 'Galley, - Logger (Msg -> Msg) - ] - r => + ( Member (ServiceRPC 'Galley) r, + Member (Logger (Msg -> Msg)) r + ) => TeamId -> Team.TeamStatus -> Maybe Currency.Alpha -> @@ -484,12 +458,10 @@ changeTeamStatus tid s cur = do . lbytes (encode $ Team.TeamStatusUpdate s cur) getTeamExposeInvitationURLsToTeamAdmin :: - Members - '[ ServiceRPC 'Galley, - Error ParseException, - Logger (Msg -> Msg) - ] - r => + ( Member (ServiceRPC 'Galley) r, + Member (Error ParseException) r, + Member (Logger (Msg -> Msg)) r + ) => TeamId -> Sem r ShowOrHideInvitationUrl getTeamExposeInvitationURLsToTeamAdmin tid = do diff --git a/services/brig/src/Brig/Effects/JwtTools.hs b/services/brig/src/Brig/Effects/JwtTools.hs index b345fcb2cf..3901602fac 100644 --- a/services/brig/src/Brig/Effects/JwtTools.hs +++ b/services/brig/src/Brig/Effects/JwtTools.hs @@ -46,9 +46,9 @@ data JwtTools m a where makeSem ''JwtTools -interpretJwtTools :: Members '[Embed IO] r => Sem (JwtTools ': r) a -> Sem r a +interpretJwtTools :: Member (Embed IO) r => Sem (JwtTools ': r) a -> Sem r a interpretJwtTools = interpret $ \(GenerateDPoPAccessToken pr ci n uri method skew ex now pem) -> do - case readHex @Word16 (cs $ client $ ciClient ci) of + case readHex @Word64 (cs $ client $ ciClient ci) of [(parsedClientId, "")] -> mapLeft RustError <$> runExceptT diff --git a/services/brig/src/Brig/Effects/PasswordResetStore/CodeStore.hs b/services/brig/src/Brig/Effects/PasswordResetStore/CodeStore.hs index c0248aa4e5..22f2b1bf45 100644 --- a/services/brig/src/Brig/Effects/PasswordResetStore/CodeStore.hs +++ b/services/brig/src/Brig/Effects/PasswordResetStore/CodeStore.hs @@ -34,7 +34,9 @@ import qualified Wire.Sem.Now as Now passwordResetStoreToCodeStore :: forall r a. - Members '[CodeStore, Now] r => + ( Member CodeStore r, + Member Now r + ) => Sem (PasswordResetStore ': r) a -> Sem r a passwordResetStoreToCodeStore = interpret $ \case @@ -49,7 +51,9 @@ ttl :: NominalDiffTime ttl = 3600 -- 60 minutes create :: - Members '[CodeStore, Now] r => + ( Member CodeStore r, + Member Now r + ) => UserId -> Either Email Phone -> Sem r PasswordResetPair @@ -64,7 +68,9 @@ create u target = do pure (key, code) lookup :: - Members '[CodeStore, Now] r => + ( Member CodeStore r, + Member Now r + ) => UserId -> Sem r (Maybe PasswordResetCode) lookup u = do @@ -76,7 +82,9 @@ lookup u = do validate _ _ = pure Nothing verify :: - Members '[CodeStore, Now] r => + ( Member CodeStore r, + Member Now r + ) => PasswordResetPair -> Sem r (Maybe UserId) verify (k, c) = do diff --git a/services/brig/src/Brig/Effects/PublicKeyBundle.hs b/services/brig/src/Brig/Effects/PublicKeyBundle.hs index 2846bc7a5e..ab236b2566 100644 --- a/services/brig/src/Brig/Effects/PublicKeyBundle.hs +++ b/services/brig/src/Brig/Effects/PublicKeyBundle.hs @@ -14,7 +14,7 @@ data PublicKeyBundle m a where makeSem ''PublicKeyBundle -interpretPublicKeyBundle :: Members '[Embed IO] r => Sem (PublicKeyBundle ': r) a -> Sem r a +interpretPublicKeyBundle :: Member (Embed IO) r => Sem (PublicKeyBundle ': r) a -> Sem r a interpretPublicKeyBundle = interpret $ \(Get fp) -> do contents :: Either IOException ByteString <- liftIO $ try $ BS.readFile fp pure $ either (const Nothing) fromByteString contents diff --git a/services/brig/src/Brig/Effects/RPC/IO.hs b/services/brig/src/Brig/Effects/RPC/IO.hs index 4f1b13d062..8ef33ef80f 100644 --- a/services/brig/src/Brig/Effects/RPC/IO.hs +++ b/services/brig/src/Brig/Effects/RPC/IO.hs @@ -11,7 +11,7 @@ import Control.Monad.Catch import Imports import Polysemy -interpretRpcToIO :: Members '[Final IO] r => Manager -> RequestId -> Sem (RPC ': r) a -> Sem r a +interpretRpcToIO :: Member (Final IO) r => Manager -> RequestId -> Sem (RPC ': r) a -> Sem r a interpretRpcToIO mgr rid = interpret $ \case ServiceRequest txt f sm g -> embedFinal @IO $ viaHttpIO mgr rid $ RPC.serviceRequestImpl txt f sm g diff --git a/services/brig/src/Brig/Effects/SFT.hs b/services/brig/src/Brig/Effects/SFT.hs index de325e810e..c4a4184ceb 100644 --- a/services/brig/src/Brig/Effects/SFT.hs +++ b/services/brig/src/Brig/Effects/SFT.hs @@ -66,7 +66,7 @@ interpretSFT httpManager = interpret $ \(SFTGetAllServers url) -> do debug $ Log.field "URLs" (show res) . Log.msg ("Fetched the following server URLs" :: ByteString) pure res -runSftError :: Members '[TinyLog] r => HttpsUrl -> Sem (Error SFTError : r) a -> Sem r (Either SFTError a) +runSftError :: Member TinyLog r => HttpsUrl -> Sem (Error SFTError : r) a -> Sem r (Either SFTError a) runSftError urlWithPath act = runError $ act diff --git a/services/brig/src/Brig/Email.hs b/services/brig/src/Brig/Email.hs index 26faabe634..4e38272931 100644 --- a/services/brig/src/Brig/Email.hs +++ b/services/brig/src/Brig/Email.hs @@ -43,14 +43,13 @@ import qualified Brig.AWS as AWS import Brig.App (Env, applog, awsEnv, smtpEnv) import qualified Brig.SMTP as SMTP import Control.Lens (view) -import Control.Monad.Catch import qualified Data.Text as Text import Imports import Network.Mail.Mime import Wire.API.User ------------------------------------------------------------------------------- -sendMail :: (MonadIO m, MonadCatch m, MonadReader Env m) => Mail -> m () +sendMail :: (MonadIO m, MonadReader Env m) => Mail -> m () sendMail m = view smtpEnv >>= \case Just smtp -> view applog >>= \logger -> SMTP.sendMail logger smtp m diff --git a/services/brig/src/Brig/Federation/Client.hs b/services/brig/src/Brig/Federation/Client.hs index 37eb4924ba..1b38057912 100644 --- a/services/brig/src/Brig/Federation/Client.hs +++ b/services/brig/src/Brig/Federation/Client.hs @@ -47,7 +47,6 @@ import Wire.API.UserMap getUserHandleInfo :: ( MonadReader Env m, MonadIO m, - CallsFed 'Brig "get-user-by-handle", Log.MonadLogger m ) => Remote Handle -> @@ -59,7 +58,6 @@ getUserHandleInfo (tUntagged -> Qualified handle domain) = do getUsersByIds :: ( MonadReader Env m, MonadIO m, - CallsFed 'Brig "get-users-by-ids", Log.MonadLogger m ) => Domain -> @@ -70,7 +68,7 @@ getUsersByIds domain uids = do runBrigFederatorClient domain $ fedClient @'Brig @"get-users-by-ids" uids claimPrekey :: - (MonadReader Env m, MonadIO m, Log.MonadLogger m, CallsFed 'Brig "claim-prekey") => + (MonadReader Env m, MonadIO m, Log.MonadLogger m) => Qualified UserId -> ClientId -> ExceptT FederationError m (Maybe ClientPrekey) @@ -81,7 +79,6 @@ claimPrekey (Qualified user domain) client = do claimPrekeyBundle :: ( MonadReader Env m, MonadIO m, - CallsFed 'Brig "claim-prekey-bundle", Log.MonadLogger m ) => Qualified UserId -> @@ -93,8 +90,7 @@ claimPrekeyBundle (Qualified user domain) = do claimMultiPrekeyBundle :: ( Log.MonadLogger m, MonadReader Env m, - MonadIO m, - CallsFed 'Brig "claim-multi-prekey-bundle" + MonadIO m ) => Domain -> UserClients -> @@ -106,8 +102,7 @@ claimMultiPrekeyBundle domain uc = do searchUsers :: ( MonadReader Env m, MonadIO m, - Log.MonadLogger m, - CallsFed 'Brig "search-users" + Log.MonadLogger m ) => Domain -> SearchRequest -> @@ -119,8 +114,7 @@ searchUsers domain searchTerm = do getUserClients :: ( MonadReader Env m, MonadIO m, - Log.MonadLogger m, - CallsFed 'Brig "get-user-clients" + Log.MonadLogger m ) => Domain -> GetUserClients -> @@ -130,7 +124,7 @@ getUserClients domain guc = do runBrigFederatorClient domain $ fedClient @'Brig @"get-user-clients" guc sendConnectionAction :: - (MonadReader Env m, MonadIO m, Log.MonadLogger m, CallsFed 'Brig "send-connection-action") => + (MonadReader Env m, MonadIO m, Log.MonadLogger m) => Local UserId -> Remote UserId -> RemoteConnectionAction -> diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index 922ef8b67d..3c98f8ab9a 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -86,8 +86,6 @@ import Data.Qualified import Data.Range import qualified Data.Set as Set import GHC.TypeLits -import Galley.Types.Conversations.Intra (UpsertOne2OneConversationRequest, UpsertOne2OneConversationResponse) -import Galley.Types.Teams.Intra (GuardLegalholdPolicyConflicts (GuardLegalholdPolicyConflicts)) import Gundeck.Types.Push.V2 import qualified Gundeck.Types.Push.V2 as Push import Imports @@ -98,10 +96,11 @@ import qualified System.Logger.Extended as ExLog import Wire.API.Connection import Wire.API.Conversation import Wire.API.Event.Conversation (Connect (Connect)) -import Wire.API.Federation.API import Wire.API.Federation.API.Brig import Wire.API.Federation.Error import Wire.API.Properties +import Wire.API.Routes.Internal.Galley.ConversationsIntra (UpsertOne2OneConversationRequest, UpsertOne2OneConversationResponse) +import Wire.API.Routes.Internal.Galley.TeamsIntra (GuardLegalholdPolicyConflicts (GuardLegalholdPolicyConflicts)) import Wire.API.Team.LegalHold (LegalholdProtectee) import qualified Wire.API.Team.Member as Team import Wire.API.User @@ -118,8 +117,7 @@ onUserEvent :: MonadHttp m, HasRequestId m, MonadUnliftIO m, - MonadClient m, - CallsFed 'Brig "on-user-deleted-connections" + MonadClient m ) => UserId -> Maybe ConnId -> @@ -189,7 +187,6 @@ onClientEvent orig conn e = do updateSearchIndex :: ( MonadClient m, - MonadCatch m, MonadLogger m, MonadIndexIO m ) => @@ -244,15 +241,13 @@ journalEvent orig e = case e of -- as well as his other clients about a change to his user account -- or profile. dispatchNotifications :: - ( MonadIO m, - Log.MonadLogger m, + ( Log.MonadLogger m, MonadReader Env m, MonadMask m, MonadHttp m, HasRequestId m, MonadUnliftIO m, - MonadClient m, - CallsFed 'Brig "on-user-deleted-connections" + MonadClient m ) => UserId -> Maybe ConnId -> @@ -281,14 +276,12 @@ dispatchNotifications orig conn e = case e of event = singleton $ UserEvent e notifyUserDeletionLocals :: - ( MonadIO m, - Log.MonadLogger m, + ( Log.MonadLogger m, MonadReader Env m, MonadMask m, MonadHttp m, HasRequestId m, MonadUnliftIO m, - CallsFed 'Brig "on-user-deleted-connections", MonadClient m ) => UserId -> @@ -303,8 +296,7 @@ notifyUserDeletionRemotes :: forall m. ( MonadReader Env m, MonadClient m, - MonadLogger m, - CallsFed 'Brig "on-user-deleted-connections" + MonadLogger m ) => UserId -> m () @@ -423,8 +415,7 @@ rawPush (toList -> events) usrs orig route conn = do -- | (Asynchronously) notifies other users of events. notify :: - ( MonadIO m, - Log.MonadLogger m, + ( Log.MonadLogger m, MonadReader Env m, MonadMask m, MonadHttp m, @@ -446,7 +437,7 @@ notify events orig route conn recipients = fork (Just orig) $ do push events rs orig route conn fork :: - (MonadIO m, MonadUnliftIO m, MonadReader Env m) => + (MonadUnliftIO m, MonadReader Env m) => Maybe UserId -> m a -> m () @@ -463,8 +454,7 @@ fork u ma = do user = maybe id (field "user" . toByteString) notifySelf :: - ( MonadIO m, - Log.MonadLogger m, + ( Log.MonadLogger m, MonadReader Env m, MonadMask m, MonadHttp m, @@ -508,14 +498,14 @@ notifyContacts events orig route conn = do contacts = lookupContactList orig teamContacts :: m [UserId] - teamContacts = screenMemberList =<< getTeamContacts orig + teamContacts = screenMemberList <$> getTeamContacts orig -- If we have a truncated team, we just ignore it all together to avoid very large fanouts -- - screenMemberList :: Maybe Team.TeamMemberList -> m [UserId] + screenMemberList :: Maybe Team.TeamMemberList -> [UserId] screenMemberList (Just mems) | mems ^. Team.teamMemberListType == Team.ListComplete = - pure $ fmap (view Team.userId) (mems ^. Team.teamMembers) - screenMemberList _ = pure [] + view Team.userId <$> mems ^. Team.teamMembers + screenMemberList _ = [] -- Event Serialisation: diff --git a/services/brig/src/Brig/Index/Migrations.hs b/services/brig/src/Brig/Index/Migrations.hs index e022aac8ed..a3037b5e0b 100644 --- a/services/brig/src/Brig/Index/Migrations.hs +++ b/services/brig/src/Brig/Index/Migrations.hs @@ -148,7 +148,7 @@ persistVersion v = . Log.field "migrationVersion" v else throwM $ PersistVersionFailed v $ show persistResponse -latestMigrationVersion :: (Monad m, MonadThrow m, MonadIO m) => MigrationActionT m MigrationVersion +latestMigrationVersion :: (MonadThrow m, MonadIO m) => MigrationActionT m MigrationVersion latestMigrationVersion = do resp <- ES.parseEsResponse =<< ES.searchByIndex indexName (ES.mkSearch Nothing Nothing) result <- either (throwM . FetchMigrationVersionsFailed . show) pure resp diff --git a/services/brig/src/Brig/Index/Options.hs b/services/brig/src/Brig/Index/Options.hs index c614172794..af164ff20f 100644 --- a/services/brig/src/Brig/Index/Options.hs +++ b/services/brig/src/Brig/Index/Options.hs @@ -146,7 +146,18 @@ elasticServerParser = restrictedElasticSettingsParser :: Parser ElasticSettings restrictedElasticSettingsParser = do server <- elasticServerParser - pure $ localElasticSettings & esServer .~ server + prefix <- + strOption + ( long "elasticsearch-index-prefix" + <> metavar "PREFIX" + <> help "Elasticsearch Index Prefix. The actual index name will be PREFIX_test." + <> value "directory" + <> showDefault + ) + pure $ + localElasticSettings + & esServer .~ server + & esIndex .~ ES.IndexName (prefix <> "_test") indexNameParser :: Parser ES.IndexName indexNameParser = diff --git a/services/brig/src/Brig/InternalEvent/Process.hs b/services/brig/src/Brig/InternalEvent/Process.hs index 31bbf7076a..f9082c8c5b 100644 --- a/services/brig/src/Brig/InternalEvent/Process.hs +++ b/services/brig/src/Brig/InternalEvent/Process.hs @@ -39,22 +39,19 @@ import Imports import System.Logger.Class (field, msg, val, (~~)) import qualified System.Logger.Class as Log import UnliftIO (timeout) -import Wire.API.Federation.API -- | Handle an internal event. -- -- Has a one-minute timeout that should be enough for anything that it does. onEvent :: ( Log.MonadLogger m, - MonadCatch m, MonadIndexIO m, MonadReader Env m, MonadMask m, MonadHttp m, HasRequestId m, MonadUnliftIO m, - MonadClient m, - CallsFed 'Brig "on-user-deleted-connections" + MonadClient m ) => InternalNotification -> m () diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 941a683aba..71b3c09dfe 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -1,6 +1,8 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE TemplateHaskell #-} +-- Disabling to stop errors on Getters +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- @@ -21,10 +23,10 @@ module Brig.Options where +import Brig.Allowlists (AllowlistEmailDomains (..), AllowlistPhonePrefixes (..)) import Brig.Queue.Types (Queue (..)) import Brig.SMTP (SMTPConnType (..)) import Brig.User.Auth.Cookie.Limit -import Brig.Whitelist (Whitelist (..)) import qualified Brig.ZAuth as ZAuth import Control.Applicative import qualified Control.Lens as Lens @@ -492,7 +494,8 @@ data Settings = Settings -- | STOMP broker credentials setStomp :: !(Maybe FilePathSecrets), -- | Whitelist of allowed emails/phones - setWhitelist :: !(Maybe Whitelist), + setAllowlistEmailDomains :: !(Maybe AllowlistEmailDomains), + setAllowlistPhonePrefixes :: !(Maybe AllowlistPhonePrefixes), -- | Max. number of sent/accepted -- connections per user setUserMaxConnections :: !Int64, diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 529cc5a776..23872d077f 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -74,10 +74,10 @@ import Data.Predicate import Data.Qualified import Data.Range import qualified Data.Set as Set -import qualified Data.Swagger.Build.Api as Doc import qualified Data.Text.Ascii as Ascii import qualified Data.Text.Encoding as Text import qualified Data.ZAuth.Token as ZAuth +import GHC.TypeNats import Imports import Network.HTTP.Types.Status import Network.Wai (Response) @@ -98,7 +98,7 @@ import qualified Ssl.Util as SSL import System.Logger.Class (MonadLogger) import UnliftIO.Async (pooledMapConcurrentlyN_) import qualified Web.Cookie as Cookie -import Wire.API.Conversation +import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Bot import qualified Wire.API.Conversation.Bot as Public import Wire.API.Conversation.Role @@ -126,8 +126,10 @@ import qualified Wire.API.User.Identity as Public (Email) import Wire.Sem.Concurrency (Concurrency, ConcurrencySafety (Unsafe)) routesPublic :: - Members '[GalleyProvider, Concurrency 'Unsafe] r => - Routes Doc.ApiBuilder (Handler r) () + ( Member GalleyProvider r, + Member (Concurrency 'Unsafe) r + ) => + Routes () (Handler r) () routesPublic = do -- Public API (Unauthenticated) -------------------------------------------- @@ -326,7 +328,7 @@ routesPublic = do .&> zauth ZAuthBot .&> capture "uid" -routesInternal :: Members '[GalleyProvider] r => Routes a (Handler r) () +routesInternal :: Member GalleyProvider r => Routes a (Handler r) () routesInternal = do get "/i/provider/activation-code" (continue getActivationCodeH) $ accept "application" "json" @@ -335,7 +337,7 @@ routesInternal = do -------------------------------------------------------------------------------- -- Public API (Unauthenticated) -newAccountH :: Members '[GalleyProvider] r => JsonRequest Public.NewProvider -> (Handler r) Response +newAccountH :: Member GalleyProvider r => JsonRequest Public.NewProvider -> (Handler r) Response newAccountH req = do guardSecondFactorDisabled Nothing setStatus status201 . json <$> (newAccount =<< parseJsonBody req) @@ -372,7 +374,7 @@ newAccount new = do lift $ sendActivationMail name email key val False pure $ Public.NewProviderResponse pid newPass -activateAccountKeyH :: Members '[GalleyProvider] r => Code.Key ::: Code.Value -> (Handler r) Response +activateAccountKeyH :: Member GalleyProvider r => Code.Key ::: Code.Value -> (Handler r) Response activateAccountKeyH (key ::: val) = do guardSecondFactorDisabled Nothing maybe (setStatus status204 empty) json <$> activateAccountKey key val @@ -399,7 +401,7 @@ activateAccountKey key val = do lift $ sendApprovalConfirmMail name email pure . Just $ Public.ProviderActivationResponse email -getActivationCodeH :: Members '[GalleyProvider] r => Public.Email -> (Handler r) Response +getActivationCodeH :: Member GalleyProvider r => Public.Email -> (Handler r) Response getActivationCodeH e = do guardSecondFactorDisabled Nothing json <$> getActivationCode e @@ -420,7 +422,7 @@ instance ToJSON FoundActivationCode where toJSON $ Code.KeyValuePair (Code.codeKey vcode) (Code.codeValue vcode) -approveAccountKeyH :: Members '[GalleyProvider] r => Code.Key ::: Code.Value -> (Handler r) Response +approveAccountKeyH :: Member GalleyProvider r => Code.Key ::: Code.Value -> (Handler r) Response approveAccountKeyH (key ::: val) = do guardSecondFactorDisabled Nothing empty <$ approveAccountKey key val @@ -435,7 +437,7 @@ approveAccountKey key val = do lift $ sendApprovalConfirmMail name email _ -> throwStd (errorToWai @'E.InvalidCode) -loginH :: Members '[GalleyProvider] r => JsonRequest Public.ProviderLogin -> (Handler r) Response +loginH :: Member GalleyProvider r => JsonRequest Public.ProviderLogin -> (Handler r) Response loginH req = do guardSecondFactorDisabled Nothing tok <- login =<< parseJsonBody req @@ -449,7 +451,7 @@ login l = do throwStd (errorToWai @'E.BadCredentials) ZAuth.newProviderToken pid -beginPasswordResetH :: Members '[GalleyProvider] r => JsonRequest Public.PasswordReset -> (Handler r) Response +beginPasswordResetH :: Member GalleyProvider r => JsonRequest Public.PasswordReset -> (Handler r) Response beginPasswordResetH req = do guardSecondFactorDisabled Nothing setStatus status201 empty <$ (beginPasswordReset =<< parseJsonBody req) @@ -471,7 +473,7 @@ beginPasswordReset (Public.PasswordReset target) = do tryInsertVerificationCode code $ verificationCodeThrottledError . VerificationCodeThrottled lift $ sendPasswordResetMail target (Code.codeKey code) (Code.codeValue code) -completePasswordResetH :: Members '[GalleyProvider] r => JsonRequest Public.CompletePasswordReset -> (Handler r) Response +completePasswordResetH :: Member GalleyProvider r => JsonRequest Public.CompletePasswordReset -> (Handler r) Response completePasswordResetH req = do guardSecondFactorDisabled Nothing empty <$ (completePasswordReset =<< parseJsonBody req) @@ -492,7 +494,7 @@ completePasswordReset (Public.CompletePasswordReset key val newpwd) = do -------------------------------------------------------------------------------- -- Provider API -getAccountH :: Members '[GalleyProvider] r => ProviderId -> (Handler r) Response +getAccountH :: Member GalleyProvider r => ProviderId -> (Handler r) Response getAccountH pid = do guardSecondFactorDisabled Nothing getAccount pid <&> \case @@ -502,7 +504,7 @@ getAccountH pid = do getAccount :: ProviderId -> (Handler r) (Maybe Public.Provider) getAccount = wrapClientE . DB.lookupAccount -updateAccountProfileH :: Members '[GalleyProvider] r => ProviderId ::: JsonRequest Public.UpdateProvider -> (Handler r) Response +updateAccountProfileH :: Member GalleyProvider r => ProviderId ::: JsonRequest Public.UpdateProvider -> (Handler r) Response updateAccountProfileH (pid ::: req) = do guardSecondFactorDisabled Nothing empty <$ (updateAccountProfile pid =<< parseJsonBody req) @@ -517,7 +519,7 @@ updateAccountProfile pid upd = do (updateProviderUrl upd) (updateProviderDescr upd) -updateAccountEmailH :: Members '[GalleyProvider] r => ProviderId ::: JsonRequest Public.EmailUpdate -> (Handler r) Response +updateAccountEmailH :: Member GalleyProvider r => ProviderId ::: JsonRequest Public.EmailUpdate -> (Handler r) Response updateAccountEmailH (pid ::: req) = do guardSecondFactorDisabled Nothing setStatus status202 empty <$ (updateAccountEmail pid =<< parseJsonBody req) @@ -540,7 +542,7 @@ updateAccountEmail pid (Public.EmailUpdate new) = do tryInsertVerificationCode code $ verificationCodeThrottledError . VerificationCodeThrottled lift $ sendActivationMail (Name "name") email (Code.codeKey code) (Code.codeValue code) True -updateAccountPasswordH :: Members '[GalleyProvider] r => ProviderId ::: JsonRequest Public.PasswordChange -> (Handler r) Response +updateAccountPasswordH :: Member GalleyProvider r => ProviderId ::: JsonRequest Public.PasswordChange -> (Handler r) Response updateAccountPasswordH (pid ::: req) = do guardSecondFactorDisabled Nothing empty <$ (updateAccountPassword pid =<< parseJsonBody req) @@ -554,7 +556,7 @@ updateAccountPassword pid upd = do throwStd newPasswordMustDiffer wrapClientE $ DB.updateAccountPassword pid (cpNewPassword upd) -addServiceH :: Members '[GalleyProvider] r => ProviderId ::: JsonRequest Public.NewService -> (Handler r) Response +addServiceH :: Member GalleyProvider r => ProviderId ::: JsonRequest Public.NewService -> (Handler r) Response addServiceH (pid ::: req) = do guardSecondFactorDisabled Nothing setStatus status201 . json <$> (addService pid =<< parseJsonBody req) @@ -575,7 +577,7 @@ addService pid new = do let rstoken = maybe (Just token) (const Nothing) (newServiceToken new) pure $ Public.NewServiceResponse sid rstoken -listServicesH :: Members '[GalleyProvider] r => ProviderId -> (Handler r) Response +listServicesH :: Member GalleyProvider r => ProviderId -> (Handler r) Response listServicesH pid = do guardSecondFactorDisabled Nothing json <$> listServices pid @@ -583,7 +585,7 @@ listServicesH pid = do listServices :: ProviderId -> (Handler r) [Public.Service] listServices = wrapClientE . DB.listServices -getServiceH :: Members '[GalleyProvider] r => ProviderId ::: ServiceId -> (Handler r) Response +getServiceH :: Member GalleyProvider r => ProviderId ::: ServiceId -> (Handler r) Response getServiceH (pid ::: sid) = do guardSecondFactorDisabled Nothing json <$> getService pid sid @@ -592,7 +594,7 @@ getService :: ProviderId -> ServiceId -> (Handler r) Public.Service getService pid sid = wrapClientE (DB.lookupService pid sid) >>= maybeServiceNotFound -updateServiceH :: Members '[GalleyProvider] r => ProviderId ::: ServiceId ::: JsonRequest Public.UpdateService -> (Handler r) Response +updateServiceH :: Member GalleyProvider r => ProviderId ::: ServiceId ::: JsonRequest Public.UpdateService -> (Handler r) Response updateServiceH (pid ::: sid ::: req) = do guardSecondFactorDisabled Nothing empty <$ (updateService pid sid =<< parseJsonBody req) @@ -625,7 +627,7 @@ updateService pid sid upd = do tagsChange (serviceEnabled svc) -updateServiceConnH :: Members '[GalleyProvider] r => ProviderId ::: ServiceId ::: JsonRequest Public.UpdateServiceConn -> (Handler r) Response +updateServiceConnH :: Member GalleyProvider r => ProviderId ::: ServiceId ::: JsonRequest Public.UpdateServiceConn -> (Handler r) Response updateServiceConnH (pid ::: sid ::: req) = do guardSecondFactorDisabled Nothing empty <$ (updateServiceConn pid sid =<< parseJsonBody req) @@ -666,12 +668,12 @@ updateServiceConn pid sid upd = do -- TODO: Send informational email to provider. --- | Members '[GalleyProvider] r => The endpoint that is called to delete a service. +-- | Member GalleyProvider r => The endpoint that is called to delete a service. -- -- Since deleting a service can be costly, it just marks the service as -- disabled and then creates an event that will, when processed, actually -- delete the service. See 'finishDeleteService'. -deleteServiceH :: Members '[GalleyProvider] r => ProviderId ::: ServiceId ::: JsonRequest Public.DeleteService -> (Handler r) Response +deleteServiceH :: Member GalleyProvider r => ProviderId ::: ServiceId ::: JsonRequest Public.DeleteService -> (Handler r) Response deleteServiceH (pid ::: sid ::: req) = do guardSecondFactorDisabled Nothing setStatus status202 empty <$ (deleteService pid sid =<< parseJsonBody req) @@ -719,7 +721,7 @@ finishDeleteService pid sid = do kick (bid, cid, _) = deleteBot (botUserId bid) Nothing bid cid deleteAccountH :: - Members '[GalleyProvider] r => + Member GalleyProvider r => ProviderId ::: JsonRequest Public.DeleteProvider -> ExceptT Error (AppT r) Response deleteAccountH (pid ::: req) = do @@ -760,7 +762,7 @@ deleteAccount pid del = do -------------------------------------------------------------------------------- -- User API -getProviderProfileH :: Members '[GalleyProvider] r => ProviderId -> (Handler r) Response +getProviderProfileH :: Member GalleyProvider r => ProviderId -> (Handler r) Response getProviderProfileH pid = do guardSecondFactorDisabled Nothing json <$> getProviderProfile pid @@ -769,7 +771,7 @@ getProviderProfile :: ProviderId -> (Handler r) Public.ProviderProfile getProviderProfile pid = wrapClientE (DB.lookupAccountProfile pid) >>= maybeProviderNotFound -listServiceProfilesH :: Members '[GalleyProvider] r => ProviderId -> (Handler r) Response +listServiceProfilesH :: Member GalleyProvider r => ProviderId -> (Handler r) Response listServiceProfilesH pid = do guardSecondFactorDisabled Nothing json <$> listServiceProfiles pid @@ -777,7 +779,7 @@ listServiceProfilesH pid = do listServiceProfiles :: ProviderId -> (Handler r) [Public.ServiceProfile] listServiceProfiles = wrapClientE . DB.listServiceProfiles -getServiceProfileH :: Members '[GalleyProvider] r => ProviderId ::: ServiceId -> (Handler r) Response +getServiceProfileH :: Member GalleyProvider r => ProviderId ::: ServiceId -> (Handler r) Response getServiceProfileH (pid ::: sid) = do guardSecondFactorDisabled Nothing json <$> getServiceProfile pid sid @@ -786,7 +788,7 @@ getServiceProfile :: ProviderId -> ServiceId -> (Handler r) Public.ServiceProfil getServiceProfile pid sid = wrapClientE (DB.lookupServiceProfile pid sid) >>= maybeServiceNotFound -searchServiceProfilesH :: Members '[GalleyProvider] r => Maybe (Public.QueryAnyTags 1 3) ::: Maybe Text ::: Range 10 100 Int32 -> (Handler r) Response +searchServiceProfilesH :: Member GalleyProvider r => Maybe (Public.QueryAnyTags 1 3) ::: Maybe Text ::: Range 10 100 Int32 -> (Handler r) Response searchServiceProfilesH (qt ::: start ::: size) = do guardSecondFactorDisabled Nothing json <$> searchServiceProfiles qt start size @@ -805,7 +807,7 @@ searchServiceProfiles Nothing Nothing _ = do throwStd $ badRequest "At least `tags` or `start` must be provided." searchTeamServiceProfilesH :: - Members '[GalleyProvider] r => + Member GalleyProvider r => UserId ::: TeamId ::: Maybe (Range 1 128 Text) ::: Bool ::: Range 10 100 Int32 -> (Handler r) Response searchTeamServiceProfilesH (uid ::: tid ::: prefix ::: filterDisabled ::: size) = do @@ -830,7 +832,7 @@ searchTeamServiceProfiles uid tid prefix filterDisabled size = do -- Get search results wrapClientE $ DB.paginateServiceWhitelist tid prefix filterDisabled (fromRange size) -getServiceTagListH :: Members '[GalleyProvider] r => () -> (Handler r) Response +getServiceTagListH :: Member GalleyProvider r => () -> (Handler r) Response getServiceTagListH () = do guardSecondFactorDisabled Nothing json <$> getServiceTagList () @@ -840,7 +842,7 @@ getServiceTagList () = pure (Public.ServiceTagList allTags) where allTags = [(minBound :: Public.ServiceTag) ..] -updateServiceWhitelistH :: Members '[GalleyProvider] r => UserId ::: ConnId ::: TeamId ::: JsonRequest Public.UpdateServiceWhitelist -> (Handler r) Response +updateServiceWhitelistH :: Member GalleyProvider r => UserId ::: ConnId ::: TeamId ::: JsonRequest Public.UpdateServiceWhitelist -> (Handler r) Response updateServiceWhitelistH (uid ::: con ::: tid ::: req) = do guardSecondFactorDisabled (Just uid) resp <- updateServiceWhitelist uid con tid =<< parseJsonBody req @@ -853,7 +855,7 @@ data UpdateServiceWhitelistResp = UpdateServiceWhitelistRespChanged | UpdateServiceWhitelistRespUnchanged -updateServiceWhitelist :: Members '[GalleyProvider] r => UserId -> ConnId -> TeamId -> Public.UpdateServiceWhitelist -> (Handler r) UpdateServiceWhitelistResp +updateServiceWhitelist :: Member GalleyProvider r => UserId -> ConnId -> TeamId -> Public.UpdateServiceWhitelist -> (Handler r) UpdateServiceWhitelistResp updateServiceWhitelist uid con tid upd = do let pid = updateServiceWhitelistProvider upd sid = updateServiceWhitelistService upd @@ -886,12 +888,12 @@ updateServiceWhitelist uid con tid upd = do wrapClientE $ DB.deleteServiceWhitelist (Just tid) pid sid pure UpdateServiceWhitelistRespChanged -addBotH :: Members '[GalleyProvider] r => UserId ::: ConnId ::: ConvId ::: JsonRequest Public.AddBot -> (Handler r) Response +addBotH :: Member GalleyProvider r => UserId ::: ConnId ::: ConvId ::: JsonRequest Public.AddBot -> (Handler r) Response addBotH (zuid ::: zcon ::: cid ::: req) = do guardSecondFactorDisabled (Just zuid) setStatus status201 . json <$> (addBot zuid zcon cid =<< parseJsonBody req) -addBot :: Members '[GalleyProvider] r => UserId -> ConnId -> ConvId -> Public.AddBot -> (Handler r) Public.AddBotResponse +addBot :: Member GalleyProvider r => UserId -> ConnId -> ConvId -> Public.AddBot -> (Handler r) Public.AddBotResponse addBot zuid zcon cid add = do zusr <- lift (wrapClient $ User.lookupUser NoPendingInvitations zuid) >>= maybeInvalidUser let pid = addBotProvider add @@ -974,12 +976,12 @@ addBot zuid zcon cid add = do Public.rsAddBotEvent = ev } -removeBotH :: Members '[GalleyProvider] r => UserId ::: ConnId ::: ConvId ::: BotId -> (Handler r) Response +removeBotH :: Member GalleyProvider r => UserId ::: ConnId ::: ConvId ::: BotId -> (Handler r) Response removeBotH (zusr ::: zcon ::: cid ::: bid) = do guardSecondFactorDisabled (Just zusr) maybe (setStatus status204 empty) json <$> removeBot zusr zcon cid bid -removeBot :: Members '[GalleyProvider] r => UserId -> ConnId -> ConvId -> BotId -> (Handler r) (Maybe Public.RemoveBotResponse) +removeBot :: Member GalleyProvider r => UserId -> ConnId -> ConvId -> BotId -> (Handler r) (Maybe Public.RemoveBotResponse) removeBot zusr zcon cid bid = do -- Get the conversation and check preconditions lcid <- qualifyLocal cid @@ -1009,7 +1011,7 @@ guardConvAdmin conv = do -------------------------------------------------------------------------------- -- Bot API -botGetSelfH :: Members '[GalleyProvider] r => BotId -> (Handler r) Response +botGetSelfH :: Member GalleyProvider r => BotId -> (Handler r) Response botGetSelfH bot = do guardSecondFactorDisabled (Just (botUserId bot)) json <$> botGetSelf bot @@ -1019,7 +1021,7 @@ botGetSelf bot = do p <- lift $ wrapClient $ User.lookupUser NoPendingInvitations (botUserId bot) maybe (throwStd (errorToWai @'E.UserNotFound)) (pure . (`Public.publicProfile` UserLegalHoldNoConsent)) p -botGetClientH :: Members '[GalleyProvider] r => BotId -> (Handler r) Response +botGetClientH :: Member GalleyProvider r => BotId -> (Handler r) Response botGetClientH bot = do guardSecondFactorDisabled (Just (botUserId bot)) maybe (throwStd (errorToWai @'E.ClientNotFound)) (pure . json) =<< lift (botGetClient bot) @@ -1028,7 +1030,7 @@ botGetClient :: BotId -> (AppT r) (Maybe Public.Client) botGetClient bot = listToMaybe <$> wrapClient (User.lookupClients (botUserId bot)) -botListPrekeysH :: Members '[GalleyProvider] r => BotId -> (Handler r) Response +botListPrekeysH :: Member GalleyProvider r => BotId -> (Handler r) Response botListPrekeysH bot = do guardSecondFactorDisabled (Just (botUserId bot)) json <$> botListPrekeys bot @@ -1040,7 +1042,7 @@ botListPrekeys bot = do Nothing -> pure [] Just ci -> lift (wrapClient $ User.lookupPrekeyIds (botUserId bot) ci) -botUpdatePrekeysH :: Members '[GalleyProvider] r => BotId ::: JsonRequest Public.UpdateBotPrekeys -> (Handler r) Response +botUpdatePrekeysH :: Member GalleyProvider r => BotId ::: JsonRequest Public.UpdateBotPrekeys -> (Handler r) Response botUpdatePrekeysH (bot ::: req) = do guardSecondFactorDisabled (Just (botUserId bot)) empty <$ (botUpdatePrekeys bot =<< parseJsonBody req) @@ -1055,7 +1057,9 @@ botUpdatePrekeys bot upd = do wrapClientE (User.updatePrekeys (botUserId bot) (clientId c) pks) !>> clientDataError botClaimUsersPrekeysH :: - Members '[GalleyProvider, Concurrency 'Unsafe] r => + ( Member GalleyProvider r, + Member (Concurrency 'Unsafe) r + ) => JsonRequest Public.UserClients -> Handler r Response botClaimUsersPrekeysH req = do @@ -1063,7 +1067,7 @@ botClaimUsersPrekeysH req = do json <$> (botClaimUsersPrekeys =<< parseJsonBody req) botClaimUsersPrekeys :: - Members '[Concurrency 'Unsafe] r => + Member (Concurrency 'Unsafe) r => Public.UserClients -> Handler r Public.UserClientPrekeyMap botClaimUsersPrekeys body = do @@ -1072,7 +1076,7 @@ botClaimUsersPrekeys body = do throwStd (errorToWai @'E.TooManyClients) Client.claimLocalMultiPrekeyBundles UnprotectedBot body !>> clientError -botListUserProfilesH :: Members '[GalleyProvider] r => List UserId -> (Handler r) Response +botListUserProfilesH :: Member GalleyProvider r => List UserId -> (Handler r) Response botListUserProfilesH uids = do guardSecondFactorDisabled Nothing -- should we check all user ids? json <$> botListUserProfiles uids @@ -1082,7 +1086,7 @@ botListUserProfiles uids = do us <- lift . wrapClient $ User.lookupUsers NoPendingInvitations (fromList uids) pure (map mkBotUserView us) -botGetUserClientsH :: Members '[GalleyProvider] r => UserId -> (Handler r) Response +botGetUserClientsH :: Member GalleyProvider r => UserId -> (Handler r) Response botGetUserClientsH uid = do guardSecondFactorDisabled (Just uid) json <$> lift (botGetUserClients uid) @@ -1093,12 +1097,12 @@ botGetUserClients uid = where pubClient c = Public.PubClient (clientId c) (clientClass c) -botDeleteSelfH :: Members '[GalleyProvider] r => BotId ::: ConvId -> (Handler r) Response +botDeleteSelfH :: Member GalleyProvider r => BotId ::: ConvId -> (Handler r) Response botDeleteSelfH (bid ::: cid) = do guardSecondFactorDisabled (Just (botUserId bid)) empty <$ botDeleteSelf bid cid -botDeleteSelf :: Members '[GalleyProvider] r => BotId -> ConvId -> (Handler r) () +botDeleteSelf :: Member GalleyProvider r => BotId -> ConvId -> (Handler r) () botDeleteSelf bid cid = do guardSecondFactorDisabled (Just (botUserId bid)) bot <- lift . wrapClient $ User.lookupUser NoPendingInvitations (botUserId bid) @@ -1112,7 +1116,7 @@ botDeleteSelf bid cid = do -- | If second factor auth is enabled, make sure that end-points that don't support it, but should, are blocked completely. -- (This is a workaround until we have 2FA for those end-points as well.) guardSecondFactorDisabled :: - Members '[GalleyProvider] r => + Member GalleyProvider r => Maybe UserId -> ExceptT Error (AppT r) () guardSecondFactorDisabled mbUserId = do @@ -1234,7 +1238,7 @@ maybeInvalidBot = maybe (throwStd invalidBot) pure maybeInvalidUser :: Monad m => Maybe a -> (ExceptT Error m) a maybeInvalidUser = maybe (throwStd (errorToWai @'E.InvalidUser)) pure -rangeChecked :: (Within a n m, Monad monad) => a -> (ExceptT Error monad) (Range n m a) +rangeChecked :: (KnownNat n, KnownNat m, Within a n m, Monad monad) => a -> (ExceptT Error monad) (Range n m a) rangeChecked = either (throwStd . invalidRange . fromString) pure . checkedEither invalidServiceKey :: Wai.Error diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index 4e600477df..b4035c3beb 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -67,7 +67,7 @@ import Network.Wai.Routing.Route (App) import Network.Wai.Utilities (lookupRequestId) import Network.Wai.Utilities.Server import qualified Network.Wai.Utilities.Server as Server -import Polysemy (Members) +import Polysemy (Member) import Servant (Context ((:.)), (:<|>) (..)) import qualified Servant import System.Logger (msg, val, (.=), (~~)) @@ -185,7 +185,7 @@ bodyParserErrorFormatter _ _ errMsg = Servant.errHeaders = [(HTTP.hContentType, HTTPMedia.renderHeader (Servant.contentType (Proxy @Servant.JSON)))] } -pendingActivationCleanup :: forall r p. (P.Paging p, Members '[UserPendingActivationStore p] r) => AppT r () +pendingActivationCleanup :: forall r p. (P.Paging p, Member (UserPendingActivationStore p) r) => AppT r () pendingActivationCleanup = do safeForever "pendingActivationCleanup" $ do now <- liftIO =<< view currentTime diff --git a/services/brig/src/Brig/SMTP.hs b/services/brig/src/Brig/SMTP.hs index 9267b82760..d90a42296c 100644 --- a/services/brig/src/Brig/SMTP.hs +++ b/services/brig/src/Brig/SMTP.hs @@ -214,14 +214,14 @@ ensureSMTPConnectionTimeout timeoutDuration action = -- a timeout happens and on every other network failure. -- -- `defaultTimeoutDuration` is used as timeout duration for all actions. -sendMail :: (MonadIO m, MonadCatch m) => Logger -> SMTP -> Mail -> m () +sendMail :: MonadIO m => Logger -> SMTP -> Mail -> m () sendMail = sendMail' defaultTimeoutDuration -- | `sendMail` with configurable timeout duration -- -- This is mostly useful for testing. (We don't want to waste the amount of -- `defaultTimeoutDuration` in tests with waiting.) -sendMail' :: (MonadIO m, MonadCatch m, TimeUnit t) => t -> Logger -> SMTP -> Mail -> m () +sendMail' :: (MonadIO m, TimeUnit t) => t -> Logger -> SMTP -> Mail -> m () sendMail' timeoutDuration lg s m = liftIO $ withResource (s ^. pool) sendMail'' where sendMail'' :: SMTP.SMTPConnection -> IO () diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index c8e2776bd3..ee2f2b0d8d 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -53,21 +53,20 @@ import qualified Data.List1 as List1 import Data.Range import Data.String.Conversions (cs) import qualified Galley.Types.Teams as Team -import qualified Galley.Types.Teams.Intra as Team import Imports hiding (head) import Network.HTTP.Types.Status import Network.Wai (Response) import Network.Wai.Predicate hiding (and, result, setStatus) import Network.Wai.Routing import Network.Wai.Utilities hiding (code, message) -import Polysemy (Members) +import Polysemy (Member) 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.Federation.API +import qualified Wire.API.Routes.Internal.Galley.TeamsIntra as Team import Wire.API.Routes.Named import Wire.API.Routes.Public.Brig import Wire.API.Team @@ -82,11 +81,9 @@ import Wire.API.User hiding (fromEmail) import qualified Wire.API.User as Public servantAPI :: - Members - '[ BlacklistStore, - GalleyProvider - ] - r => + ( Member BlacklistStore r, + Member GalleyProvider r + ) => ServerT TeamsAPI (Handler r) servantAPI = Named @"send-team-invitation" createInvitationPublicH @@ -98,13 +95,9 @@ servantAPI = :<|> Named @"get-team-size" teamSizePublic routesInternal :: - ( Members - '[ BlacklistStore, - GalleyProvider, - UserPendingActivationStore p - ] - r, - CallsFed 'Brig "on-user-deleted-connections" + ( Member BlacklistStore r, + Member GalleyProvider r, + Member (UserPendingActivationStore p) r ) => Routes a (Handler r) () routesInternal = do @@ -133,7 +126,7 @@ routesInternal = do accept "application" "json" .&. jsonRequest @NewUserScimInvitation -teamSizePublic :: Members '[GalleyProvider] r => UserId -> TeamId -> (Handler r) TeamSize +teamSizePublic :: Member 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 teamSize tid @@ -160,11 +153,9 @@ instance ToJSON FoundInvitationCode where toJSON (FoundInvitationCode c) = object ["code" .= c] createInvitationPublicH :: - Members - '[ BlacklistStore, - GalleyProvider - ] - r => + ( Member BlacklistStore r, + Member GalleyProvider r + ) => UserId -> TeamId -> Public.InvitationRequest -> @@ -184,11 +175,9 @@ data CreateInvitationInviter = CreateInvitationInviter deriving (Eq, Show) createInvitationPublic :: - Members - '[ BlacklistStore, - GalleyProvider - ] - r => + ( Member BlacklistStore r, + Member GalleyProvider r + ) => UserId -> TeamId -> Public.InvitationRequest -> @@ -213,12 +202,10 @@ createInvitationPublic uid tid body = do (createInvitation' tid inviteeRole (Just (inviterUid inviter)) (inviterEmail inviter) body) createInvitationViaScimH :: - Members - '[ BlacklistStore, - GalleyProvider, - UserPendingActivationStore p - ] - r => + ( Member BlacklistStore r, + Member GalleyProvider r, + Member (UserPendingActivationStore p) r + ) => JSON ::: JsonRequest NewUserScimInvitation -> (Handler r) Response createInvitationViaScimH (_ ::: req) = do @@ -226,12 +213,10 @@ createInvitationViaScimH (_ ::: req) = do setStatus status201 . json <$> createInvitationViaScim body createInvitationViaScim :: - Members - '[ BlacklistStore, - GalleyProvider, - UserPendingActivationStore p - ] - r => + ( Member BlacklistStore r, + Member GalleyProvider r, + Member (UserPendingActivationStore p) r + ) => NewUserScimInvitation -> (Handler r) UserAccount createInvitationViaScim newUser@(NewUserScimInvitation tid loc name email role) = do @@ -272,11 +257,9 @@ logInvitationRequest context action = pure (Right result) createInvitation' :: - Members - '[ BlacklistStore, - GalleyProvider - ] - r => + ( Member BlacklistStore r, + Member GalleyProvider r + ) => TeamId -> Public.Role -> Maybe UserId -> @@ -336,19 +319,19 @@ createInvitation' tid inviteeRole mbInviterUid fromEmail body = do timeout (newInv, code) <$ sendInvitationMail inviteeEmail tid fromEmail code locale -deleteInvitation :: Members '[GalleyProvider] r => UserId -> TeamId -> InvitationId -> (Handler r) () +deleteInvitation :: Member GalleyProvider r => UserId -> TeamId -> InvitationId -> (Handler r) () deleteInvitation uid tid iid = do ensurePermissions uid tid [AddTeamMember] lift $ wrapClient $ DB.deleteInvitation tid iid -listInvitations :: Members '[GalleyProvider] r => UserId -> TeamId -> Maybe InvitationId -> Maybe (Range 1 500 Int32) -> (Handler r) Public.InvitationList +listInvitations :: Member 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 (fromMaybe (unsafeRange 100) mSize) pure $! Public.InvitationList (DB.resultList rs) (DB.resultHasMore rs) -getInvitation :: Members '[GalleyProvider] r => UserId -> TeamId -> InvitationId -> (Handler r) (Maybe Public.Invitation) +getInvitation :: Member 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 @@ -380,25 +363,25 @@ getInvitationByEmail email = do inv <- lift $ wrapClient $ DB.lookupInvitationByEmail HideInvitationUrl email maybe (throwStd (notFound "Invitation not found")) pure inv -suspendTeamH :: (Members '[GalleyProvider] r, CallsFed 'Brig "on-user-deleted-connections") => JSON ::: TeamId -> (Handler r) Response +suspendTeamH :: (Member GalleyProvider r) => JSON ::: TeamId -> (Handler r) Response suspendTeamH (_ ::: tid) = do empty <$ suspendTeam tid -suspendTeam :: (Members '[GalleyProvider] r, CallsFed 'Brig "on-user-deleted-connections") => TeamId -> (Handler r) () +suspendTeam :: (Member GalleyProvider r) => TeamId -> (Handler r) () suspendTeam tid = do changeTeamAccountStatuses tid Suspended lift $ wrapClient $ DB.deleteInvitations tid lift $ liftSem $ GalleyProvider.changeTeamStatus tid Team.Suspended Nothing unsuspendTeamH :: - (Members '[GalleyProvider] r, CallsFed 'Brig "on-user-deleted-connections") => + (Member GalleyProvider r) => JSON ::: TeamId -> (Handler r) Response unsuspendTeamH (_ ::: tid) = do empty <$ unsuspendTeam tid unsuspendTeam :: - (Members '[GalleyProvider] r, CallsFed 'Brig "on-user-deleted-connections") => + (Member GalleyProvider r) => TeamId -> (Handler r) () unsuspendTeam tid = do @@ -409,7 +392,7 @@ unsuspendTeam tid = do -- Internal changeTeamAccountStatuses :: - (Members '[GalleyProvider] r, CallsFed 'Brig "on-user-deleted-connections") => + (Member GalleyProvider r) => TeamId -> AccountStatus -> (Handler r) () diff --git a/services/brig/src/Brig/User/API/Handle.hs b/services/brig/src/Brig/User/API/Handle.hs index 7d3c37e878..eb59ca038f 100644 --- a/services/brig/src/Brig/User/API/Handle.hs +++ b/services/brig/src/Brig/User/API/Handle.hs @@ -39,14 +39,13 @@ import Imports import Network.Wai.Utilities ((!>>)) import Polysemy import qualified System.Logger.Class as Log -import Wire.API.Federation.API import Wire.API.User import qualified Wire.API.User as Public import Wire.API.User.Search import qualified Wire.API.User.Search as Public getHandleInfo :: - (Members '[GalleyProvider] r, CallsFed 'Brig "get-user-by-handle", CallsFed 'Brig "get-users-by-ids") => + (Member GalleyProvider r) => UserId -> Qualified Handle -> (Handler r) (Maybe Public.UserProfile) @@ -58,7 +57,7 @@ getHandleInfo self handle = do getRemoteHandleInfo handle -getRemoteHandleInfo :: CallsFed 'Brig "get-user-by-handle" => Remote Handle -> (Handler r) (Maybe Public.UserProfile) +getRemoteHandleInfo :: Remote Handle -> (Handler r) (Maybe Public.UserProfile) getRemoteHandleInfo handle = do lift . Log.info $ Log.msg (Log.val "getHandleInfo - remote lookup") @@ -66,7 +65,7 @@ getRemoteHandleInfo handle = do Federation.getUserHandleInfo handle !>> fedError getLocalHandleInfo :: - (Members '[GalleyProvider] r, CallsFed 'Brig "get-users-by-ids") => + (Member GalleyProvider r) => Local UserId -> Handle -> (Handler r) (Maybe Public.UserProfile) diff --git a/services/brig/src/Brig/User/API/Search.hs b/services/brig/src/Brig/User/API/Search.hs index 2713256f82..c85ef79822 100644 --- a/services/brig/src/Brig/User/API/Search.hs +++ b/services/brig/src/Brig/User/API/Search.hs @@ -50,7 +50,6 @@ import Polysemy import System.Logger (field, msg) import System.Logger.Class (val, (~~)) import qualified System.Logger.Class as Log -import Wire.API.Federation.API import qualified Wire.API.Federation.API.Brig as FedBrig import qualified Wire.API.Federation.API.Brig as S import qualified Wire.API.Team.Permission as Public @@ -86,7 +85,7 @@ routesInternal = do -- FUTUREWORK: Consider augmenting 'SearchResult' with full user profiles -- for all results. This is tracked in https://wearezeta.atlassian.net/browse/SQCORE-599 search :: - (Members '[GalleyProvider] r, CallsFed 'Brig "get-users-by-ids", CallsFed 'Brig "search-users") => + (Member GalleyProvider r) => UserId -> Text -> Maybe Domain -> @@ -99,7 +98,7 @@ search searcherId searchTerm maybeDomain maybeMaxResults = do then searchLocally searcherId searchTerm maybeMaxResults else searchRemotely queryDomain searchTerm -searchRemotely :: CallsFed 'Brig "search-users" => Domain -> Text -> (Handler r) (Public.SearchResult Public.Contact) +searchRemotely :: Domain -> Text -> (Handler r) (Public.SearchResult Public.Contact) searchRemotely domain searchTerm = do lift . Log.info $ msg (val "searchRemotely") @@ -121,7 +120,7 @@ searchRemotely domain searchTerm = do searchLocally :: forall r. - (Members '[GalleyProvider] r, CallsFed 'Brig "get-users-by-ids") => + (Member GalleyProvider r) => UserId -> Text -> Maybe (Range 1 500 Int32) -> @@ -176,7 +175,7 @@ searchLocally searcherId searchTerm maybeMaxResults = do <$$> HandleAPI.getLocalHandleInfo lsearcherId handle teamUserSearch :: - Members '[GalleyProvider] r => + Member GalleyProvider r => UserId -> TeamId -> Maybe Text -> diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index 39fc1b7462..8aa2d04302 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -78,7 +78,6 @@ import Network.Wai.Utilities.Error ((!>>)) import Polysemy import System.Logger (field, msg, val, (~~)) import qualified System.Logger.Class as Log -import Wire.API.Federation.API import Wire.API.Team.Feature import qualified Wire.API.Team.Feature as Public import Wire.API.User @@ -135,7 +134,7 @@ lookupLoginCode phone = login :: forall r. - (Members '[GalleyProvider] r, CallsFed 'Brig "on-user-deleted-connections") => + (Member GalleyProvider r) => Login -> CookieType -> ExceptT LoginError (AppT r) (Access ZAuth.User) @@ -172,7 +171,7 @@ login (SmsLogin (SmsLoginData phone code label)) typ = do verifyCode :: forall r. - Members '[GalleyProvider] r => + Member GalleyProvider r => Maybe Code.Value -> VerificationAction -> UserId -> @@ -212,7 +211,7 @@ checkRetryLimit :: (MonadClient m, MonadReader Env m) => UserId -> ExceptT Login checkRetryLimit = withRetryLimit checkBudget withRetryLimit :: - (MonadClient m, MonadReader Env m) => + MonadReader Env m => (BudgetKey -> Budget -> ExceptT LoginError m (Budgeted ())) -> UserId -> ExceptT LoginError m () @@ -252,8 +251,7 @@ renewAccess :: MonadMask m, MonadHttp m, HasRequestId m, - MonadUnliftIO m, - CallsFed 'Brig "on-user-deleted-connections" + MonadUnliftIO m ) => List1 (ZAuth.Token u) -> Maybe (ZAuth.Token a) -> @@ -291,8 +289,7 @@ catchSuspendInactiveUser :: MonadHttp m, HasRequestId m, MonadUnliftIO m, - Log.MonadLogger m, - CallsFed 'Brig "on-user-deleted-connections" + Log.MonadLogger m ) => UserId -> e -> @@ -324,8 +321,7 @@ newAccess :: MonadMask m, MonadHttp m, HasRequestId m, - MonadUnliftIO m, - CallsFed 'Brig "on-user-deleted-connections" + MonadUnliftIO m ) => UserId -> Maybe ClientId -> @@ -418,7 +414,6 @@ validateTokens uts at = do validateToken :: ( ZAuth.TokenPair u a, - Monad m, ZAuth.MonadZAuth m, MonadClient m ) => @@ -446,8 +441,7 @@ ssoLogin :: MonadMask m, MonadHttp m, HasRequestId m, - MonadUnliftIO m, - CallsFed 'Brig "on-user-deleted-connections" + MonadUnliftIO m ) => SsoLogin -> CookieType -> @@ -468,7 +462,7 @@ ssoLogin (SsoLogin uid label) typ = do -- | Log in as a LegalHold service, getting LegalHoldUser/Access Tokens. legalHoldLogin :: - (Members '[GalleyProvider] r, CallsFed 'Brig "on-user-deleted-connections") => + (Member GalleyProvider r) => LegalHoldLogin -> CookieType -> ExceptT LegalHoldLoginError (AppT r) (Access ZAuth.LegalHoldUser) @@ -486,7 +480,7 @@ legalHoldLogin (LegalHoldLogin uid plainTextPassword label) typ = do !>> LegalHoldLoginError assertLegalHoldEnabled :: - Members '[GalleyProvider] r => + Member GalleyProvider r => TeamId -> ExceptT LegalHoldLoginError (AppT r) () assertLegalHoldEnabled tid = do diff --git a/services/brig/src/Brig/User/Auth/Cookie.hs b/services/brig/src/Brig/User/Auth/Cookie.hs index a72d560de9..db235c7c16 100644 --- a/services/brig/src/Brig/User/Auth/Cookie.hs +++ b/services/brig/src/Brig/User/Auth/Cookie.hs @@ -264,7 +264,7 @@ newCookieLimited u c typ label = do -- HTTP setResponseCookie :: - (Monad m, MonadReader Env m, ZAuth.UserTokenLike u) => + (MonadReader Env m, ZAuth.UserTokenLike u) => Cookie (ZAuth.Token u) -> Response -> m Response @@ -272,7 +272,7 @@ setResponseCookie c r = do hdr <- toByteString' . WebCookie.renderSetCookie <$> toWebCookie c pure (addHeader "Set-Cookie" hdr r) -toWebCookie :: (Monad m, MonadReader Env m, ZAuth.UserTokenLike u) => Cookie (ZAuth.Token u) -> m WebCookie.SetCookie +toWebCookie :: (MonadReader Env m, ZAuth.UserTokenLike u) => Cookie (ZAuth.Token u) -> m WebCookie.SetCookie toWebCookie c = do s <- view settings pure $ diff --git a/services/brig/src/Brig/User/Email.hs b/services/brig/src/Brig/User/Email.hs index 5d200af829..73063a10c9 100644 --- a/services/brig/src/Brig/User/Email.hs +++ b/services/brig/src/Brig/User/Email.hs @@ -42,7 +42,6 @@ import Brig.Types.Activation (ActivationPair) import Brig.Types.User (PasswordResetPair) import Brig.User.Template import Control.Lens (view) -import Control.Monad.Catch import qualified Data.Code as Code import Data.Json.Util (fromUTCTimeMillis) import Data.Range @@ -56,7 +55,6 @@ import Wire.API.User.Password sendVerificationMail :: ( MonadIO m, - MonadCatch m, MonadReader Env m ) => Email -> @@ -71,8 +69,7 @@ sendVerificationMail to pair loc = do sendLoginVerificationMail :: ( MonadReader Env m, - MonadIO m, - MonadCatch m + MonadIO m ) => Email -> Code.Value -> @@ -85,7 +82,6 @@ sendLoginVerificationMail email code mbLocale = do sendCreateScimTokenVerificationMail :: ( MonadIO m, - MonadCatch m, MonadReader Env m ) => Email -> @@ -99,7 +95,6 @@ sendCreateScimTokenVerificationMail email code mbLocale = do sendTeamDeletionVerificationMail :: ( MonadIO m, - MonadCatch m, MonadReader Env m ) => Email -> @@ -113,7 +108,6 @@ sendTeamDeletionVerificationMail email code mbLocale = do sendActivationMail :: ( MonadIO m, - MonadCatch m, MonadReader Env m ) => Email -> @@ -135,7 +129,6 @@ sendActivationMail to name pair loc ident = do sendPasswordResetMail :: ( MonadIO m, - MonadCatch m, MonadReader Env m ) => Email -> @@ -150,7 +143,6 @@ sendPasswordResetMail to pair loc = do sendDeletionEmail :: ( MonadIO m, - MonadCatch m, MonadReader Env m ) => Name -> @@ -166,7 +158,6 @@ sendDeletionEmail name email key code locale = do sendNewClientEmail :: ( MonadIO m, - MonadCatch m, MonadReader Env m ) => Name -> @@ -181,7 +172,6 @@ sendNewClientEmail name email client locale = do sendTeamActivationMail :: ( MonadIO m, - MonadCatch m, MonadReader Env m ) => Email -> diff --git a/services/brig/src/Brig/User/Search/Index.hs b/services/brig/src/Brig/User/Search/Index.hs index 28e3b41e5e..5d5a089ffb 100644 --- a/services/brig/src/Brig/User/Search/Index.hs +++ b/services/brig/src/Brig/User/Search/Index.hs @@ -168,7 +168,7 @@ withAdditionalESUrl action = do -------------------------------------------------------------------------------- -- Updates -reindex :: (MonadLogger m, MonadCatch m, MonadIndexIO m, C.MonadClient m) => UserId -> m () +reindex :: (MonadLogger m, MonadIndexIO m, C.MonadClient m) => UserId -> m () reindex u = do ixu <- lookupIndexUser u updateIndex (maybe (IndexDeleteUser u) (IndexUpdateUser IndexUpdateIfNewerVersion) ixu) @@ -679,12 +679,12 @@ mappingName :: ES.MappingName mappingName = ES.MappingName "user" lookupIndexUser :: - (MonadCatch m, MonadIndexIO m, C.MonadClient m) => + (MonadIndexIO m, C.MonadClient m) => UserId -> m (Maybe IndexUser) lookupIndexUser = lookupForIndex -lookupForIndex :: (MonadThrow m, C.MonadClient m, MonadIndexIO m) => UserId -> m (Maybe IndexUser) +lookupForIndex :: (C.MonadClient m, MonadIndexIO m) => UserId -> m (Maybe IndexUser) lookupForIndex u = do mrow <- C.retry C.x1 (C.query1 cql (C.params C.LocalQuorum (Identity u))) for mrow $ \row -> do diff --git a/services/brig/src/Brig/User/Search/TeamUserSearch.hs b/services/brig/src/Brig/User/Search/TeamUserSearch.hs index 3731f02ab6..c2d3359faa 100644 --- a/services/brig/src/Brig/User/Search/TeamUserSearch.hs +++ b/services/brig/src/Brig/User/Search/TeamUserSearch.hs @@ -1,5 +1,7 @@ {-# LANGUAGE StrictData #-} {-# OPTIONS_GHC -Wno-orphans #-} +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- diff --git a/services/brig/src/Brig/Whitelist.hs b/services/brig/src/Brig/Whitelist.hs deleted file mode 100644 index 9f9bac98da..0000000000 --- a/services/brig/src/Brig/Whitelist.hs +++ /dev/null @@ -1,78 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - --- | > docs/reference/user/activation.md {#RefActivationWhitelist} --- --- Email/phone whitelist. -module Brig.Whitelist - ( Whitelist (..), - verify, - ) -where - -import Bilge.IO -import Bilge.Request -import Bilge.Response -import Bilge.Retry -import Control.Monad.Catch (MonadMask, throwM) -import Control.Retry -import Data.Aeson -import Data.Text -import Data.Text.Encoding (encodeUtf8) -import Imports -import Network.HTTP.Client (HttpExceptionContent (..)) -import Wire.API.User.Identity - --- | A service providing a whitelist of allowed email addresses and phone numbers -data Whitelist = Whitelist - { -- | Service URL - whitelistUrl :: !Text, - -- | Username - whitelistUser :: !Text, - -- | Password - whitelistPass :: !Text - } - deriving (Show, Generic) - -instance FromJSON Whitelist - --- | Do a request to the whitelist service and verify that the provided email/phone address is --- whitelisted. -verify :: (MonadIO m, MonadMask m, MonadHttp m) => Whitelist -> Either Email Phone -> m Bool -verify (Whitelist url user pass) key = - if isKnownDomain key - then pure True - else recovering x3 httpHandlers . const $ do - rq <- parseRequest $ unpack url - rsp <- get' rq $ req (encodeUtf8 user) (encodeUtf8 pass) - case statusCode rsp of - 200 -> pure True - 404 -> pure False - _ -> - throwM $ - HttpExceptionRequest rq (StatusCodeException (rsp {responseBody = ()}) mempty) - where - isKnownDomain (Left e) = emailDomain e == "wire.com" - isKnownDomain _ = False - urlEmail = queryItem "email" . encodeUtf8 . fromEmail - urlPhone = queryItem "mobile" . encodeUtf8 . fromPhone - req u p = - port 443 - . secure - . either urlEmail urlPhone key - . applyBasicAuth u p - x3 = limitRetries 3 <> exponentialBackoff 100000 diff --git a/services/brig/test/integration/API/Calling.hs b/services/brig/test/integration/API/Calling.hs index 3efa9c2e4a..0ca4166bb5 100644 --- a/services/brig/test/integration/API/Calling.hs +++ b/services/brig/test/integration/API/Calling.hs @@ -23,7 +23,7 @@ import Bilge import Bilge.Assert import qualified Brig.Options as Opts import Control.Lens (view, (.~), (?~), (^.)) -import Control.Monad.Catch (MonadCatch, MonadThrow) +import Control.Monad.Catch (MonadCatch) import Data.Bifunctor (Bifunctor (first)) import Data.ByteString.Conversion import qualified Data.ByteString.Lazy as LB @@ -219,10 +219,10 @@ assertConfiguration cfg expected = do getTurnConfigurationV1 :: UserId -> Brig -> Http RTCConfiguration getTurnConfigurationV1 = getAndValidateTurnConfiguration "" -getTurnConfigurationV2 :: HasCallStack => UserId -> Brig -> ((Monad m, MonadHttp m, MonadIO m, MonadCatch m) => m RTCConfiguration) +getTurnConfigurationV2 :: HasCallStack => UserId -> Brig -> ((MonadHttp m, MonadIO m, MonadCatch m) => m RTCConfiguration) getTurnConfigurationV2 = getAndValidateTurnConfiguration "v2" -getTurnConfiguration :: ByteString -> UserId -> Brig -> ((MonadHttp m, MonadIO m) => m (Response (Maybe LB.ByteString))) +getTurnConfiguration :: ByteString -> UserId -> Brig -> (MonadHttp m => m (Response (Maybe LB.ByteString))) getTurnConfiguration suffix u b = get ( b @@ -231,7 +231,7 @@ getTurnConfiguration suffix u b = . zConn "conn" ) -getAndValidateTurnConfiguration :: HasCallStack => ByteString -> UserId -> Brig -> ((Monad m, MonadIO m, MonadHttp m, MonadThrow m, MonadCatch m) => m RTCConfiguration) +getAndValidateTurnConfiguration :: HasCallStack => ByteString -> UserId -> Brig -> ((MonadIO m, MonadHttp m, MonadCatch m) => m RTCConfiguration) getAndValidateTurnConfiguration suffix u b = responseJsonError =<< (getTurnConfiguration suffix u b Domain -> Text -> [Qualified UserId] -> FederatedUserSearchPolicy -> WaiTest.Session () + expectSearch domain squery expectedUsers expectedSearchPolicy = do searchResponse <- runWaiTestFedClient domain $ createWaiTestFedClient @"search-users" @'Brig (SearchRequest squery) diff --git a/services/brig/test/integration/API/Internal.hs b/services/brig/test/integration/API/Internal.hs index f3fbeae676..516b8934c9 100644 --- a/services/brig/test/integration/API/Internal.hs +++ b/services/brig/test/integration/API/Internal.hs @@ -1,3 +1,6 @@ +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -33,7 +36,6 @@ import qualified Cassandra as Cass import Cassandra.Util import Control.Exception (ErrorCall (ErrorCall), throwIO) import Control.Lens ((^.), (^?!)) -import Control.Monad.Catch import Data.Aeson (decode) import qualified Data.Aeson.Lens as Aeson import qualified Data.Aeson.Types as Aeson @@ -102,7 +104,7 @@ testSuspendNonExistingUser db brig = do isUserCreated <- Cass.runClient db (userExists nonExistingUserId) liftIO $ isUserCreated @?= False -setAccountStatus :: (MonadIO m, MonadHttp m, HasCallStack, MonadCatch m) => Brig -> UserId -> AccountStatus -> m ResponseLBS +setAccountStatus :: (MonadHttp m, HasCallStack) => Brig -> UserId -> AccountStatus -> m ResponseLBS setAccountStatus brig u s = put ( brig @@ -366,11 +368,11 @@ testAddKeyPackageRef brig = do (Request -> Request) -> UserId -> m ResponseLBS +getFeatureConfig :: forall cfg m. (MonadHttp m, HasCallStack, KnownSymbol (ApiFt.FeatureSymbol cfg)) => (Request -> Request) -> UserId -> m ResponseLBS getFeatureConfig galley uid = do get $ apiVersion "v1" . galley . paths ["feature-configs", featureNameBS @cfg] . zUser uid -getAllFeatureConfigs :: (MonadIO m, MonadHttp m, HasCallStack) => (Request -> Request) -> UserId -> m ResponseLBS +getAllFeatureConfigs :: (MonadHttp m, HasCallStack) => (Request -> Request) -> UserId -> m ResponseLBS getAllFeatureConfigs galley uid = do get $ galley . paths ["feature-configs"] . zUser uid diff --git a/services/brig/test/integration/API/Internal/Util.hs b/services/brig/test/integration/API/Internal/Util.hs index b893dcad3e..2fd3610800 100644 --- a/services/brig/test/integration/API/Internal/Util.hs +++ b/services/brig/test/integration/API/Internal/Util.hs @@ -1,3 +1,6 @@ +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -91,7 +94,7 @@ scaffolding brig gundeck = do (userPhone usr) toks - registerPushToken :: TestConstraints m => Gundeck -> UserId -> m Text + registerPushToken :: Gundeck -> UserId -> m Text registerPushToken gd u = do t <- randomToken rsp <- registerPushTokenRequest gd u t @@ -100,7 +103,7 @@ scaffolding brig gundeck = do (error . show) (pure . PushToken.tokenText . view PushToken.token) - registerPushTokenRequest :: TestConstraints m => Gundeck -> UserId -> PushToken.PushToken -> m ResponseLBS + registerPushTokenRequest :: Gundeck -> UserId -> PushToken.PushToken -> m ResponseLBS registerPushTokenRequest gd u t = do post ( gd @@ -111,7 +114,7 @@ scaffolding brig gundeck = do . json t ) - randomToken :: MonadIO m => m PushToken.PushToken + randomToken :: m PushToken.PushToken randomToken = liftIO $ do c <- liftIO $ newClientId <$> (randomIO :: IO Word64) tok <- (PushToken.Token . T.decodeUtf8) . B16.encode <$> randomBytes 32 @@ -129,19 +132,19 @@ putAccountConferenceCallingConfigClientM = Client.client (Proxy @("i" :> IAPI.Pu deleteAccountConferenceCallingConfigClientM :: UserId -> Client.ClientM NoContent deleteAccountConferenceCallingConfigClientM = Client.client (Proxy @("i" :> IAPI.DeleteAccountConferenceCallingConfig)) -ejpdRequestClient :: (HasCallStack, MonadThrow m, MonadIO m, MonadHttp m) => Endpoint -> Manager -> Maybe Bool -> EJPDRequestBody -> m EJPDResponseBody +ejpdRequestClient :: (HasCallStack, MonadThrow m, MonadIO m) => Endpoint -> Manager -> Maybe Bool -> EJPDRequestBody -> m EJPDResponseBody ejpdRequestClient brigep mgr includeContacts ejpdReqBody = runHereClientM brigep mgr (ejpdRequestClientM includeContacts ejpdReqBody) >>= either throwM pure -getAccountConferenceCallingConfigClient :: (HasCallStack, MonadIO m, MonadHttp m) => Endpoint -> Manager -> UserId -> m (Either Client.ClientError (Public.WithStatusNoLock Public.ConferenceCallingConfig)) +getAccountConferenceCallingConfigClient :: (HasCallStack, MonadIO m) => Endpoint -> Manager -> UserId -> m (Either Client.ClientError (Public.WithStatusNoLock Public.ConferenceCallingConfig)) getAccountConferenceCallingConfigClient brigep mgr uid = runHereClientM brigep mgr (getAccountConferenceCallingConfigClientM uid) -putAccountConferenceCallingConfigClient :: (HasCallStack, MonadIO m, MonadHttp m) => Endpoint -> Manager -> UserId -> Public.WithStatusNoLock Public.ConferenceCallingConfig -> m (Either Client.ClientError NoContent) +putAccountConferenceCallingConfigClient :: (HasCallStack, MonadIO m) => Endpoint -> Manager -> UserId -> Public.WithStatusNoLock Public.ConferenceCallingConfig -> m (Either Client.ClientError NoContent) putAccountConferenceCallingConfigClient brigep mgr uid cfg = runHereClientM brigep mgr (putAccountConferenceCallingConfigClientM uid cfg) -deleteAccountConferenceCallingConfigClient :: (HasCallStack, MonadIO m, MonadHttp m) => Endpoint -> Manager -> UserId -> m (Either Client.ClientError NoContent) +deleteAccountConferenceCallingConfigClient :: (HasCallStack, MonadIO m) => Endpoint -> Manager -> UserId -> m (Either Client.ClientError NoContent) deleteAccountConferenceCallingConfigClient brigep mgr uid = runHereClientM brigep mgr (deleteAccountConferenceCallingConfigClientM uid) -runHereClientM :: (HasCallStack, MonadIO m, MonadHttp m) => Endpoint -> Manager -> Client.ClientM a -> m (Either Client.ClientError a) +runHereClientM :: (HasCallStack, MonadIO m) => Endpoint -> Manager -> Client.ClientM a -> m (Either Client.ClientError a) runHereClientM brigep mgr action = do let env = Client.mkClientEnv mgr baseurl baseurl = Client.BaseUrl Client.Http (cs $ brigep ^. epHost) (fromIntegral $ brigep ^. epPort) "" diff --git a/services/brig/test/integration/API/MLS.hs b/services/brig/test/integration/API/MLS.hs index 93c118cf17..440da8e28d 100644 --- a/services/brig/test/integration/API/MLS.hs +++ b/services/brig/test/integration/API/MLS.hs @@ -84,7 +84,7 @@ testKeyPackageZeroCount brig = do testKeyPackageExpired :: Brig -> Http () testKeyPackageExpired brig = do u <- userQualifiedId <$> randomUser brig - let lifetime = 2 # Second + let lifetime = 3 # Second [c1, c2] <- for [(0, Just lifetime), (1, Nothing)] $ \(i, lt) -> do c <- createClient brig u i -- upload 1 key package for each client @@ -95,7 +95,7 @@ testKeyPackageExpired brig = do count <- getKeyPackageCount brig u cid liftIO $ count @?= expectedCount -- wait for c1's key package to expire - threadDelay (fromIntegral ((lifetime + 3 # Second) #> MicroSecond)) + threadDelay (fromIntegral ((lifetime + 4 # Second) #> MicroSecond)) -- c1's key package has expired by now for_ [(c1, 0), (c2, 1)] $ \(cid, expectedCount) -> do diff --git a/services/brig/test/integration/API/Metrics.hs b/services/brig/test/integration/API/Metrics.hs index 807942b60d..22dffa10f9 100644 --- a/services/brig/test/integration/API/Metrics.hs +++ b/services/brig/test/integration/API/Metrics.hs @@ -26,6 +26,7 @@ where import Bilge import Bilge.Assert +import qualified Brig.Options as Opt import Data.Attoparsec.Text import Data.ByteString.Conversion import Imports @@ -34,13 +35,13 @@ import Test.Tasty.HUnit import Util import Wire.API.User -tests :: Manager -> Brig -> IO TestTree -tests manager brig = do +tests :: Manager -> Opt.Opts -> Brig -> IO TestTree +tests manager opts brig = do pure $ testGroup "metrics" [ testCase "prometheus" . void $ runHttpT manager (testPrometheusMetrics brig), - testCase "work" . void $ runHttpT manager (testMetricsEndpoint brig) + testCase "work" . void $ runHttpT manager (testMetricsEndpoint opts brig) ] testPrometheusMetrics :: Brig -> Http () @@ -50,8 +51,8 @@ testPrometheusMetrics brig = do -- Should contain the request duration metric in its output const (Just "TYPE http_request_duration_seconds histogram") =~= responseBody -testMetricsEndpoint :: Brig -> Http () -testMetricsEndpoint brig0 = do +testMetricsEndpoint :: Opt.Opts -> Brig -> Http () +testMetricsEndpoint opts brig0 = withSettingsOverrides opts $ do let brig = apiVersion "v1" . brig0 p1 = "/self" p2 uid = "/users/" <> uid <> "/clients" @@ -67,11 +68,11 @@ testMetricsEndpoint brig0 = do _ <- post (brig . path p3 . contentJson . queryItem "persist" "true" . json (defEmailLogin email) . expect2xx) _ <- post (brig . path p3 . contentJson . queryItem "persist" "true" . json (defEmailLogin email) . expect2xx) countSelf <- getCount "/self" "GET" - liftIO $ assertEqual "/self was called once" (beforeSelf + 1) countSelf + liftIO $ assertBool "/self was called at least once" ((beforeSelf + 1) <= countSelf) countClients <- getCount "/users/:uid/clients" "GET" - liftIO $ assertEqual "/users/:uid/clients was called twice" (beforeClients + 2) countClients + liftIO $ assertBool "/users/:uid/clients was called at least twice" ((beforeClients + 2) <= countClients) countProperties <- getCount "/login" "POST" - liftIO $ assertEqual "/login was called twice" (beforeProperties + 2) countProperties + liftIO $ assertBool "/login was called at least twice" ((beforeProperties + 2) <= countProperties) where getCount endpoint m = do rsp <- responseBody <$> get (brig0 . path "i/metrics") diff --git a/services/brig/test/integration/API/Provider.hs b/services/brig/test/integration/API/Provider.hs index 93172e6ad6..88b33eb8f6 100644 --- a/services/brig/test/integration/API/Provider.hs +++ b/services/brig/test/integration/API/Provider.hs @@ -53,6 +53,7 @@ import Data.PEM import Data.Qualified import Data.Range import qualified Data.Set as Set +import Data.Streaming.Network (bindRandomPortTCP) import qualified Data.Text as Text import qualified Data.Text.Ascii as Ascii import Data.Text.Encoding (encodeUtf8) @@ -63,6 +64,8 @@ import qualified Data.UUID as UUID import qualified Data.ZAuth.Token as ZAuth import Imports hiding (threadDelay) import Network.HTTP.Types.Status (status200, status201, status400) +import Network.Socket +import qualified Network.Socket as Socket import Network.Wai (Application, responseLBS, strictRequestBody) import qualified Network.Wai.Handler.Warp as Warp import qualified Network.Wai.Handler.Warp.Internal as Warp @@ -168,8 +171,7 @@ data Config = Config { privateKey :: FilePath, publicKey :: FilePath, cert :: FilePath, - botHost :: Text, - botPort :: Int + botHost :: Text } deriving (Show, Generic) @@ -634,14 +636,14 @@ testMessageBot config db brig galley cannon = withTestService config db brig def testMessageBotUtil quid uc cid pid sid sref buf brig galley cannon testBadFingerprint :: Config -> DB.ClientState -> Brig -> Galley -> Cannon -> Http () -testBadFingerprint config db brig galley _cannon = do +testBadFingerprint config db brig galley _cannon = withFreePortAnyAddr $ \(sPort, sock) -> do -- Generate a random key and register a service using that key sref <- withSystemTempFile "wire-provider.key" $ \fp h -> do ServiceKeyPEM key <- randServiceKey liftIO $ BS.hPut h (pemWriteBS key) >> hClose h - registerService config {publicKey = fp} db brig + registerService config {publicKey = fp} sPort db brig -- Run the service with a different key (i.e. the key from the config) - runService config defServiceApp $ \_ -> do + runService config sPort sock defServiceApp $ \_ -> do let pid = sref ^. serviceRefProvider let sid = sref ^. serviceRefId -- Prepare user with client @@ -686,14 +688,49 @@ testBotTeamOnlyConv config db brig galley cannon = withTestService config db bri _ <- waitFor (5 # Second) not (isMember galley lbuid cid) getBotConv galley bid cid !!! const 404 === statusCode - svcAssertConvAccessUpdate - buf - (tUntagged luid1) - (ConversationAccessData (Set.singleton InviteAccess) (Set.fromList [TeamMemberAccessRole])) - qcid - svcAssertMemberLeave buf (tUntagged lbuid) [tUntagged lbuid] qcid + -- Two events are sent concurrently: + -- - ConvAccessUpdate + -- - MemberLeave (for the bot) + -- + -- We cannot guarantee the order, so we have to check for both + let expectedConvAccessData = ConversationAccessData (Set.singleton InviteAccess) (Set.fromList [TeamMemberAccessRole]) + expectedMemberLeave = [tUntagged lbuid] + assertAndRetrieveEvent = do + event <- + timeout (5 # Second) (readChan buf) + >>= assertJust + >>= assertBotMessage + assertAccessUpdateOrMemberLeave (tUntagged luid1) expectedConvAccessData (tUntagged lbuid) expectedMemberLeave qcid event + pure event + event1 <- assertAndRetrieveEvent + event2 <- assertAndRetrieveEvent + -- Ensure there is exactly one of each types of event + liftIO $ + assertEqual + "there should be 1 ConvAccessUpdate and 1 MemberLeave event" + (Set.fromList [ConvAccessUpdate, MemberLeave]) + (Set.fromList (map evtType [event1, event2])) wsAssertMemberLeave ws qcid (tUntagged lbuid) [tUntagged lbuid] where + assertBotMessage :: (HasCallStack, MonadIO m) => TestBotEvent -> m Event + assertBotMessage = + liftIO . \case + TestBotMessage e -> pure e + evt -> assertFailure $ "expected TestBotMessage, got: " <> show evt + assertAccessUpdateOrMemberLeave :: (HasCallStack, MonadIO m) => Qualified UserId -> ConversationAccessData -> Qualified UserId -> [Qualified UserId] -> Qualified ConvId -> Event -> m () + assertAccessUpdateOrMemberLeave updFrom upd leaveFrom gone cnv e = liftIO $ + case evtType e of + ConvAccessUpdate -> do + assertEqual "conv" cnv (evtConv e) + assertEqual "user" updFrom (evtFrom e) + assertEqual "event data" (EdConvAccessUpdate upd) (evtData e) + MemberLeave -> do + let msg = QualifiedUserIdList gone + assertEqual "conv" cnv (evtConv e) + assertEqual "user" leaveFrom (evtFrom e) + assertEqual "event data" (EdMembersLeave msg) (evtData e) + _ -> + assertFailure $ "expected event of type: ConvAccessUpdate or MemberLeave, got: " <> show e setAccessRole uid qcid role = updateConversationAccess galley uid qcid [InviteAccess] role !!! const 200 === statusCode @@ -1378,7 +1415,7 @@ 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 postMessage :: Galley -> @@ -1696,6 +1733,12 @@ waitFor t f ma = do liftIO $ threadDelay (1 # Second) waitFor (t - 1 # Second) f ma +withFreePortAnyAddr :: (MonadMask m, MonadIO m) => ((Warp.Port, Socket) -> m a) -> m a +withFreePortAnyAddr = bracket openFreePortAnyAddr (liftIO . Socket.close . snd) + +openFreePortAnyAddr :: MonadIO m => m (Warp.Port, Socket) +openFreePortAnyAddr = liftIO $ bindRandomPortTCP "*" + -- | Run a test case with an external service application. withTestService :: Config -> @@ -1704,19 +1747,19 @@ withTestService :: (Chan e -> Application) -> (ServiceRef -> Chan e -> Http a) -> Http a -withTestService config db brig mkApp go = do - sref <- registerService config db brig - runService config mkApp (go sref) +withTestService config db brig mkApp go = withFreePortAnyAddr $ \(sPort, sock) -> do + sref <- registerService config sPort db brig + runService config sPort sock mkApp (go sref) -registerService :: Config -> DB.ClientState -> Brig -> Http ServiceRef -registerService config db brig = do +registerService :: Config -> Warp.Port -> DB.ClientState -> Brig -> Http ServiceRef +registerService config sPort db brig = do prv <- randomProvider db brig new <- defNewService config let Just url = fromByteString $ encodeUtf8 (botHost config) <> ":" - <> C8.pack (show (botPort config)) + <> C8.pack (show sPort) svc <- addGetService brig (providerId prv) (new {newServiceUrl = url}) let pid = providerId prv let sid = serviceId svc @@ -1725,16 +1768,18 @@ registerService config db brig = do runService :: Config -> + Warp.Port -> + Socket -> (Chan e -> Application) -> (Chan e -> Http a) -> Http a -runService config mkApp go = do +runService config sPort sock mkApp go = do let tlss = Warp.tlsSettings (cert config) (privateKey config) - let defs = Warp.defaultSettings {Warp.settingsPort = botPort config} + let defs = Warp.defaultSettings {Warp.settingsPort = sPort} buf <- liftIO newChan srv <- liftIO . Async.async $ - Warp.runTLS tlss defs $ + Warp.runTLSSocket tlss defs sock $ mkApp buf go buf `finally` liftIO (Async.cancel srv) @@ -1753,6 +1798,7 @@ data TestBot = TestBot data TestBotEvent = TestBotCreated TestBot | TestBotMessage Event + deriving (Show, Eq) -- TODO: Test that the authorization header is properly set defServiceApp :: Chan TestBotEvent -> Application @@ -1799,7 +1845,7 @@ defServiceApp buf = writeChan buf (TestBotMessage ev) k $ responseLBS status200 [] "success" -wsAssertMemberJoin :: MonadIO m => WS.WebSocket -> Qualified ConvId -> Qualified UserId -> [Qualified UserId] -> m () +wsAssertMemberJoin :: (HasCallStack, MonadIO m) => WS.WebSocket -> Qualified ConvId -> Qualified UserId -> [Qualified UserId] -> m () wsAssertMemberJoin ws conv usr new = void $ liftIO $ WS.assertMatch (5 # Second) ws $ @@ -1811,7 +1857,7 @@ wsAssertMemberJoin ws conv usr new = void $ evtFrom e @?= usr evtData e @?= EdMembersJoin (SimpleMembers (fmap (\u -> SimpleMember u roleNameWireAdmin) new)) -wsAssertMemberLeave :: MonadIO m => WS.WebSocket -> Qualified ConvId -> Qualified UserId -> [Qualified UserId] -> m () +wsAssertMemberLeave :: (HasCallStack, MonadIO m) => WS.WebSocket -> Qualified ConvId -> Qualified UserId -> [Qualified UserId] -> m () wsAssertMemberLeave ws conv usr old = void $ liftIO $ WS.assertMatch (5 # Second) ws $ @@ -1823,7 +1869,7 @@ wsAssertMemberLeave ws conv usr old = void $ evtFrom e @?= usr evtData e @?= EdMembersLeave (QualifiedUserIdList old) -wsAssertConvDelete :: MonadIO m => WS.WebSocket -> Qualified ConvId -> Qualified UserId -> m () +wsAssertConvDelete :: (HasCallStack, MonadIO m) => WS.WebSocket -> Qualified ConvId -> Qualified UserId -> m () wsAssertConvDelete ws conv from = void $ liftIO $ WS.assertMatch (5 # Second) ws $ @@ -1835,7 +1881,7 @@ wsAssertConvDelete ws conv from = void $ evtFrom e @?= from evtData e @?= EdConvDelete -wsAssertMessage :: MonadIO m => WS.WebSocket -> Qualified ConvId -> Qualified UserId -> ClientId -> ClientId -> Text -> m () +wsAssertMessage :: (HasCallStack, MonadIO m) => WS.WebSocket -> Qualified ConvId -> Qualified UserId -> ClientId -> ClientId -> Text -> m () wsAssertMessage ws conv fromu fromc to txt = void $ liftIO $ WS.assertMatch (5 # Second) ws $ @@ -1847,7 +1893,7 @@ wsAssertMessage ws conv fromu fromc to txt = void $ evtFrom e @?= fromu evtData e @?= EdOtrMessage (OtrMessage fromc to txt (Just "data")) -svcAssertMemberJoin :: MonadIO m => Chan TestBotEvent -> Qualified UserId -> [Qualified UserId] -> Qualified ConvId -> m () +svcAssertMemberJoin :: (HasCallStack, MonadIO m) => Chan TestBotEvent -> Qualified UserId -> [Qualified UserId] -> Qualified ConvId -> m () svcAssertMemberJoin buf usr new cnv = liftIO $ do evt <- timeout (5 # Second) $ readChan buf case evt of @@ -1859,7 +1905,7 @@ svcAssertMemberJoin buf usr new cnv = liftIO $ do assertEqual "event data" (EdMembersJoin msg) (evtData e) _ -> assertFailure "Event timeout (TestBotMessage: member-join)" -svcAssertMemberLeave :: MonadIO m => Chan TestBotEvent -> Qualified UserId -> [Qualified UserId] -> Qualified ConvId -> m () +svcAssertMemberLeave :: (HasCallStack, MonadIO m) => Chan TestBotEvent -> Qualified UserId -> [Qualified UserId] -> Qualified ConvId -> m () svcAssertMemberLeave buf usr gone cnv = liftIO $ do evt <- timeout (5 # Second) $ readChan buf case evt of @@ -1871,20 +1917,7 @@ svcAssertMemberLeave buf usr gone cnv = liftIO $ do assertEqual "event data" (EdMembersLeave msg) (evtData e) _ -> assertFailure "Event timeout (TestBotMessage: member-leave)" -svcAssertConvAccessUpdate :: MonadIO m => Chan TestBotEvent -> Qualified UserId -> ConversationAccessData -> Qualified ConvId -> m () -svcAssertConvAccessUpdate buf usr upd cnv = liftIO $ do - evt <- timeout (5 # Second) $ readChan buf - case evt of - Just (TestBotMessage e) -> do - -- FUTUREWORK: Sometimes the assertion on the event type fails, but not - -- always. See https://wearezeta.atlassian.net/browse/BE-522. - assertEqual "event type" ConvAccessUpdate (evtType e) - assertEqual "conv" cnv (evtConv e) - assertEqual "user" usr (evtFrom e) - assertEqual "event data" (EdConvAccessUpdate upd) (evtData e) - _ -> assertFailure "Event timeout (TestBotMessage: conv-access-update)" - -svcAssertConvDelete :: MonadIO m => Chan TestBotEvent -> Qualified UserId -> Qualified ConvId -> m () +svcAssertConvDelete :: (HasCallStack, MonadIO m) => Chan TestBotEvent -> Qualified UserId -> Qualified ConvId -> m () svcAssertConvDelete buf usr cnv = liftIO $ do evt <- timeout (5 # Second) $ readChan buf case evt of @@ -1895,7 +1928,7 @@ svcAssertConvDelete buf usr cnv = liftIO $ do assertEqual "event data" EdConvDelete (evtData e) _ -> assertFailure "Event timeout (TestBotMessage: conv-delete)" -svcAssertBotCreated :: MonadIO m => Chan TestBotEvent -> BotId -> ConvId -> m TestBot +svcAssertBotCreated :: (HasCallStack, MonadIO m) => Chan TestBotEvent -> BotId -> ConvId -> m TestBot svcAssertBotCreated buf bid cid = liftIO $ do evt <- timeout (5 # Second) $ readChan buf case evt of @@ -1907,7 +1940,7 @@ svcAssertBotCreated buf bid cid = liftIO $ do pure b _ -> assertFailure "Event timeout (TestBotCreated)" -svcAssertMessage :: MonadIO m => Chan TestBotEvent -> Qualified UserId -> OtrMessage -> Qualified ConvId -> m () +svcAssertMessage :: (HasCallStack, MonadIO m) => Chan TestBotEvent -> Qualified UserId -> OtrMessage -> Qualified ConvId -> m () svcAssertMessage buf from msg cnv = liftIO $ do evt <- timeout (5 # Second) $ readChan buf case evt of @@ -1918,7 +1951,7 @@ svcAssertMessage buf from msg cnv = liftIO $ do assertEqual "event data" (EdOtrMessage msg) (evtData e) _ -> assertFailure "Event timeout (TestBotMessage: otr-message-add)" -svcAssertEventuallyConvDelete :: MonadIO m => Chan TestBotEvent -> Qualified UserId -> Qualified ConvId -> m () +svcAssertEventuallyConvDelete :: (HasCallStack, MonadIO m) => Chan TestBotEvent -> Qualified UserId -> Qualified ConvId -> m () svcAssertEventuallyConvDelete buf usr cnv = liftIO $ do evt <- timeout (5 # Second) $ readChan buf case evt of @@ -1944,7 +1977,7 @@ mkMessage fromc rcps = ] where mk (u, c, m) = (text u, HashMap.singleton (text c) m) - text :: (FromByteString a, ToByteString a) => a -> Text + text :: ToByteString a => a -> Text text = fromJust . fromByteString . toByteString' -- | A list of 20 services, all having names that begin with the given prefix. diff --git a/services/brig/test/integration/API/RichInfo/Util.hs b/services/brig/test/integration/API/RichInfo/Util.hs index 8607b5fe07..896f90735b 100644 --- a/services/brig/test/integration/API/RichInfo/Util.hs +++ b/services/brig/test/integration/API/RichInfo/Util.hs @@ -1,3 +1,6 @@ +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH diff --git a/services/brig/test/integration/API/Search.hs b/services/brig/test/integration/API/Search.hs index 9bf6ace507..8637659a32 100644 --- a/services/brig/test/integration/API/Search.hs +++ b/services/brig/test/integration/API/Search.hs @@ -680,7 +680,7 @@ indexProxyServer idx opts mgr = else Wai.WPRResponse (Wai.responseLBS HTTP.status400 [] $ "Refusing to proxy to path=" <> cs (Wai.rawPathInfo req)) in waiProxyTo proxyApp Wai.defaultOnExc mgr -waitForTaskToComplete :: forall a m. (ES.MonadBH m, MonadIO m, MonadThrow m, FromJSON a) => ES.TaskNodeId -> m () +waitForTaskToComplete :: forall a m. (ES.MonadBH m, MonadThrow m, FromJSON a) => ES.TaskNodeId -> m () waitForTaskToComplete taskNodeId = do let policy = constantDelay 100000 <> limitRetries 30 let retryCondition _ = fmap not . isTaskComplete diff --git a/services/brig/test/integration/API/Search/Util.hs b/services/brig/test/integration/API/Search/Util.hs index 48cdead2c1..f4636026b8 100644 --- a/services/brig/test/integration/API/Search/Util.hs +++ b/services/brig/test/integration/API/Search/Util.hs @@ -47,7 +47,7 @@ executeSearch' brig self q maybeDomain maybeSize = do Brig -> UserId -> Text -> Maybe Domain -> Maybe Int -> m ResponseLBS +searchRequest :: MonadHttp m => Brig -> UserId -> Text -> Maybe Domain -> Maybe Int -> m ResponseLBS searchRequest brig self q maybeDomain maybeSize = do get ( brig @@ -59,11 +59,11 @@ searchRequest brig self q maybeDomain maybeSize = do ) -- | ES is only refreshed occasionally; we don't want to wait for that in tests. -refreshIndex :: (Monad m, MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> m () +refreshIndex :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> m () refreshIndex brig = post (brig . path "/i/index/refresh") !!! const 200 === statusCode -reindex :: (Monad m, MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> m () +reindex :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> m () reindex brig = post (brig . path "/i/index/reindex") !!! const 200 === statusCode diff --git a/services/brig/test/integration/API/SystemSettings.hs b/services/brig/test/integration/API/SystemSettings.hs index a46e631b05..ea4b11a3a0 100644 --- a/services/brig/test/integration/API/SystemSettings.hs +++ b/services/brig/test/integration/API/SystemSettings.hs @@ -1,3 +1,20 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + module API.SystemSettings (tests) where import Bilge @@ -5,6 +22,7 @@ import Bilge.Assert import Brig.Options import Control.Lens import qualified Data.ByteString.Char8 as BS +import Data.Id import Imports import Network.Wai.Test as WaiTest import Test.Tasty @@ -17,7 +35,8 @@ tests :: Opts -> Manager -> IO TestTree tests opts m = pure $ do testGroup "settings" - [ test m "GET /system/settings/unauthorized" $ testGetSettings opts + [ test m "GET /system/settings/unauthorized" $ testGetSettings opts, + test m "GET /system/settings" $ testGetSettingsInternal opts ] testGetSettings :: Opts -> Http () @@ -35,14 +54,34 @@ testGetSettings opts = liftIO $ do -- made. This happens due to the `MonadHttp WaiTest.Session` instance. queriedSettings <- withSettingsOverrides newOpts $ getSystemSettings liftIO $ - queriedSettings @?= SystemSettings expectedRes + queriedSettings @?= SystemSettingsPublic expectedRes + + getSystemSettings :: WaiTest.Session SystemSettingsPublic + getSystemSettings = + responseJsonError + =<< get (path (BS.pack ("/" ++ latestVersion ++ "/system/settings/unauthorized"))) + Http () +testGetSettingsInternal opts = liftIO $ do + uid <- randomId + expectResultForEnableMls uid Nothing False + expectResultForEnableMls uid (Just False) False + expectResultForEnableMls uid (Just True) True where - latestVersion :: String - latestVersion = map toLower $ show (maxBound :: Version) + expectResultForEnableMls :: UserId -> Maybe Bool -> Bool -> IO () + expectResultForEnableMls uid setEnableMlsValue expectedRes = do + let newOpts = opts & (optionSettings . enableMLS) .~ setEnableMlsValue + -- Run call in `WaiTest.Session` with an adjusted brig `Application`. I.e. + -- the response is created by running the brig `Application` (with + -- modified options) directly on the `Request`. No real HTTP request is + -- made. This happens due to the `MonadHttp WaiTest.Session` instance. + queriedSettings <- withSettingsOverrides newOpts $ getSystemSettings uid + liftIO $ ssInternal queriedSettings @?= SystemSettingsInternal expectedRes + + getSystemSettings :: UserId -> WaiTest.Session SystemSettings + getSystemSettings uid = + responseJsonError =<< get (paths ["system", "settings"] . zUser uid) Manager -> Nginz -> Brig -> Cannon -> Galley -> AWS.Env -> IO TestTree +tests :: Opt.Opts -> Manager -> Nginz -> Brig -> Cannon -> Galley -> UserJournalWatcher -> IO TestTree tests conf m n b c g aws = do let tl = TeamSizeLimit . Opt.setMaxTeamSize . Opt.optSettings $ conf let it = Opt.setTeamInvitationTimeout . Opt.optSettings $ conf @@ -97,20 +96,20 @@ tests conf m n b c g aws = do test m "post /teams/:tid/invitations - email lookup nginz" $ testInvitationEmailLookupNginz b n, test m "post /teams/:tid/invitations - email lookup register" $ testInvitationEmailLookupRegister b, test m "post /teams/:tid/invitations - 403 no permission" $ testInvitationNoPermission b, - test m "post /teams/:tid/invitations - 403 too many pending" $ testInvitationTooManyPending b tl, + test m "post /teams/:tid/invitations - 403 too many pending" $ testInvitationTooManyPending conf b tl, test m "post /teams/:tid/invitations - roles" $ testInvitationRoles b g, - test' aws m "post /register - 201 accepted" $ testInvitationEmailAccepted b g, - test' aws m "post /register - 201 accepted (with domain blocking customer extension)" $ testInvitationEmailAcceptedInBlockedDomain conf b g, - test' aws m "post /register - 201 extended accepted" $ testInvitationEmailAndPhoneAccepted b g, - test' aws m "post /register user & team - 201 accepted" $ testCreateTeam b g aws, - test' aws m "post /register user & team - 201 preverified" $ testCreateTeamPreverified b g aws, + test m "post /register - 201 accepted" $ testInvitationEmailAccepted b g, + test m "post /register - 201 accepted (with domain blocking customer extension)" $ testInvitationEmailAcceptedInBlockedDomain conf b g, + test m "post /register - 201 extended accepted" $ testInvitationEmailAndPhoneAccepted b g, + test m "post /register user & team - 201 accepted" $ testCreateTeam b g aws, + test m "post /register user & team - 201 preverified" $ testCreateTeamPreverified b g aws, test m "post /register - 400 no passwordless" $ testTeamNoPassword b, test m "post /register - 400 code already used" $ testInvitationCodeExists b, test m "post /register - 400 bad code" $ testInvitationInvalidCode b, test m "post /register - 400 no wireless" $ testInvitationCodeNoIdentity b, test m "post /register - 400 mutually exclusive" $ testInvitationMutuallyExclusive b, test m "post /register - 403 too many members" $ testInvitationTooManyMembers b g tl, - test m "get /teams/:tid/invitations - 200 (paging)" $ testInvitationPaging b, + test m "get /teams/:tid/invitations - 200 (paging)" $ testInvitationPaging conf b, test m "get /teams/:tid/invitations/info - 200" $ testInvitationInfo b, test m "get /teams/:tid/invitations/info - 400" $ testInvitationInfoBadCode b, test m "get /teams/:tid/invitations/info - 400 expired" $ testInvitationInfoExpired b it, @@ -355,14 +354,17 @@ headInvitationByEmail service email expectedCode = Bilge.head (service . path "/teams/invitations/by-email" . contentJson . queryItem "email" (toByteString' email)) !!! const expectedCode === statusCode -testInvitationTooManyPending :: Brig -> TeamSizeLimit -> Http () -testInvitationTooManyPending brig (TeamSizeLimit limit) = do +testInvitationTooManyPending :: Opt.Opts -> Brig -> TeamSizeLimit -> Http () +testInvitationTooManyPending opts brig (TeamSizeLimit limit) = do (inviter, tid) <- createUserWithTeam brig emails <- replicateConcurrently (fromIntegral limit) randomEmail - pooledForConcurrentlyN_ 16 emails $ postInvitation brig tid inviter . stdInvitationRequest email <- randomEmail - -- TODO: If this test takes longer to run than `team-invitation-timeout`, then some of the - -- invitations have likely expired already and this test will actually _fail_ + -- If this test takes longer to run than `team-invitation-timeout`, then some of the + -- invitations have likely expired already and this test will actually _fail_ + -- therefore we increase the timeout from default 10 to 300 seconds + let longerTimeout = opts {Opt.optSettings = (Opt.optSettings opts) {Opt.setTeamInvitationTimeout = 300}} + withSettingsOverrides longerTimeout $ do + forM_ emails $ postInvitation brig tid inviter . stdInvitationRequest postInvitation brig tid inviter (stdInvitationRequest email) !!! do const 403 === statusCode const (Just "too-many-team-invitations") === fmap Error.label . responseJsonMaybe @@ -470,7 +472,6 @@ createAndVerifyInvitation' :: ( HasCallStack, MonadIO m, MonadHttp m, - MonadThrow m, MonadCatch m, MonadFail m, a ~ (Maybe (UserId, UTCTimeMillis), Invitation, UserId, ResponseLBS) @@ -488,7 +489,6 @@ createAndVerifyInvitation' replacementBrigApp acceptFn invite brig galley = do ( HasCallStack, MonadIO m', MonadHttp m', - MonadThrow m', MonadCatch m', MonadFail m' ) => @@ -526,8 +526,8 @@ createAndVerifyInvitation' replacementBrigApp acceptFn invite brig galley = do liftIO $ assertBool "User should have no connections" (null (clConnections conns) && not (clHasMore conns)) pure (responseJsonMaybe rsp2, invitation) -testCreateTeam :: Brig -> Galley -> AWS.Env -> Http () -testCreateTeam brig galley aws = do +testCreateTeam :: Brig -> Galley -> UserJournalWatcher -> Http () +testCreateTeam brig galley userJournalWatcher = do email <- randomEmail usr <- responseJsonError =<< register email newTeam brig let uid = userId usr @@ -549,13 +549,13 @@ testCreateTeam brig galley aws = do case act of Nothing -> liftIO $ assertFailure "activation key/code not found" Just kc -> activate brig kc !!! const 200 === statusCode - liftIO $ Util.assertUserJournalQueue "user activate" aws (userActivateJournaled usr) + Util.assertUserActivateJournaled userJournalWatcher usr "user activate" -- Verify that Team has status Active now team3 <- getTeam galley (team ^. teamId) liftIO $ assertEqual "status" Team.Active (Team.tdStatus team3) -testCreateTeamPreverified :: Brig -> Galley -> AWS.Env -> Http () -testCreateTeamPreverified brig galley aws = do +testCreateTeamPreverified :: Brig -> Galley -> UserJournalWatcher -> Http () +testCreateTeamPreverified brig galley userJournalWatcher = do email <- randomEmail requestActivationCode brig 200 (Left email) act <- getActivationCode brig (Left email) @@ -564,7 +564,7 @@ testCreateTeamPreverified brig galley aws = do Just (_, c) -> do usr <- responseJsonError =<< register' email newTeam c brig getTeams uid galley liftIO $ assertBool "User not part of exactly one team" (length teams == 1) let team = fromMaybe (error "No team??") $ listToMaybe teams @@ -722,30 +722,36 @@ testInvitationTooManyMembers brig galley (TeamSizeLimit limit) = do const 403 === statusCode const (Just "too-many-team-members") === fmap Error.label . responseJsonMaybe -testInvitationPaging :: HasCallStack => Brig -> Http () -testInvitationPaging brig = do +testInvitationPaging :: HasCallStack => Opt.Opts -> Brig -> Http () +testInvitationPaging opts brig = do before <- liftIO $ toUTCTimeMillis . addUTCTime (-1) <$> getCurrentTime (uid, tid) <- createUserWithTeam brig let total = 5 invite email = stdInvitationRequest email - emails <- replicateM total $ do - email <- randomEmail - postInvitation brig tid uid (invite email) !!! const 201 === statusCode - pure email + longerTimeout = opts {Opt.optSettings = (Opt.optSettings opts) {Opt.setTeamInvitationTimeout = 300}} + emails <- + withSettingsOverrides longerTimeout $ + replicateM total $ do + email <- randomEmail + postInvitation brig tid uid (invite email) !!! const 201 === statusCode + pure email after1ms <- liftIO $ toUTCTimeMillis . addUTCTime 1 <$> getCurrentTime - let next :: HasCallStack => Int -> (Int, Maybe InvitationId) -> Int -> Http (Int, Maybe InvitationId) - next step (count, start) actualPageLen = do - let count' = count + step + let getPages :: HasCallStack => Int -> Maybe InvitationId -> Int -> Http [[Invitation]] + getPages count start step = 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 - liftIO $ validateInv `mapM_` invs - pure (count', fmap inInvitation . listToMaybe . reverse $ invs) + (invs, more) <- (ilInvitations &&& ilHasMore) <$> responseJsonError r + if more + then (invs :) <$> getPages (count + step) (fmap inInvitation . listToMaybe . reverse $ invs) step + else pure [invs] + let checkSize :: HasCallStack => Int -> [Int] -> Http () + checkSize pageSize expectedSizes = + getPages 0 Nothing pageSize >>= \invss -> liftIO $ do + assertEqual "page sizes" expectedSizes (take (length expectedSizes) (map length invss)) + mapM_ validateInv $ concat invss validateInv :: Invitation -> Assertion validateInv inv = do assertEqual "tid" tid (inTeam inv) @@ -756,9 +762,9 @@ testInvitationPaging brig = do assertEqual "uid" (Just uid) (inCreatedBy inv) -- not checked: @inInvitation inv :: InvitationId@ - foldM_ (next 2) (0, Nothing) [2, 2, 1, 0] - foldM_ (next total) (0, Nothing) [total, 0] - foldM_ (next (total + 1)) (0, Nothing) [total, 0] + checkSize 2 [2, 2, 1] + checkSize total [total] + checkSize (total + 1) [total] testInvitationInfo :: Brig -> Http () testInvitationInfo brig = do diff --git a/services/brig/test/integration/API/Team/Util.hs b/services/brig/test/integration/API/Team/Util.hs index c1a6724b9d..b809b89c48 100644 --- a/services/brig/test/integration/API/Team/Util.hs +++ b/services/brig/test/integration/API/Team/Util.hs @@ -1,4 +1,6 @@ {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- @@ -23,7 +25,7 @@ import API.User.Util import Bilge hiding (accept, head, timeout) import Bilge.Assert import Control.Lens ((^?)) -import Control.Monad.Catch (MonadCatch, MonadThrow) +import Control.Monad.Catch (MonadCatch) import Data.Aeson hiding (json) import Data.Aeson.Lens import Data.ByteString.Conversion @@ -32,7 +34,6 @@ import Data.Misc (Milliseconds) import Data.Range import qualified Data.Set as Set import qualified Data.Text.Encoding as T -import qualified Galley.Types.Teams.Intra as Team import Imports import qualified Network.Wai.Utilities.Error as Error import Test.Tasty.HUnit @@ -41,6 +42,7 @@ import Web.Cookie (parseSetCookie, setCookieName) import Wire.API.Conversation import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role +import qualified Wire.API.Routes.Internal.Galley.TeamsIntra as Team import Wire.API.Team hiding (newTeam) import Wire.API.Team.Feature (FeatureStatus (..)) import qualified Wire.API.Team.Feature as Public @@ -128,7 +130,7 @@ createTeam u galley = do -- | Create user and binding team. -- -- NB: the created user is the team owner. -createUserWithTeam :: (MonadIO m, MonadHttp m, MonadCatch m, MonadThrow m) => Brig -> m (UserId, TeamId) +createUserWithTeam :: (MonadIO m, MonadHttp m, MonadCatch m) => Brig -> m (UserId, TeamId) createUserWithTeam brig = do (user, tid) <- createUserWithTeam' brig pure (userId user, tid) @@ -136,7 +138,7 @@ createUserWithTeam brig = do -- | Create user and binding team. -- -- NB: the created user is the team owner. -createUserWithTeam' :: (MonadIO m, MonadHttp m, MonadCatch m, MonadThrow m, HasCallStack) => Brig -> m (User, TeamId) +createUserWithTeam' :: (MonadIO m, MonadHttp m, MonadCatch m, HasCallStack) => Brig -> m (User, TeamId) createUserWithTeam' brig = do e <- randomEmail n <- randomName @@ -148,7 +150,10 @@ createUserWithTeam' brig = do "password" .= defPassword, "team" .= newTeam ] - user <- responseJsonError =<< post (brig . path "/i/users" . contentJson . body p) + user <- + responseJsonError + =<< post (brig . path "/i/users" . contentJson . body p) + getSelfProfile brig (userId user) liftIO $ assertBool "Team ID in self profile and team table do not match" (selfTeam == Just tid) @@ -231,7 +236,6 @@ createTeamConvWithRole role g tid u us mtimer = do Nothing role ProtocolProteusTag - Nothing r <- post ( g @@ -382,7 +386,7 @@ 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) => + (MonadHttp m, HasCallStack) => Brig -> TeamId -> UserId -> diff --git a/services/brig/test/integration/API/User.hs b/services/brig/test/integration/API/User.hs index f2c41833e2..2b6adebc40 100644 --- a/services/brig/test/integration/API/User.hs +++ b/services/brig/test/integration/API/User.hs @@ -39,6 +39,7 @@ import Data.List.NonEmpty (NonEmpty ((:|))) import Imports import Test.Tasty hiding (Timeout) import Util +import Util.AWS (UserJournalWatcher) import Util.Options.Common import Wire.API.Federation.Component @@ -54,8 +55,9 @@ tests :: Nginz -> AWS.Env -> DB.ClientState -> + UserJournalWatcher -> IO TestTree -tests conf fbc fgc p b c ch g n aws db = do +tests conf fbc fgc p b c ch g n aws db userJournalWatcher = do let cl = ConnectionLimit $ Opt.setUserMaxConnections (Opt.optSettings conf) let at = Opt.setActivationTimeout (Opt.optSettings conf) z <- mkZAuthEnv (Just conf) @@ -63,7 +65,7 @@ tests conf fbc fgc p b c ch g n aws db = do testGroup "user" [ API.User.Client.tests cl at conf p db b c g, - API.User.Account.tests cl at conf p b c ch g aws, + API.User.Account.tests cl at conf p b c ch g aws userJournalWatcher, API.User.Auth.tests conf p z db b g n, API.User.Connection.tests cl at conf p b c g fbc fgc db, API.User.Handles.tests cl at conf p b c g, diff --git a/services/brig/test/integration/API/User/Account.hs b/services/brig/test/integration/API/User/Account.hs index 2de33cc489..639686ba81 100644 --- a/services/brig/test/integration/API/User/Account.hs +++ b/services/brig/test/integration/API/User/Account.hs @@ -1,4 +1,4 @@ -{-# OPTIONS_GHC -Wno-deferred-out-of-scope-variables #-} +{-# LANGUAGE NumericUnderscores #-} {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} -- This file is part of the Wire Server implementation. @@ -99,77 +99,77 @@ import Wire.API.User.Auth import qualified Wire.API.User.Auth as Auth import Wire.API.User.Client -tests :: ConnectionLimit -> Opt.Timeout -> Opt.Opts -> Manager -> Brig -> Cannon -> CargoHold -> Galley -> AWS.Env -> TestTree -tests _ at opts p b c ch g aws = +tests :: ConnectionLimit -> Opt.Timeout -> Opt.Opts -> Manager -> Brig -> Cannon -> CargoHold -> Galley -> AWS.Env -> UserJournalWatcher -> TestTree +tests _ at opts p b c ch g aws userJournalWatcher = testGroup "account" - [ test' aws p "post /register - 201 (with preverified)" $ testCreateUserWithPreverified opts b aws, - test' aws p "post /register - 400 (with preverified)" $ testCreateUserWithInvalidVerificationCode b, - test' aws p "post /register - 201" $ testCreateUser b g, - test' aws p "post /register - 201 + no email" $ testCreateUserNoEmailNoPassword b, - test' aws p "post /register - 201 anonymous" $ testCreateUserAnon b g, - test' aws p "post /register - 400 empty name" $ testCreateUserEmptyName b, - test' aws p "post /register - 400 name too long" $ testCreateUserLongName b, - test' aws p "post /register - 201 anonymous expiry" $ testCreateUserAnonExpiry b, - test' aws p "post /register - 201 pending" $ testCreateUserPending opts b, - test' aws p "post /register - 201 existing activation" $ testCreateAccountPendingActivationKey opts b, - test' aws p "post /register - 409 conflict" $ testCreateUserConflict opts b, - test' aws p "post /register - 400 invalid input" $ testCreateUserInvalidEmailOrPhone opts b, - test' aws p "post /register - 403 blacklist" $ testCreateUserBlacklist opts b aws, - test' aws p "post /register - 400 external-SSO" $ testCreateUserExternalSSO b, - test' aws p "post /register - 403 restricted user creation" $ testRestrictedUserCreation opts b, - test' aws p "post /register - 403 too many members for legalhold" $ testTooManyMembersForLegalhold opts b, - test' aws p "post /activate - 200/204 + expiry" $ testActivateWithExpiry opts b at, - test' aws p "get /users/:uid - 404" $ testNonExistingUserUnqualified b, - test' aws p "get /users//:uid - 404" $ testNonExistingUser b, - test' aws p "get /users/:domain/:uid - 422" $ testUserInvalidDomain b, - test' aws p "get /users/:uid - 200" $ testExistingUserUnqualified b, - test' aws p "get /users//:uid - 200" $ testExistingUser b, - test' aws p "get /users?:id=.... - 200" $ testMultipleUsersUnqualified b, - test' aws p "head /users/:uid - 200" $ testUserExistsUnqualified b, - test' aws p "head /users/:uid - 404" $ testUserDoesNotExistUnqualified b, - test' aws p "head /users/:domain/:uid - 200" $ testUserExists b, - test' aws p "head /users/:domain/:uid - 404" $ testUserDoesNotExist b, - test' aws p "post /list-users - 200" $ testMultipleUsers b, - test' aws p "put /self - 200" $ testUserUpdate b c aws, - test' aws p "put /access/self/email - 2xx" $ testEmailUpdate b aws, - test' aws p "put /self/phone - 202" $ testPhoneUpdate b, - test' aws p "put /self/phone - 403" $ testPhoneUpdateBlacklisted b, - test' aws p "put /self/phone - 409" $ testPhoneUpdateConflict b, - test' aws p "head /self/password - 200/404" $ testPasswordSet b, - test' aws p "put /self/password - 200" $ testPasswordChange b, - test' aws p "put /self/locale - 200" $ testUserLocaleUpdate b aws, - test' aws p "post /activate/send - 200" $ testSendActivationCode opts b, - test' aws p "post /activate/send - 400 invalid input" $ testSendActivationCodeInvalidEmailOrPhone b, - test' aws p "post /activate/send - 403 prefix excluded" $ testSendActivationCodePrefixExcluded b, - test' aws p "post /i/users/phone-prefix" $ testInternalPhonePrefixes b, - test' aws p "put /i/users/:uid/status (suspend)" $ testSuspendUser b, - test' aws p "get /i/users?:(email|phone) - 200" $ testGetByIdentity b, + [ test p "post /register - 201 (with preverified)" $ testCreateUserWithPreverified opts b userJournalWatcher, + test p "post /register - 400 (with preverified)" $ testCreateUserWithInvalidVerificationCode b, + test p "post /register - 201" $ testCreateUser b g, + test p "post /register - 201 + no email" $ testCreateUserNoEmailNoPassword b, + test p "post /register - 201 anonymous" $ testCreateUserAnon b g, + test p "post /register - 400 empty name" $ testCreateUserEmptyName b, + test p "post /register - 400 name too long" $ testCreateUserLongName b, + test p "post /register - 201 anonymous expiry" $ testCreateUserAnonExpiry b, + test p "post /register - 201 pending" $ testCreateUserPending opts b, + test p "post /register - 201 existing activation" $ testCreateAccountPendingActivationKey opts b, + test p "post /register - 409 conflict" $ testCreateUserConflict opts b, + test p "post /register - 400 invalid input" $ testCreateUserInvalidEmailOrPhone opts b, + test p "post /register - 403 blacklist" $ testCreateUserBlacklist opts b aws, + test p "post /register - 400 external-SSO" $ testCreateUserExternalSSO b, + test p "post /register - 403 restricted user creation" $ testRestrictedUserCreation opts b, + test p "post /register - 403 too many members for legalhold" $ testTooManyMembersForLegalhold opts b, + test p "post /activate - 200/204 + expiry" $ testActivateWithExpiry opts b at, + test p "get /users/:uid - 404" $ testNonExistingUserUnqualified b, + test p "get /users//:uid - 404" $ testNonExistingUser b, + test p "get /users/:domain/:uid - 422" $ testUserInvalidDomain b, + test p "get /users/:uid - 200" $ testExistingUserUnqualified b, + test p "get /users//:uid - 200" $ testExistingUser b, + test p "get /users?:id=.... - 200" $ testMultipleUsersUnqualified b, + test p "head /users/:uid - 200" $ testUserExistsUnqualified b, + test p "head /users/:uid - 404" $ testUserDoesNotExistUnqualified b, + test p "head /users/:domain/:uid - 200" $ testUserExists b, + test p "head /users/:domain/:uid - 404" $ testUserDoesNotExist b, + test p "post /list-users - 200" $ testMultipleUsers b, + test p "put /self - 200" $ testUserUpdate b c userJournalWatcher, + test p "put /access/self/email - 2xx" $ testEmailUpdate b userJournalWatcher, + test p "put /self/phone - 202" $ testPhoneUpdate b, + test p "put /self/phone - 403" $ testPhoneUpdateBlacklisted b, + test p "put /self/phone - 409" $ testPhoneUpdateConflict b, + test p "head /self/password - 200/404" $ testPasswordSet b, + test p "put /self/password - 200" $ testPasswordChange b, + test p "put /self/locale - 200" $ testUserLocaleUpdate b userJournalWatcher, + test p "post /activate/send - 200" $ testSendActivationCode opts b, + test p "post /activate/send - 400 invalid input" $ testSendActivationCodeInvalidEmailOrPhone b, + test p "post /activate/send - 403 prefix excluded" $ testSendActivationCodePrefixExcluded b, + test p "post /i/users/phone-prefix" $ testInternalPhonePrefixes b, + test p "put /i/users/:uid/status (suspend)" $ testSuspendUser b, + test p "get /i/users?:(email|phone) - 200" $ testGetByIdentity b, -- "get /i/users?:ids=...&includePendingInvitations=..." is tested in 'testCreateUserNoIdP', 'testCreateUserTimeout' -- in spar's integration tests, module "Test.Spar.Scim.UserSpec" - test' aws p "delete/phone-email" $ testEmailPhoneDelete b c, - test' aws p "delete/by-password" $ testDeleteUserByPassword b c aws, - test' aws p "delete/with-legalhold" $ testDeleteUserWithLegalHold b c aws, - test' aws p "delete/by-code" $ testDeleteUserByCode b, - test' aws p "delete/anonymous" $ testDeleteAnonUser b, - test' aws p "delete with profile pic" $ testDeleteWithProfilePic b ch, - test' aws p "delete with connected remote users" $ testDeleteWithRemotes opts b, - test' aws p "delete with connected remote users and failed remote notifcations" $ testDeleteWithRemotesAndFailedNotifications opts b c, - test' aws p "put /i/users/:uid/sso-id" $ testUpdateSSOId b g, + test p "delete/phone-email" $ testEmailPhoneDelete b c, + test p "delete/by-password" $ testDeleteUserByPassword b c userJournalWatcher, + test p "delete/with-legalhold" $ testDeleteUserWithLegalHold b c userJournalWatcher, + test p "delete/by-code" $ testDeleteUserByCode b, + test p "delete/anonymous" $ testDeleteAnonUser b, + test p "delete with profile pic" $ testDeleteWithProfilePic b ch, + test p "delete with connected remote users" $ testDeleteWithRemotes opts b, + test p "delete with connected remote users and failed remote notifcations" $ testDeleteWithRemotesAndFailedNotifications opts b c, + test p "put /i/users/:uid/sso-id" $ testUpdateSSOId b g, testGroup "temporary customer extensions" - [ test' aws p "domains blocked for registration" $ testDomainsBlockedForRegistration opts b + [ test p "domains blocked for registration" $ testDomainsBlockedForRegistration opts b ], testGroup "update user email by team owner" - [ test' aws p "put /users/:uid/email" $ testUpdateUserEmailByTeamOwner b + [ test p "put /users/:uid/email" $ testUpdateUserEmailByTeamOwner opts b ], testGroup "delete /i/users/:uid" - [ test' aws p "does nothing for completely deleted user" $ testDeleteUserWithCompletelyDeletedUser b c aws, - test' aws p "does nothing when the user doesn't exist" $ testDeleteUserWithNoUser b, - test' aws p "deletes a not deleted user" $ testDeleteUserWithNotDeletedUser b c aws, - test' aws p "delete again because of dangling property" $ testDeleteUserWithDanglingProperty b c aws + [ test p "does nothing for completely deleted user" $ testDeleteUserWithCompletelyDeletedUser b c userJournalWatcher, + test p "does nothing when the user doesn't exist" $ testDeleteUserWithNoUser b, + test p "deletes a not deleted user" $ testDeleteUserWithNotDeletedUser b c userJournalWatcher, + test p "delete again because of dangling property" $ testDeleteUserWithDanglingProperty b c userJournalWatcher ] ] @@ -201,8 +201,8 @@ testCreateUserWithInvalidVerificationCode brig = do -- @END -testUpdateUserEmailByTeamOwner :: Brig -> Http () -testUpdateUserEmailByTeamOwner brig = do +testUpdateUserEmailByTeamOwner :: Opt.Opts -> Brig -> Http () +testUpdateUserEmailByTeamOwner opts brig = do (_, teamOwner, emailOwner : otherTeamMember : _) <- createPopulatedBindingTeamWithNamesAndHandles brig 2 (teamOwnerDifferentTeam, _) <- createUserWithTeam' brig newEmail <- randomEmail @@ -224,8 +224,8 @@ testUpdateUserEmailByTeamOwner brig = do where checkLetActivationExpire :: Email -> Http () checkLetActivationExpire email = do - -- assumption: `optSettings.setActivationTimeout = 5` in `brig.yaml` - threadDelay (5100 * 1000) + let timeout = round (Opt.setActivationTimeout (Opt.optSettings opts)) + threadDelay ((timeout + 1) * 1000_000) checkActivationCode email False checkActivationCode :: Email -> Bool -> Http () @@ -246,8 +246,8 @@ testUpdateUserEmailByTeamOwner brig = do setUserEmail brig (userId teamOwnerDifferentTeam) (userId emailOwner) email !!! (const 404 === statusCode) setUserEmail brig (userId otherTeamMember) (userId emailOwner) email !!! (const 403 === statusCode) -testCreateUserWithPreverified :: Opt.Opts -> Brig -> AWS.Env -> Http () -testCreateUserWithPreverified opts brig aws = do +testCreateUserWithPreverified :: Opt.Opts -> Brig -> UserJournalWatcher -> Http () +testCreateUserWithPreverified opts brig userJournalWatcher = do -- Register (pre verified) user with phone p <- randomPhone let phoneReq = RequestBodyLBS . encode $ object ["phone" .= fromPhone p] @@ -274,7 +274,7 @@ testCreateUserWithPreverified opts brig aws = do const (Just p) === (userPhone <=< responseJsonMaybe) -- check /self returns the qualified_id field in the response const (Just (Qualified uid domain)) === (fmap userQualifiedId . responseJsonMaybe) - liftIO $ Util.assertUserJournalQueue "user activate" aws (userActivateJournaled usr) + Util.assertUserActivateJournaled userJournalWatcher usr "user activate" -- Register (pre verified) user with email e <- randomEmail let emailReq = RequestBodyLBS . encode $ object ["email" .= fromEmail e] @@ -298,8 +298,7 @@ testCreateUserWithPreverified opts brig aws = do get (brig . path "/self" . zUser uid) !!! do const 200 === statusCode const (Just e) === (userEmail <=< responseJsonMaybe) - liftIO $ Util.assertUserJournalQueue "user activate" aws (userActivateJournaled usr) - liftIO $ Util.assertEmptyUserJournalQueue aws + Util.assertUserActivateJournaled userJournalWatcher usr "user activate" testCreateUser :: Brig -> Galley -> Http () -- TODO: this has nothing to do with /register. what's going on here? testCreateUser brig galley = do @@ -835,14 +834,14 @@ testCreateUserAnonExpiry b = do field :: FromJSON a => Key -> Value -> Maybe a field f u = u ^? key f >>= maybeFromJSON -testUserUpdate :: HasCallStack => Brig -> Cannon -> AWS.Env -> Http () -testUserUpdate brig cannon aws = do +testUserUpdate :: HasCallStack => Brig -> Cannon -> UserJournalWatcher -> Http () +testUserUpdate brig cannon userJournalWatcher = do aliceUser <- randomUser brig - liftIO $ Util.assertUserJournalQueue "user create alice" aws (userActivateJournaled aliceUser) + Util.assertUserActivateJournaled userJournalWatcher aliceUser "user create alice" let alice = userId aliceUser aliceQ = userQualifiedId aliceUser bobUser <- randomUser brig - liftIO $ Util.assertUserJournalQueue "user create bob" aws (userActivateJournaled bobUser) + Util.assertUserActivateJournaled userJournalWatcher bobUser "user create bob" let bob = userId bobUser aliceNewName <- randomName connectUsers brig alice (singleton bob) @@ -863,7 +862,7 @@ testUserUpdate brig cannon aws = do !!! const 200 === statusCode liftIO $ mapConcurrently_ (\ws -> assertUpdateNotification ws alice userUpdate) [aliceWS, bobWS] -- Should generate a user update journaled event with the new name - liftIO $ Util.assertUserJournalQueue "user update" aws (userUpdateJournaled alice userUpdate) + Util.assertNameUpdateJournaled userJournalWatcher alice aliceNewName "alice name update" -- get the updated profile get (brig . path "/self" . zUser alice) !!! do const 200 === statusCode @@ -883,10 +882,10 @@ testUserUpdate brig cannon aws = do -- This tests the behavior of `/i/self/email` instead of `/self/email` or -- `/access/self/email`. tests for session token handling under `/access/self/email` are in -- `services/brig/test/integration/API/User/Auth.hs`. -testEmailUpdate :: Brig -> AWS.Env -> Http () -testEmailUpdate brig aws = do +testEmailUpdate :: Brig -> UserJournalWatcher -> Http () +testEmailUpdate brig userJournalWatcher = do usr <- randomUser brig - liftIO $ Util.assertUserJournalQueue "user create random" aws (userActivateJournaled usr) + Util.assertUserActivateJournaled userJournalWatcher usr "user create random" let uid = userId usr eml <- randomEmail -- update email @@ -895,7 +894,7 @@ testEmailUpdate brig aws = do -- activate activateEmail brig eml checkEmail brig uid eml - liftIO $ Util.assertUserJournalQueue "user update" aws (userEmailUpdateJournaled uid eml) + Util.assertEmailUpdateJournaled userJournalWatcher uid eml "user update" -- update email, which is exactly the same as before (idempotency) initiateEmailUpdateLogin brig eml (emailLogin eml defPassword Nothing) uid !!! const 204 === statusCode -- ensure no other user has "test+@example.com" @@ -918,14 +917,14 @@ testEmailUpdate brig aws = do responseJsonMaybe <$> login brig (defEmailLogin eml) SessionCookie for_ tk $ \t -> do deleteUser (Auth.user t) (Just defPassword) brig !!! const 200 === statusCode - liftIO $ Util.assertUserJournalQueue "user deletion" aws (userDeleteJournaled $ Auth.user t) + Util.assertDeleteJournaled userJournalWatcher (Auth.user t) "user deletion" initiateUpdateAndActivate :: Email -> UserId -> Http () initiateUpdateAndActivate eml uid = do initiateEmailUpdateNoSend brig eml uid !!! const 202 === statusCode activateEmail brig eml checkEmail brig uid eml - liftIO $ Util.assertUserJournalQueue "user update" aws (userEmailUpdateJournaled uid eml) + Util.assertEmailUpdateJournaled userJournalWatcher uid eml "user update" -- Ensure login work both with the full email and the "short" version login brig (defEmailLogin eml) SessionCookie !!! const 200 === statusCode login brig (defEmailLogin (Email "test" "example.com")) SessionCookie !!! const 200 === statusCode @@ -996,21 +995,21 @@ testCreateAccountPendingActivationKey _ brig = do -- try to activate already active phone activate brig kc !!! const 409 === statusCode -testUserLocaleUpdate :: Brig -> AWS.Env -> Http () -testUserLocaleUpdate brig aws = do +testUserLocaleUpdate :: Brig -> UserJournalWatcher -> Http () +testUserLocaleUpdate brig userJournalWatcher = do usr <- randomUser brig - liftIO $ Util.assertUserJournalQueue "user create random" aws (userActivateJournaled usr) + Util.assertUserActivateJournaled userJournalWatcher usr "user create random" let uid = userId usr -- update locale info with locale supported in templates let locEN = fromMaybe (error "Failed to parse locale") $ parseLocale "en-US" put (brig . path "/self/locale" . contentJson . zUser uid . zConn "c" . locale locEN) !!! const 200 === statusCode - liftIO $ Util.assertUserJournalQueue "user update" aws (userLocaleUpdateJournaled uid locEN) + Util.assertLocaleUpdateJournaled userJournalWatcher uid locEN "user update" -- update locale info with locale NOT supported in templates let locPT = fromMaybe (error "Failed to parse locale") $ parseLocale "pt-PT" put (brig . path "/self/locale" . contentJson . zUser uid . zConn "c" . locale locPT) !!! const 200 === statusCode - liftIO $ Util.assertUserJournalQueue "user update" aws (userLocaleUpdateJournaled uid locPT) + Util.assertLocaleUpdateJournaled userJournalWatcher uid locPT "user update" -- get the updated locale get (brig . path "/self" . zUser uid) !!! do const 200 === statusCode @@ -1267,24 +1266,19 @@ testEmailPhoneDelete brig cannon = do const 200 === statusCode const Nothing === (userPhone <=< responseJsonMaybe) -testDeleteUserByPassword :: Brig -> Cannon -> AWS.Env -> Http () -testDeleteUserByPassword brig cannon aws = do +testDeleteUserByPassword :: Brig -> Cannon -> UserJournalWatcher -> Http () +testDeleteUserByPassword brig cannon userJournalWatcher = do u <- randomUser brig - liftIO $ Util.assertUserJournalQueue "user activate" aws (userActivateJournaled u) + Util.assertUserActivateJournaled userJournalWatcher u "user activate" let uid1 = userId u let email = fromMaybe (error "Missing email") (userEmail u) - -- Initiate a change of email address, to verify that activation - -- does not work after the account has been deleted. - eml <- randomEmail - let Just oldeml = userEmail u - initiateEmailUpdateLogin brig eml (emailLogin oldeml defPassword Nothing) uid1 !!! (const 202 === statusCode) -- Establish some connections usr2 <- randomUser brig let uid2 = userId usr2 - liftIO $ Util.assertUserJournalQueue "user activate" aws (userActivateJournaled usr2) + Util.assertUserActivateJournaled userJournalWatcher usr2 "user activate" usr3 <- randomUser brig let uid3 = userId usr3 - liftIO $ Util.assertUserJournalQueue "user activate" aws (userActivateJournaled usr3) + Util.assertUserActivateJournaled userJournalWatcher usr3 "user activate" postConnection brig uid1 uid2 !!! const 201 === statusCode postConnection brig uid1 uid3 !!! const 201 === statusCode putConnection brig uid2 uid1 Accepted !!! const 200 === statusCode @@ -1300,7 +1294,12 @@ testDeleteUserByPassword brig cannon aws = do !!! const 200 === statusCode n1 <- countCookies brig uid1 defCookieLabel liftIO $ Just 1 @=? n1 - setHandleAndDeleteUser brig cannon u [uid2, uid3] aws $ + -- Initiate a change of email address, to verify that activation + -- does not work after the account has been deleted. + eml <- randomEmail + let Just oldeml = userEmail u + initiateEmailUpdateLogin brig eml (emailLogin oldeml defPassword Nothing) uid1 !!! (const 202 === statusCode) + setHandleAndDeleteUser brig cannon u [uid2, uid3] userJournalWatcher $ \uid -> deleteUser uid (Just defPassword) brig !!! const 200 === statusCode -- Activating the new email address now should not work act <- getActivationCode brig (Left eml) @@ -1324,15 +1323,15 @@ testDeleteUserByPassword brig cannon aws = do const 200 === statusCode const (Just u3Conns) === responseJsonMaybe -testDeleteUserWithLegalHold :: Brig -> Cannon -> AWS.Env -> Http () -testDeleteUserWithLegalHold brig cannon aws = do +testDeleteUserWithLegalHold :: Brig -> Cannon -> UserJournalWatcher -> Http () +testDeleteUserWithLegalHold brig cannon userJournalWatcher = do user <- randomUser brig let uid = userId user -- Register a legalhold client addClientInternal brig uid (defNewClient LegalHoldClientType [Imports.head somePrekeys] (Imports.head someLastPrekeys)) !!! const 201 === statusCode - liftIO $ Util.assertUserJournalQueue "user activate testDeleteInternal1: " aws (userActivateJournaled user) - setHandleAndDeleteUser brig cannon user [] aws $ + Util.assertUserActivateJournaled userJournalWatcher user "user activate testDeleteInternal1: " + setHandleAndDeleteUser brig cannon user [] userJournalWatcher $ \uid' -> deleteUser uid' (Just defPassword) brig !!! const 200 === statusCode testDeleteUserByCode :: Brig -> Http () @@ -1664,11 +1663,11 @@ testTooManyMembersForLegalhold opts brig = do const 403 === statusCode const (Right "too-many-members-for-legalhold") === fmap Wai.label . responseJsonEither -testDeleteUserWithCompletelyDeletedUser :: Brig -> Cannon -> AWS.Env -> Http () -testDeleteUserWithCompletelyDeletedUser brig cannon aws = do +testDeleteUserWithCompletelyDeletedUser :: Brig -> Cannon -> UserJournalWatcher -> Http () +testDeleteUserWithCompletelyDeletedUser brig cannon userJournalWatcher = do u <- randomUser brig - liftIO $ Util.assertUserJournalQueue "user activate testDeleteUserWithCompletelyDeletedUser" aws (userActivateJournaled u) - setHandleAndDeleteUser brig cannon u [] aws $ + Util.assertUserActivateJournaled userJournalWatcher u "user activate testDeleteUserWithCompletelyDeletedUser" + setHandleAndDeleteUser brig cannon u [] userJournalWatcher $ \uid -> deleteUserInternal uid brig !!! const 202 === statusCode do let uid = userId u @@ -1683,22 +1682,22 @@ testDeleteUserWithNoUser brig = do !!! do const 404 === statusCode -testDeleteUserWithNotDeletedUser :: HasCallStack => Brig -> Cannon -> AWS.Env -> Http () -testDeleteUserWithNotDeletedUser brig cannon aws = do +testDeleteUserWithNotDeletedUser :: HasCallStack => Brig -> Cannon -> UserJournalWatcher -> Http () +testDeleteUserWithNotDeletedUser brig cannon userJournalWatcher = do u <- randomUser brig - liftIO $ Util.assertUserJournalQueue "user activate testDeleteUserWithNotDeletedUser" aws (userActivateJournaled u) + Util.assertUserActivateJournaled userJournalWatcher u "user activate testDeleteUserWithNotDeletedUser" do - setHandleAndDeleteUser brig cannon u [] aws $ + setHandleAndDeleteUser brig cannon u [] userJournalWatcher $ ( \uid' -> deleteUserInternal uid' brig !!! do const 202 === statusCode ) -testDeleteUserWithDanglingProperty :: Brig -> Cannon -> AWS.Env -> Http () -testDeleteUserWithDanglingProperty brig cannon aws = do +testDeleteUserWithDanglingProperty :: Brig -> Cannon -> UserJournalWatcher -> Http () +testDeleteUserWithDanglingProperty brig cannon userJournalWatcher = do u <- randomUser brig - liftIO $ Util.assertUserJournalQueue "user activate testDeleteUserWithDanglingProperty" aws (userActivateJournaled u) + Util.assertUserActivateJournaled userJournalWatcher u "user activate testDeleteUserWithDanglingProperty" let uid = userId u -- First set a unique handle (to verify freeing of the handle) @@ -1708,7 +1707,7 @@ testDeleteUserWithDanglingProperty brig cannon aws = do !!! const 200 === statusCode deleteUserInternal uid brig !!! const 202 === statusCode - liftIO $ Util.assertUserJournalQueue "user deletion testDeleteUserWithDanglingProperty" aws (userDeleteJournaled uid) + Util.assertDeleteJournaled userJournalWatcher uid "user deletion testDeleteUserWithDanglingProperty" setProperty brig (userId u) "foo" objectProp !!! const 200 === statusCode @@ -1716,7 +1715,7 @@ testDeleteUserWithDanglingProperty brig cannon aws = do const 200 === statusCode const (Just objectProp) === responseJsonMaybe - execAndAssertUserDeletion brig cannon u (Handle hdl) [] aws $ \uid' -> do + execAndAssertUserDeletion brig cannon u (Handle hdl) [] userJournalWatcher $ \uid' -> do deleteUserInternal uid' brig !!! do const 202 === statusCode @@ -1732,8 +1731,8 @@ testDeleteUserWithDanglingProperty brig cannon aws = do -- helpers -setHandleAndDeleteUser :: Brig -> Cannon -> User -> [UserId] -> AWS.Env -> (UserId -> HttpT IO ()) -> Http () -setHandleAndDeleteUser brig cannon u others aws execDelete = do +setHandleAndDeleteUser :: Brig -> Cannon -> User -> [UserId] -> UserJournalWatcher -> (UserId -> HttpT IO ()) -> Http () +setHandleAndDeleteUser brig cannon u others userJournalWatcher execDelete = do let uid = userId u -- First set a unique handle (to verify freeing of the handle) hdl <- randomHandle @@ -1741,10 +1740,10 @@ setHandleAndDeleteUser brig cannon u others aws execDelete = do put (brig . path "/self/handle" . contentJson . zUser uid . zConn "c" . body update) !!! const 200 === statusCode - execAndAssertUserDeletion brig cannon u (Handle hdl) others aws execDelete + execAndAssertUserDeletion brig cannon u (Handle hdl) others userJournalWatcher execDelete -execAndAssertUserDeletion :: Brig -> Cannon -> User -> Handle -> [UserId] -> AWS.Env -> (UserId -> HttpT IO ()) -> Http () -execAndAssertUserDeletion brig cannon u hdl others aws execDelete = do +execAndAssertUserDeletion :: Brig -> Cannon -> User -> Handle -> [UserId] -> UserJournalWatcher -> (UserId -> HttpT IO ()) -> Http () +execAndAssertUserDeletion brig cannon u hdl others userJournalWatcher execDelete = do let uid = userId u quid = userQualifiedId u email = fromMaybe (error "Must have an email set") (userEmail u) @@ -1753,7 +1752,7 @@ execAndAssertUserDeletion brig cannon u hdl others aws execDelete = do WS.bracketRN cannon (uid : others) $ \wss -> do execDelete uid void . liftIO . WS.assertMatchN (5 # Second) wss $ matchDeleteUserNotification quid - liftIO $ Util.assertUserJournalQueue "user deletion, setHandleAndDeleteUser: " aws (userDeleteJournaled uid) + Util.assertDeleteJournaled userJournalWatcher uid "user deletion, setHandleAndDeleteUser: " -- Cookies are gone n2 <- countCookies brig uid defCookieLabel liftIO $ Just 0 @=? n2 @@ -1782,7 +1781,7 @@ execAndAssertUserDeletion brig cannon u hdl others aws execDelete = do ] -- This will generate a new event, we need to consume it here usr <- postUserInternal o brig - liftIO $ Util.assertUserJournalQueue "user activate execAndAssertUserDeletion" aws (userActivateJournaled usr) + Util.assertUserActivateJournaled userJournalWatcher usr "user activate execAndAssertUserDeletion" -- Handle is available again Bilge.head (brig . paths ["users", "handles", toByteString' hdl] . zUser uid) !!! const 404 === statusCode diff --git a/services/brig/test/integration/API/User/Auth.hs b/services/brig/test/integration/API/User/Auth.hs index 3005674924..21f4189ba1 100644 --- a/services/brig/test/integration/API/User/Auth.hs +++ b/services/brig/test/integration/API/User/Auth.hs @@ -1,4 +1,5 @@ {-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE NumericUnderscores #-} {-# OPTIONS_GHC -Wno-incomplete-patterns #-} {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} @@ -80,7 +81,7 @@ import Wire.API.User.Client -- with this are failing then assumption that -- 'whitelist-teams-and-implicit-consent' is set in all test environments is no -- longer correct. -onlyIfLhWhitelisted :: (MonadIO m, Monad m) => m () -> m () +onlyIfLhWhitelisted :: MonadIO m => m () -> m () onlyIfLhWhitelisted action = do let isGalleyLegalholdFeatureWhitelist = True if isGalleyLegalholdFeatureWhitelist @@ -134,7 +135,7 @@ tests conf m z db b g n = [ test m "test-login-verify6-digit-email-code-success" $ testLoginVerify6DigitEmailCodeSuccess b g db, test m "test-login-verify6-digit-wrong-code-fails" $ testLoginVerify6DigitWrongCodeFails b g, test m "test-login-verify6-digit-missing-code-fails" $ testLoginVerify6DigitMissingCodeFails b g, - test m "test-login-verify6-digit-expired-code-fails" $ testLoginVerify6DigitExpiredCodeFails b g db, + test m "test-login-verify6-digit-expired-code-fails" $ testLoginVerify6DigitExpiredCodeFails conf b g db, test m "test-login-verify6-digit-resend-code-success-and-rate-limiting" $ testLoginVerify6DigitResendCodeSuccessAndRateLimiting b g conf db, test m "test-login-verify6-digit-limit-retries" $ testLoginVerify6DigitLimitRetries b g conf db ] @@ -151,7 +152,11 @@ tests conf m z db b g n = test m "token mismatch" (onlyIfLhWhitelisted (testTokenMismatchLegalhold z b g)), test m "new-persistent-cookie" (testNewPersistentCookie conf b), test m "new-session-cookie" (testNewSessionCookie conf b), - test m "suspend-inactive" (testSuspendInactiveUsers conf b), + testGroup "suspend-inactive" $ do + cookieType <- [SessionCookie, PersistentCookie] + endPoint <- ["/access", "/login"] + let testName = "cookieType=" <> show cookieType <> ",endPoint=" <> show endPoint + pure $ test m testName $ testSuspendInactiveUsers conf b cookieType endPoint, test m "client access" (testAccessWithClientId b), test m "client access with old token" (testAccessWithClientIdAndOldToken b), test m "client access incorrect" (testAccessWithIncorrectClientId b), @@ -529,8 +534,8 @@ testLoginVerify6DigitMissingCodeFails brig galley = do -- @SF.Channel @TSFI.RESTfulAPI @S2 -- -- Test that login fails with expired second factor email verification code -testLoginVerify6DigitExpiredCodeFails :: Brig -> Galley -> DB.ClientState -> Http () -testLoginVerify6DigitExpiredCodeFails brig galley db = do +testLoginVerify6DigitExpiredCodeFails :: Opts.Opts -> Brig -> Galley -> DB.ClientState -> Http () +testLoginVerify6DigitExpiredCodeFails opts brig galley db = do (u, tid) <- createUserWithTeam' brig let Just email = userEmail u Util.setTeamFeatureLockStatus @Public.SndFactorPasswordChallengeConfig galley tid Public.LockStatusUnlocked @@ -538,8 +543,8 @@ testLoginVerify6DigitExpiredCodeFails brig galley db = do Util.generateVerificationCode brig (Public.SendVerificationCode Public.Login email) key <- Code.mkKey (Code.ForEmail email) Just vcode <- Util.lookupCode db key Code.AccountLogin - -- wait > 5 sec for the code to expire (assumption: setVerificationTimeout in brig.integration.yaml is set to <= 5 sec) - threadDelay $ (5 * 1000000) + 600000 + let verificationTimeout = round (Opts.setVerificationTimeout (Opts.optSettings opts)) + threadDelay $ ((verificationTimeout + 1) * 1000_000) checkLoginFails brig $ PasswordLogin $ PasswordLoginData @@ -940,7 +945,7 @@ getAndTestDBSupersededCookieAndItsValidSuccessor :: Opts.Opts -> Brig -> Nginz - getAndTestDBSupersededCookieAndItsValidSuccessor config b n = do u <- randomUser b let renewAge = Opts.setUserCookieRenewAge $ Opts.optSettings config - let minAge = fromIntegral $ renewAge * 1000000 + 1 + let minAge = fromIntegral $ (renewAge + 1) * 1000000 Just email = userEmail u _rs <- login n (emailLogin email defPassword (Just "nexus1")) PersistentCookie @@ -1199,51 +1204,46 @@ testNewSessionCookie config b = do const 200 === statusCode const Nothing === getHeader "Set-Cookie" -testSuspendInactiveUsers :: HasCallStack => Opts.Opts -> Brig -> Http () -testSuspendInactiveUsers config brig = do - -- (context information: cookies are stored by user, not be device; so if there if the - -- cookie is old it means none of the devices of a user has used it for a request.) +testSuspendInactiveUsers :: HasCallStack => Opts.Opts -> Brig -> CookieType -> String -> Http () +testSuspendInactiveUsers config brig cookieType endPoint = do + -- (context information: cookies are stored by user, not by device; so if there is a + -- cookie that is old, it means none of the devices of the user has used it for a request.) let Just suspendAge = Opts.suspendTimeout <$> Opts.setSuspendInactiveUsers (Opts.optSettings config) unless (suspendAge <= 30) $ error "`suspendCookiesOlderThanSecs` is the number of seconds this test is running. Please pick a value < 30." - let check :: HasCallStack => CookieType -> String -> Http () - check cookieType endPoint = do - user <- randomUser brig - let Just email = userEmail user - rs <- - login brig (emailLogin email defPassword Nothing) cookieType - do - post (unversioned . brig . path "/access" . cookie cky) !!! do - const 403 === statusCode - const Nothing === getHeader "Set-Cookie" - "/login" -> do - login brig (emailLogin email defPassword Nothing) cookieType !!! do - const 403 === statusCode - const Nothing === getHeader "Set-Cookie" - let assertStatus want = do - have <- - retrying - (exponentialBackoff 200000 <> limitRetries 6) - (\_ have -> pure $ have == Suspended) - (\_ -> getStatus brig (userId user)) - let errmsg = "testSuspendInactiveUsers: " <> show (want, cookieType, endPoint, waitTime, suspendAge) - liftIO $ HUnit.assertEqual errmsg want have - assertStatus Suspended - setStatus brig (userId user) Active - assertStatus Active - login brig (emailLogin email defPassword Nothing) cookieType - !!! const 200 === statusCode - check SessionCookie "/access" - check SessionCookie "/login" - check PersistentCookie "/access" - check PersistentCookie "/login" + + user <- randomUser brig + let Just email = userEmail user + rs <- + login brig (emailLogin email defPassword Nothing) cookieType + do + post (unversioned . brig . path "/access" . cookie cky) !!! do + const 403 === statusCode + const Nothing === getHeader "Set-Cookie" + "/login" -> do + login brig (emailLogin email defPassword Nothing) cookieType !!! do + const 403 === statusCode + const Nothing === getHeader "Set-Cookie" + let assertStatus want = do + have <- + retrying + (exponentialBackoff 200000 <> limitRetries 6) + (\_ have -> pure $ have == Suspended) + (\_ -> getStatus brig (userId user)) + let errmsg = "testSuspendInactiveUsers: " <> show (want, cookieType, endPoint, waitTime, suspendAge) + liftIO $ HUnit.assertEqual errmsg want have + assertStatus Suspended + setStatus brig (userId user) Active + assertStatus Active + login brig (emailLogin email defPassword Nothing) cookieType + !!! const 200 === statusCode ------------------------------------------------------------------------------- -- Cookie Management diff --git a/services/brig/test/integration/API/User/Client.hs b/services/brig/test/integration/API/User/Client.hs index 77ccdb1e2f..87297e6bd8 100644 --- a/services/brig/test/integration/API/User/Client.hs +++ b/services/brig/test/integration/API/User/Client.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE NumericUnderscores #-} {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} {-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} @@ -97,7 +98,7 @@ tests _cl _at opts p db b c g = [ test p "success" $ testAddGetClientVerificationCode db b g, test p "missing code" $ testAddGetClientMissingCode b g, test p "wrong code" $ testAddGetClientWrongCode b g, - test p "expired code" $ testAddGetClientCodeExpired db b g + test p "expired code" $ testAddGetClientCodeExpired db opts b g ], test p "post /clients - 201 (with mls keys)" $ testAddGetClient def {addWithMLSKeys = True} b c, test p "post /clients - 403" $ testClientReauthentication b, @@ -120,7 +121,7 @@ tests _cl _at opts p db b c g = test p "get/head nonce/clients" $ testNewNonce b, testGroup "post /clients/:cid/access-token" - [ test p "success" $ testCreateAccessToken b, + [ test p "invalid values" $ testCreateAccessTokenInvalidValues b, test p "proof missing" $ testCreateAccessTokenMissingProof b, test p "no nonce" $ testCreateAccessTokenNoNonce b ] @@ -190,8 +191,8 @@ testAddGetClientWrongCode brig galley = do -- @SF.Channel @TSFI.RESTfulAPI @S2 -- -- Test that device cannot be added with expired second factor email verification code when this feature is enabled -testAddGetClientCodeExpired :: DB.ClientState -> Brig -> Galley -> Http () -testAddGetClientCodeExpired db brig galley = do +testAddGetClientCodeExpired :: DB.ClientState -> Opt.Opts -> Brig -> Galley -> Http () +testAddGetClientCodeExpired db opts brig galley = do (u, tid) <- Util.createUserWithTeam' brig let uid = userId u let Just email = userEmail u @@ -206,8 +207,8 @@ testAddGetClientCodeExpired db brig galley = do checkLoginSucceeds $ PasswordLogin $ PasswordLoginData (LoginByEmail email) defPassword (Just defCookieLabel) codeValue - -- wait > 5 sec for the code to expire (assumption: setVerificationTimeout in brig.integration.yaml is set to <= 5 sec) - threadDelay $ (5 * 1000000) + 600000 + let verificationTimeout = round (Opt.setVerificationTimeout (Opt.optSettings opts)) + threadDelay $ ((verificationTimeout + 1) * 1000_000) addClient' codeValue !!! do const 403 === statusCode const (Just "code-authentication-failed") === fmap Error.label . responseJsonMaybe @@ -1154,18 +1155,16 @@ testNewNonce brig = do Just "no-store" @=? getHeader "Cache-Control" response pure nonceBs -testCreateAccessToken :: Brig -> Http () -testCreateAccessToken brig = do - uid <- userId <$> randomUser brig - cid <- createClientForUser brig uid - n <- Util.headNonce brig uid cid Http () +testCreateAccessTokenInvalidValues brig = + do + uid <- userId <$> randomUser brig + cid <- createClientForUser brig uid + n <- Util.headNonce brig uid cid Http () testCreateAccessTokenMissingProof brig = do diff --git a/services/brig/test/integration/API/User/Connection.hs b/services/brig/test/integration/API/User/Connection.hs index f25a2f594d..1e1353d1df 100644 --- a/services/brig/test/integration/API/User/Connection.hs +++ b/services/brig/test/integration/API/User/Connection.hs @@ -991,7 +991,7 @@ testInternalGetConnStatusesAll brig opts fedBrigClient = do let ordFn x = (csv2From x, csv2To x) sortOn ordFn acceptedRemoteDomain1Only @?= sortOn ordFn (map (\u -> ConnectionStatusV2 u remoteDomain1User1 Accepted) uids) -getConnStatusInternal :: (MonadIO m, MonadHttp m) => (Request -> Request) -> ConnectionsStatusRequestV2 -> m (Response (Maybe LByteString)) +getConnStatusInternal :: MonadHttp m => (Request -> Request) -> ConnectionsStatusRequestV2 -> m (Response (Maybe LByteString)) getConnStatusInternal brig req = post $ brig diff --git a/services/brig/test/integration/API/User/Handles.hs b/services/brig/test/integration/API/User/Handles.hs index bade81ab04..9d14562812 100644 --- a/services/brig/test/integration/API/User/Handles.hs +++ b/services/brig/test/integration/API/User/Handles.hs @@ -329,7 +329,7 @@ testGetUserByQualifiedHandleNoFederation opt brig = do const "Bad Request" === statusMessage const (Right "federation-not-enabled") === fmap Wai.label . responseJsonEither -assertCanFind :: (Monad m, MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> User -> User -> m () +assertCanFind :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> User -> User -> m () assertCanFind brig from target = do liftIO $ assertBool "assertCanFind: Target must have a handle set" (isJust $ userHandle target) let targetHandle = fromMaybe (error "Impossible") (userHandle target) @@ -341,7 +341,7 @@ assertCanFind brig from target = do const 200 === statusCode const (Just (UserHandleInfo $ userQualifiedId target)) === responseJsonMaybe -assertCannotFind :: (Monad m, MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> User -> User -> m () +assertCannotFind :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> User -> User -> m () assertCannotFind brig from target = do liftIO $ assertBool "assertCannotFind: Target must have a handle set" (isJust $ userHandle target) let targetHandle = fromMaybe (error "Impossible") (userHandle target) diff --git a/services/brig/test/integration/API/User/Util.hs b/services/brig/test/integration/API/User/Util.hs index 1428b66ec0..58fcc8786e 100644 --- a/services/brig/test/integration/API/User/Util.hs +++ b/services/brig/test/integration/API/User/Util.hs @@ -1,4 +1,6 @@ {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- @@ -40,6 +42,7 @@ import qualified Data.ByteString.Lazy as LB import Data.Domain import Data.Handle (Handle (Handle)) import Data.Id hiding (client) +import Data.Kind import qualified Data.List1 as List1 import Data.Misc (PlainTextPassword (..)) import Data.Qualified @@ -78,7 +81,7 @@ import Wire.API.User.Password newtype ConnectionLimit = ConnectionLimit Int64 checkHandles :: - (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => + (MonadHttp m, HasCallStack) => Brig -> UserId -> [Text] -> @@ -151,7 +154,7 @@ createRandomPhoneUser brig = do const (Just phn) === (userPhone <=< responseJsonMaybe) pure (uid, phn) -initiatePasswordReset :: Brig -> Email -> (MonadIO m, MonadHttp m) => m ResponseLBS +initiatePasswordReset :: Brig -> Email -> MonadHttp m => m ResponseLBS initiatePasswordReset brig email = post ( brig @@ -185,7 +188,7 @@ initiateEmailUpdateLogin brig email loginCreds uid = do pure (decodeCookie rsp, decodeToken rsp) initiateEmailUpdateCreds brig email (cky, tok) uid -initiateEmailUpdateCreds :: Brig -> Email -> (Bilge.Cookie, Brig.ZAuth.Token ZAuth.Access) -> UserId -> (MonadIO m, MonadCatch m, MonadHttp m) => m ResponseLBS +initiateEmailUpdateCreds :: Brig -> Email -> (Bilge.Cookie, Brig.ZAuth.Token ZAuth.Access) -> UserId -> MonadHttp m => m ResponseLBS initiateEmailUpdateCreds brig email (cky, tok) uid = do put $ unversioned @@ -196,7 +199,7 @@ initiateEmailUpdateCreds brig email (cky, tok) uid = do . zUser uid . Bilge.json (EmailUpdate email) -initiateEmailUpdateNoSend :: Brig -> Email -> UserId -> (MonadIO m, MonadHttp m) => m ResponseLBS +initiateEmailUpdateNoSend :: Brig -> Email -> UserId -> MonadHttp m => m ResponseLBS initiateEmailUpdateNoSend brig email uid = let emailUpdate = RequestBodyLBS . encode $ EmailUpdate email in put (brig . path "/i/self/email" . contentJson . zUser uid . body emailUpdate) @@ -220,7 +223,7 @@ preparePasswordReset brig cs email uid newpw = do where runSem = liftIO . runFinal @IO . interpretClientToIO cs . codeStoreToCassandra @DB.Client -completePasswordReset :: Brig -> CompletePasswordReset -> (MonadIO m, MonadHttp m) => m ResponseLBS +completePasswordReset :: Brig -> CompletePasswordReset -> MonadHttp m => m ResponseLBS completePasswordReset brig passwordResetData = post ( brig @@ -233,7 +236,7 @@ removeBlacklist :: Brig -> Email -> (MonadIO m, MonadHttp m) => m () removeBlacklist brig email = void $ delete (brig . path "/i/users/blacklist" . queryItem "email" (toByteString' email)) -getClient :: Brig -> UserId -> ClientId -> (MonadIO m, MonadHttp m) => m ResponseLBS +getClient :: Brig -> UserId -> ClientId -> MonadHttp m => m ResponseLBS getClient brig u c = get $ brig @@ -241,7 +244,7 @@ getClient brig u c = . zUser u putClient :: - (MonadIO m, MonadHttp m, HasCallStack) => + (MonadHttp m, HasCallStack) => Brig -> UserId -> ClientId -> @@ -254,14 +257,14 @@ putClient brig uid c keys = . zUser uid . json (UpdateClient [] Nothing Nothing Nothing keys) -getClientCapabilities :: Brig -> UserId -> ClientId -> (MonadIO m, MonadHttp m) => m ResponseLBS +getClientCapabilities :: Brig -> UserId -> ClientId -> MonadHttp m => m ResponseLBS getClientCapabilities brig u c = get $ brig . paths ["clients", toByteString' c, "capabilities"] . zUser u -getUserClientsUnqualified :: Brig -> UserId -> (MonadIO m, MonadHttp m) => m ResponseLBS +getUserClientsUnqualified :: Brig -> UserId -> MonadHttp m => m ResponseLBS getUserClientsUnqualified brig uid = get $ apiVersion "v1" @@ -269,14 +272,14 @@ getUserClientsUnqualified brig uid = . paths ["users", toByteString' uid, "clients"] . zUser uid -getUserClientsQualified :: Brig -> UserId -> Domain -> UserId -> (MonadIO m, MonadHttp m) => m ResponseLBS +getUserClientsQualified :: Brig -> UserId -> Domain -> UserId -> MonadHttp m => m ResponseLBS getUserClientsQualified brig zusr domain uid = get $ brig . paths ["users", toByteString' domain, toByteString' uid, "clients"] . zUser zusr -deleteClient :: Brig -> UserId -> ClientId -> Maybe Text -> (MonadIO m, MonadHttp m) => m ResponseLBS +deleteClient :: Brig -> UserId -> ClientId -> Maybe Text -> MonadHttp m => m ResponseLBS deleteClient brig u c pw = delete $ brig @@ -290,7 +293,7 @@ deleteClient brig u c pw = RequestBodyLBS . encode . object . maybeToList $ fmap ("password" .=) pw -listConnections :: HasCallStack => Brig -> UserId -> (MonadIO m, MonadHttp m) => m ResponseLBS +listConnections :: HasCallStack => Brig -> UserId -> MonadHttp m => m ResponseLBS listConnections brig u = get $ apiVersion "v1" @@ -298,7 +301,7 @@ listConnections brig u = . path "connections" . zUser u -listAllConnections :: (MonadIO m, MonadHttp m, HasCallStack) => Brig -> UserId -> Maybe Int -> Maybe (MultiTablePagingState "Connections" LocalOrRemoteTable) -> m ResponseLBS +listAllConnections :: (MonadHttp m, HasCallStack) => Brig -> UserId -> Maybe Int -> Maybe (MultiTablePagingState "Connections" LocalOrRemoteTable) -> m ResponseLBS listAllConnections brig u size state = post $ brig @@ -315,14 +318,14 @@ listAllConnections brig u size state = ] ) -getConnectionQualified :: (MonadIO m, MonadHttp m) => Brig -> UserId -> Qualified UserId -> m ResponseLBS +getConnectionQualified :: MonadHttp m => Brig -> UserId -> Qualified UserId -> m ResponseLBS getConnectionQualified brig from (Qualified toUser toDomain) = get $ brig . paths ["connections", toByteString' toDomain, toByteString' toUser] . zUser from -setProperty :: Brig -> UserId -> ByteString -> Value -> (MonadIO m, MonadHttp m) => m ResponseLBS +setProperty :: Brig -> UserId -> ByteString -> Value -> MonadHttp m => m ResponseLBS setProperty brig u k v = put $ brig @@ -332,14 +335,14 @@ setProperty brig u k v = . contentJson . body (RequestBodyLBS $ encode v) -getProperty :: Brig -> UserId -> ByteString -> (MonadIO m, MonadHttp m) => m ResponseLBS +getProperty :: Brig -> UserId -> ByteString -> MonadHttp m => m ResponseLBS getProperty brig u k = get $ brig . paths ["/properties", k] . zUser u -deleteProperty :: Brig -> UserId -> ByteString -> (MonadIO m, MonadHttp m) => m ResponseLBS +deleteProperty :: Brig -> UserId -> ByteString -> MonadHttp m => m ResponseLBS deleteProperty brig u k = delete $ brig @@ -467,7 +470,7 @@ uploadAsset c usr sts dat = do === statusCode downloadAsset :: - (MonadIO m, MonadHttp m) => + MonadHttp m => CargoHold -> UserId -> Qualified AssetKey -> @@ -480,7 +483,7 @@ downloadAsset c usr ast = . zConn "conn" ) -requestLegalHoldDevice :: Brig -> UserId -> UserId -> LastPrekey -> (MonadIO m, MonadHttp m) => m ResponseLBS +requestLegalHoldDevice :: Brig -> UserId -> UserId -> LastPrekey -> MonadHttp m => m ResponseLBS requestLegalHoldDevice brig requesterId targetUserId lastPrekey' = post $ brig @@ -492,7 +495,7 @@ requestLegalHoldDevice brig requesterId targetUserId lastPrekey' = RequestBodyLBS . encode $ LegalHoldClientRequest requesterId lastPrekey' -deleteLegalHoldDevice :: Brig -> UserId -> (MonadIO m, MonadHttp m) => m ResponseLBS +deleteLegalHoldDevice :: Brig -> UserId -> MonadHttp m => m ResponseLBS deleteLegalHoldDevice brig uid = delete $ brig @@ -528,7 +531,7 @@ generateVerificationCodeExpect :: (MonadCatch m, MonadIO m, MonadHttp m, HasCall generateVerificationCodeExpect expectedStatus brig req = do generateVerificationCode' brig req !!! const expectedStatus === statusCode -generateVerificationCode' :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> Public.SendVerificationCode -> m ResponseLBS +generateVerificationCode' :: (MonadHttp m, HasCallStack) => Brig -> Public.SendVerificationCode -> m ResponseLBS generateVerificationCode' brig req = do let js = RequestBodyLBS $ encode req post (brig . paths ["verification-code", "send"] . contentJson . body js) @@ -539,12 +542,11 @@ setTeamSndFactorPasswordChallenge galley tid status = do put (galley . paths ["i", "teams", toByteString' tid, "features", featureNameBS @Public.SndFactorPasswordChallengeConfig] . contentJson . body js) !!! const 200 === statusCode setTeamFeatureLockStatus :: - forall (cfg :: *) m. + forall (cfg :: Type) m. ( MonadCatch m, MonadIO m, MonadHttp m, HasCallStack, - Public.IsFeatureConfig cfg, KnownSymbol (Public.FeatureSymbol cfg) ) => Galley -> @@ -558,7 +560,7 @@ lookupCode :: MonadIO m => DB.ClientState -> Code.Key -> Code.Scope -> m (Maybe lookupCode db k = liftIO . DB.runClient db . Code.lookup k getNonce :: - (MonadIO m, MonadHttp m) => + MonadHttp m => Brig -> UserId -> ClientId -> @@ -566,7 +568,7 @@ getNonce :: getNonce = nonce get headNonce :: - (MonadIO m, MonadHttp m) => + MonadHttp m => Brig -> UserId -> ClientId -> @@ -581,7 +583,7 @@ nonce m brig uid cid = . zUser uid ) -createAccessToken :: (MonadIO m, MonadHttp m, HasCallStack) => Brig -> UserId -> ClientId -> Maybe Proof -> m ResponseLBS +createAccessToken :: (MonadHttp m, HasCallStack) => Brig -> UserId -> ClientId -> Maybe Proof -> m ResponseLBS createAccessToken brig uid cid mProof = post ( brig diff --git a/services/brig/test/integration/API/UserPendingActivation.hs b/services/brig/test/integration/API/UserPendingActivation.hs index c9c6c4f01f..b4a9a82a4b 100644 --- a/services/brig/test/integration/API/UserPendingActivation.hs +++ b/services/brig/test/integration/API/UserPendingActivation.hs @@ -124,7 +124,7 @@ assertUserExist msg db' uid shouldExist = liftIO $ do exists <- aFewTimes 12 (runClient db' (userExists uid)) (== shouldExist) assertEqual msg shouldExist exists -waitUserExpiration :: (MonadIO m, MonadUnliftIO m) => Opts -> m () +waitUserExpiration :: MonadUnliftIO m => Opts -> m () waitUserExpiration opts' = do let timeoutSecs = round @Double . realToFrac . setTeamInvitationTimeout . optSettings $ opts' Control.Exception.assert (timeoutSecs < 30) $ do @@ -189,7 +189,7 @@ randomScimUserWithSubject = do -- | See 'randomScimUser', 'randomScimUserWithSubject'. randomScimUserWithSubjectAndRichInfo :: - (HasCallStack, MonadRandom m, MonadIO m) => + (HasCallStack, MonadRandom m) => RichInfo -> m (Scim.User.User SparTag, SAML.UnqualifiedNameID) randomScimUserWithSubjectAndRichInfo richInfo = do diff --git a/services/brig/test/integration/API/Version.hs b/services/brig/test/integration/API/Version.hs index bd4945e879..07e52638ad 100644 --- a/services/brig/test/integration/API/Version.hs +++ b/services/brig/test/integration/API/Version.hs @@ -151,7 +151,7 @@ disabledVersionIsNotAdvertised opts brig version = liftIO $ filter (== version) (vinfoDevelopment vinfo) @?= [] getVersionInfo :: - (MonadIO m, MonadCatch m, MonadFail m, MonadHttp m, HasCallStack) => + (MonadIO m, MonadCatch m, MonadHttp m, HasCallStack) => Brig -> m VersionInfo getVersionInfo brig = diff --git a/services/brig/test/integration/Federation/End2end.hs b/services/brig/test/integration/Federation/End2end.hs index a13db52313..e52238aeca 100644 --- a/services/brig/test/integration/Federation/End2end.hs +++ b/services/brig/test/integration/Federation/End2end.hs @@ -58,6 +58,7 @@ import Wire.API.Asset import Wire.API.Conversation import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role +import Wire.API.Conversation.Typing import Wire.API.Event.Conversation import Wire.API.Internal.Notification (ntfTransient) import Wire.API.MLS.Credential @@ -118,7 +119,9 @@ spec _brigOpts mg brig galley cargohold cannon _federator brigTwo galleyTwo carg test mg "download remote asset" $ testRemoteAsset brig brigTwo cargohold cargoholdTwo, test mg "claim remote key packages" $ claimRemoteKeyPackages brig brigTwo, test mg "send an MLS message to a remote user" $ - testSendMLSMessage brig brigTwo galley galleyTwo cannon cannonTwo + testSendMLSMessage brig brigTwo galley galleyTwo cannon cannonTwo, + test mg "remote typing indicator" $ + testRemoteTypingIndicator brig brigTwo galley galleyTwo cannon cannonTwo ] -- | Path covered by this test: @@ -274,7 +277,6 @@ testAddRemoteUsersToLocalConv brig1 galley1 brig2 galley2 = do Nothing roleNameWireAdmin ProtocolProteusTag - Nothing convId <- fmap cnvQualifiedId . responseJsonError =<< post @@ -875,9 +877,9 @@ testSendMLSMessage brig1 brig2 galley1 galley2 cannon1 cannon2 = do !!! const 201 === statusCode post - ( galley2 - . paths - ["mls", "welcome"] + ( unversioned + . galley2 + . paths ["v2", "mls", "welcome"] . zUser (userId bob) . zConn "conn" . header "Z-Type" "access" @@ -946,3 +948,51 @@ testSendMLSMessage brig1 brig2 galley1 galley2 cannon1 cannon2 = do evtType e @?= MLSMessageAdd evtFrom e @?= userQualifiedId alice evtData e @?= EdMLSMessage reply + +testRemoteTypingIndicator :: Brig -> Brig -> Galley -> Galley -> Cannon -> Cannon -> Http () +testRemoteTypingIndicator brig1 brig2 galley1 galley2 cannon1 cannon2 = do + alice <- randomUser brig1 + bob <- randomUser brig2 + + connectUsersEnd2End brig1 brig2 (userQualifiedId alice) (userQualifiedId bob) + + cnv <- + responseJsonError + =<< createConversation galley1 (userId alice) [userQualifiedId bob] + do + let e = List1.head (WS.unpackPayload n) + ntfTransient n @?= True + evtConv e @?= cnvQualifiedId cnv + evtType e @?= Typing + evtFrom e @?= userQualifiedId u + evtData e @?= EdTyping s + + -- -- alice is typing, bob gets events + WS.bracketR cannon2 (userId bob) $ \wsBob -> do + isTyping galley1 alice StartedTyping + checkEvent wsBob alice StartedTyping + isTyping galley1 alice StoppedTyping + checkEvent wsBob alice StoppedTyping + + -- bob is typing, alice gets events + WS.bracketR cannon1 (userId alice) $ \wsAlice -> do + isTyping galley2 bob StartedTyping + checkEvent wsAlice bob StartedTyping + isTyping galley2 bob StoppedTyping + checkEvent wsAlice bob StoppedTyping diff --git a/services/brig/test/integration/Main.hs b/services/brig/test/integration/Main.hs index f278b8f499..90b64986f4 100644 --- a/services/brig/test/integration/Main.hs +++ b/services/brig/test/integration/Main.hs @@ -64,6 +64,7 @@ import Test.Tasty.HUnit import Util import Util.Options import Util.Test +import qualified Util.Test.SQS as SQS import Web.HttpApiData import Wire.API.Federation.API import Wire.API.Routes.Version @@ -138,12 +139,13 @@ runTests iConf brigOpts otherArgs = do let fedGalleyClient = FedClient @'Galley mg (galley iConf) emailAWSOpts <- parseEmailAWSOpts awsEnv <- AWS.mkEnv lg awsOpts emailAWSOpts mg - userApi <- User.tests brigOpts fedBrigClient fedGalleyClient mg b c ch g n awsEnv db + mUserJournalWatcher <- for (view AWS.userJournalQueue awsEnv) $ SQS.watchSQSQueue (view AWS.amazonkaEnv awsEnv) + userApi <- User.tests brigOpts fedBrigClient fedGalleyClient mg b c ch g n awsEnv db mUserJournalWatcher providerApi <- Provider.tests localDomain (provider iConf) mg db b c g searchApis <- Search.tests brigOpts mg g b - teamApis <- Team.tests brigOpts mg n b c g awsEnv + teamApis <- Team.tests brigOpts mg n b c g mUserJournalWatcher turnApi <- Calling.tests mg b brigOpts turnFile turnFileV2 - metricsApi <- Metrics.tests mg b + metricsApi <- Metrics.tests mg brigOpts b systemSettingsApi <- SystemSettings.tests brigOpts mg settingsApi <- Settings.tests brigOpts mg b g createIndex <- Index.Create.spec brigOpts diff --git a/services/brig/test/integration/SMTP.hs b/services/brig/test/integration/SMTP.hs index 02a5108f1e..3b3e052d94 100644 --- a/services/brig/test/integration/SMTP.hs +++ b/services/brig/test/integration/SMTP.hs @@ -1,4 +1,6 @@ {-# OPTIONS_GHC -Wno-deferred-out-of-scope-variables #-} +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} module SMTP where diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index 9e2b99dc8d..07b82f458c 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -1,5 +1,7 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE NumericUnderscores #-} +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2020 Wire Swiss GmbH @@ -23,7 +25,6 @@ module Util where import Bilge import Bilge.Assert -import qualified Brig.AWS as AWS import Brig.AWS.Types import Brig.App (applog, fsWatcher, sftEnv, turnEnv) import Brig.Calling as Calling @@ -102,7 +103,6 @@ import qualified Test.Tasty.Cannon as WS import Test.Tasty.HUnit import Text.Printf (printf) import qualified UnliftIO.Async as Async -import Util.AWS import Util.Options import Wire.API.Connection import Wire.API.Conversation @@ -228,9 +228,6 @@ instance ToJSON SESNotification where test :: Manager -> TestName -> Http a -> TestTree test m n h = testCase n (void $ runHttpT m h) -test' :: AWS.Env -> Manager -> TestName -> Http a -> TestTree -test' e m n h = testCase n $ void $ runHttpT m (liftIO (purgeJournalQueue e) >> h) - twoRandomUsers :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> m (Qualified UserId, UserId, Qualified UserId, UserId) twoRandomUsers brig = do quid1 <- userQualifiedId <$> randomUser brig @@ -334,7 +331,7 @@ requestActivationCode brig expectedStatus ep = bdy (Right p) = object ["phone" .= fromPhone p] getActivationCode :: - (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => + (MonadCatch m, MonadHttp m, HasCallStack) => Brig -> Either Email Phone -> m (Maybe (ActivationKey, ActivationCode)) @@ -352,7 +349,7 @@ getPhoneLoginCode brig p = do let lbs = fromMaybe "" $ responseBody r pure (LoginCode <$> (lbs ^? key "code" . _String)) -assertUpdateNotification :: WS.WebSocket -> UserId -> UserUpdate -> IO () +assertUpdateNotification :: HasCallStack => WS.WebSocket -> UserId -> UserUpdate -> IO () assertUpdateNotification ws uid upd = WS.assertMatch (5 # Second) ws $ \n -> do let j = Object $ List1.head (ntfPayload n) j ^? key "type" . _String @?= Just "user.update" @@ -441,11 +438,11 @@ postUserRegister payload brig = do rs <- postUserRegister' payload brig Object -> Brig -> m ResponseLBS +postUserRegister' :: MonadHttp m => Object -> Brig -> m ResponseLBS postUserRegister' payload brig = do post (brig . path "/register" . contentJson . body (RequestBodyLBS $ encode payload)) -deleteUser :: (Functor m, MonadIO m, MonadCatch m, MonadHttp m, HasCallStack) => UserId -> Maybe PlainTextPassword -> Brig -> m ResponseLBS +deleteUser :: (MonadHttp m, HasCallStack) => UserId -> Maybe PlainTextPassword -> Brig -> m ResponseLBS deleteUser u p brig = delete $ brig @@ -460,7 +457,7 @@ deleteUserInternal u brig = brig . paths ["/i/users", toByteString' u] -activate :: Brig -> ActivationPair -> (MonadIO m, MonadHttp m) => m ResponseLBS +activate :: Brig -> ActivationPair -> MonadHttp m => m ResponseLBS activate brig (k, c) = get $ brig @@ -483,7 +480,7 @@ getUser brig zusr usr = -- | NB: you can also use nginz as the first argument here. The type aliases are compatible, -- and so are the end-points. This is important in tests where the cookie must come from the -- nginz domain, so it can be passed back to it. -login :: Brig -> Login -> CookieType -> (MonadIO m, MonadHttp m) => m ResponseLBS +login :: Brig -> Login -> CookieType -> MonadHttp m => m ResponseLBS login b l t = let js = RequestBodyLBS (encode l) in post $ @@ -545,7 +542,7 @@ sendLoginCode b p typ force = "force" .= force ] -postConnection :: Brig -> UserId -> UserId -> (MonadIO m, MonadHttp m) => m ResponseLBS +postConnection :: Brig -> UserId -> UserId -> MonadHttp m => m ResponseLBS postConnection brig from to = post $ apiVersion "v1" @@ -560,7 +557,7 @@ postConnection brig from to = RequestBodyLBS . encode $ ConnectionRequest to (unsafeRange "some conv name") -postConnectionQualified :: (MonadIO m, MonadHttp m) => Brig -> UserId -> Qualified UserId -> m ResponseLBS +postConnectionQualified :: MonadHttp m => Brig -> UserId -> Qualified UserId -> m ResponseLBS postConnectionQualified brig from (Qualified toUser toDomain) = post $ brig @@ -569,7 +566,7 @@ postConnectionQualified brig from (Qualified toUser toDomain) = . zUser from . zConn "conn" -putConnection :: Brig -> UserId -> UserId -> Relation -> (MonadIO m, MonadHttp m) => m ResponseLBS +putConnection :: Brig -> UserId -> UserId -> Relation -> MonadHttp m => m ResponseLBS putConnection brig from to r = put $ apiVersion "v1" @@ -582,7 +579,7 @@ putConnection brig from to r = where payload = RequestBodyLBS . encode $ object ["status" .= r] -putConnectionQualified :: Brig -> UserId -> Qualified UserId -> Relation -> (MonadIO m, MonadHttp m) => m ResponseLBS +putConnectionQualified :: Brig -> UserId -> Qualified UserId -> Relation -> MonadHttp m => m ResponseLBS putConnectionQualified brig from (Qualified to toDomain) r = put $ brig @@ -602,7 +599,7 @@ connectUsers b u = mapM_ connectTo void $ putConnection b v u Accepted putHandle :: - (MonadIO m, MonadHttp m, HasCallStack) => + (MonadHttp m, HasCallStack) => Brig -> UserId -> Text -> @@ -633,7 +630,7 @@ createUserWithHandle brig = do pure (handle, userWithHandle) getUserInfoFromHandle :: - (MonadIO m, MonadCatch m, MonadFail m, MonadHttp m, HasCallStack) => + (MonadIO m, MonadCatch m, MonadHttp m, HasCallStack) => Brig -> Domain -> Handle -> @@ -650,7 +647,7 @@ getUserInfoFromHandle brig domain handle = do ) addClient :: - (Monad m, MonadCatch m, MonadIO m, MonadHttp m, MonadFail m, HasCallStack) => + (MonadHttp m, HasCallStack) => Brig -> UserId -> NewClient -> @@ -688,7 +685,7 @@ defNewClientWithVerificationCode mbCode ty pks lpk = } getPreKey :: - (MonadIO m, MonadCatch m, MonadFail m, MonadHttp m, HasCallStack) => + (MonadHttp m, HasCallStack) => Brig -> UserId -> UserId -> @@ -702,7 +699,7 @@ getPreKey brig zusr u c = . zUser zusr getTeamMember :: - (MonadIO m, MonadCatch m, MonadFail m, MonadHttp m, HasCallStack) => + (MonadIO m, MonadCatch m, MonadHttp m, HasCallStack) => UserId -> TeamId -> Galley -> @@ -716,14 +713,14 @@ getTeamMember u tid galley = . expect2xx ) -getConversationQualified :: (MonadIO m, MonadHttp m) => Galley -> UserId -> Qualified ConvId -> m ResponseLBS +getConversationQualified :: MonadHttp m => Galley -> UserId -> Qualified ConvId -> m ResponseLBS getConversationQualified galley usr cnv = get $ galley . paths ["conversations", toByteString' (qDomain cnv), toByteString' (qUnqualified cnv)] . zAuthAccess usr "conn" -createMLSConversation :: (MonadIO m, MonadHttp m) => Galley -> UserId -> ClientId -> m ResponseLBS +createMLSConversation :: MonadHttp m => Galley -> UserId -> ClientId -> m ResponseLBS createMLSConversation galley zusr c = do let conv = NewConv @@ -737,15 +734,15 @@ createMLSConversation galley zusr c = do Nothing roleNameWireAdmin ProtocolMLSTag - (Just c) post $ galley . path "/conversations" . zUser zusr . zConn "conn" + . zClient c . json conv -createConversation :: (MonadIO m, MonadHttp m) => Galley -> UserId -> [Qualified UserId] -> m ResponseLBS +createConversation :: MonadHttp m => Galley -> UserId -> [Qualified UserId] -> m ResponseLBS createConversation galley zusr usersToAdd = do let conv = NewConv @@ -759,7 +756,6 @@ createConversation galley zusr usersToAdd = do Nothing roleNameWireAdmin ProtocolProteusTag - Nothing post $ galley . path "/conversations" @@ -767,7 +763,7 @@ createConversation galley zusr usersToAdd = do . zConn "conn" . json conv -listConvIdsFirstPage :: (MonadIO m, MonadHttp m) => Galley -> UserId -> m ResponseLBS +listConvIdsFirstPage :: MonadHttp m => Galley -> UserId -> m ResponseLBS listConvIdsFirstPage galley zusr = do let req = GetMultiTablePageRequest (toRange (Proxy @1000)) Nothing :: GetPaginatedConversationIds post $ @@ -778,7 +774,7 @@ listConvIdsFirstPage galley zusr = do . json req listConvs :: - (MonadIO m, MonadHttp m) => + MonadHttp m => Galley -> UserId -> Range 1 1000 [Qualified ConvId] -> @@ -849,6 +845,9 @@ zAuthAccess u c = header "Z-Type" "access" . zUser u . zConn c zUser :: UserId -> Request -> Request zUser = header "Z-User" . B8.pack . show +zClient :: ClientId -> Request -> Request +zClient = header "Z-Client" . toByteString' + zConn :: ByteString -> Request -> Request zConn = header "Z-Connection" @@ -1045,7 +1044,7 @@ recoverN n m = -- service which is not being mocked, this helper can be used to do that. -- -- This is just an alias to 'runHttpT' to make the intent clear. -circumventSettingsOverride :: MonadIO m => Manager -> HttpT m a -> m a +circumventSettingsOverride :: Manager -> HttpT m a -> m a circumventSettingsOverride = runHttpT -- | This allows you to run requests against a brig instantiated using the given options. @@ -1315,13 +1314,18 @@ spawn cp minput = do (mout, ex) <- withCreateProcess cp { std_out = CreatePipe, - std_in = if isJust minput then CreatePipe else Inherit + std_in = CreatePipe } $ \minh mouth _ ph -> - let writeInput = for_ ((,) <$> minput <*> minh) $ \(input, inh) -> - BS.hPutStr inh input >> hClose inh + let writeInput = for_ minh $ \inh -> do + forM_ minput $ BS.hPutStr inh + hClose inh readOutput = (,) <$> traverse BS.hGetContents mouth <*> waitForProcess ph in snd <$> concurrently writeInput readOutput case (mout, ex) of (Just out, ExitSuccess) -> pure out _ -> assertFailure "Failed spawning process" + +assertJust :: (HasCallStack, MonadIO m) => Maybe a -> m a +assertJust (Just a) = pure a +assertJust Nothing = liftIO $ error "Expected Just, got Nothing" diff --git a/services/brig/test/integration/Util/AWS.hs b/services/brig/test/integration/Util/AWS.hs index 1c05b57b6a..5307fc0122 100644 --- a/services/brig/test/integration/Util/AWS.hs +++ b/services/brig/test/integration/Util/AWS.hs @@ -22,7 +22,6 @@ import Control.Lens import Data.ByteString.Conversion import qualified Data.ByteString.Lazy as Lazy import Data.Id -import qualified Data.ProtoLens as DP import qualified Data.Text as Text import qualified Data.Text.Encoding as Text import qualified Data.UUID as UUID @@ -33,80 +32,95 @@ import Test.Tasty.HUnit import qualified Util.Test.SQS as SQS import Wire.API.User +type UserJournalWatcher = Maybe (SQS.SQSWatcher PU.UserEvent) + +assertMessage :: (MonadUnliftIO m, HasCallStack) => UserJournalWatcher -> String -> (PU.UserEvent -> Bool) -> (String -> Maybe PU.UserEvent -> m ()) -> m () +assertMessage mWatcher label predicate callback = for_ mWatcher $ \watcher -> SQS.assertMessage watcher label predicate callback + isRealSESEnv :: AWS.Env -> Bool isRealSESEnv env = case view AWS.userJournalQueue env of Just url | "amazonaws.com" `Text.isSuffixOf` url -> True _ -> False -purgeJournalQueue :: AWS.Env -> IO () -purgeJournalQueue env = - for_ (view AWS.userJournalQueue env) $ - SQS.execute (view AWS.amazonkaEnv env) - . SQS.purgeQueue - --- | Fail unless journal queue is empty. -assertEmptyUserJournalQueue :: AWS.Env -> IO () -assertEmptyUserJournalQueue env = - for_ (view AWS.userJournalQueue env) $ \url -> do - let awsEnv = view AWS.amazonkaEnv env - SQS.assertNoMessages url awsEnv - --- | Fail unless journal queue passes a given check. -assertUserJournalQueue :: - DP.Message a => - String -> - AWS.Env -> - (String -> Maybe a -> IO ()) -> - IO () -assertUserJournalQueue label env check = do - for_ (view AWS.userJournalQueue env) $ \url -> do - let awsEnv = view AWS.amazonkaEnv env - SQS.assertQueue url label awsEnv check - -userActivateJournaled :: HasCallStack => User -> String -> Maybe PU.UserEvent -> IO () -userActivateJournaled u l (Just ev) = do +userActivateJournaled :: (HasCallStack, MonadIO m) => User -> String -> Maybe PU.UserEvent -> m () +userActivateJournaled u l (Just ev) = liftIO $ do assertEventType l PU.UserEvent'USER_ACTIVATE ev assertUserId l (userId u) ev assertTeamId l (userTeam u) ev assertName l (Just $ userDisplayName u) ev assertEmail l (userEmail u) ev assertLocale l (Just $ userLocale u) ev -userActivateJournaled _ l Nothing = assertFailure $ l <> ": Expected 1 UserActivate, got nothing" +userActivateJournaled _ l Nothing = liftIO $ assertFailure $ l <> ": Expected 1 UserActivate, got nothing" + +assertUserActivateJournaled :: forall m. (HasCallStack, MonadUnliftIO m) => UserJournalWatcher -> User -> String -> m () +assertUserActivateJournaled userJournalWatcher u label = + assertMessage userJournalWatcher label userActivateMatcher (userActivateJournaled u) + where + userActivateMatcher :: PU.UserEvent -> Bool + userActivateMatcher ev = + ev ^. PU.eventType == PU.UserEvent'USER_ACTIVATE + && decodeIdFromBS (ev ^. PU.userId) == userId u -- | Check for user update event in journal queue. -userUpdateJournaled :: HasCallStack => UserId -> UserUpdate -> String -> Maybe PU.UserEvent -> IO () -userUpdateJournaled uid update l (Just ev) = do +nameUpdateJournaled :: (HasCallStack, MonadIO m) => UserId -> Name -> String -> Maybe PU.UserEvent -> m () +nameUpdateJournaled uid name l (Just ev) = liftIO $ do assertEventType l PU.UserEvent'USER_UPDATE ev assertUserId l uid ev - assertName l (uupName update) ev -userUpdateJournaled _ _ l Nothing = assertFailure $ l <> ": Expected 1 UserUpdate, got nothing" + assertName l (Just name) ev +nameUpdateJournaled _ _ l Nothing = liftIO $ assertFailure $ l <> ": Expected 1 UserUpdate, got nothing" + +assertNameUpdateJournaled :: (HasCallStack, MonadUnliftIO m) => UserJournalWatcher -> UserId -> Name -> String -> m () +assertNameUpdateJournaled userJournalWatcher uid name label = + assertMessage userJournalWatcher label (userUpdateMatcher uid) (nameUpdateJournaled uid name) + +userUpdateMatcher :: UserId -> PU.UserEvent -> Bool +userUpdateMatcher uid ev = + ev ^. PU.eventType == PU.UserEvent'USER_UPDATE + && decodeIdFromBS (ev ^. PU.userId) == uid -userLocaleUpdateJournaled :: HasCallStack => UserId -> Locale -> String -> Maybe PU.UserEvent -> IO () -userLocaleUpdateJournaled uid loc l (Just ev) = do +userLocaleUpdateJournaled :: (HasCallStack, MonadIO m) => UserId -> Locale -> String -> Maybe PU.UserEvent -> m () +userLocaleUpdateJournaled uid loc l (Just ev) = liftIO $ do assertEventType l PU.UserEvent'USER_UPDATE ev assertUserId l uid ev assertLocale l (Just loc) ev -userLocaleUpdateJournaled _ _ l Nothing = assertFailure $ l <> ": Expected 1 UserUpdate, got nothing" +userLocaleUpdateJournaled _ _ l Nothing = liftIO $ assertFailure $ l <> ": Expected 1 UserUpdate, got nothing" -userEmailUpdateJournaled :: HasCallStack => UserId -> Email -> String -> Maybe PU.UserEvent -> IO () -userEmailUpdateJournaled uid em l (Just ev) = do +assertLocaleUpdateJournaled :: (HasCallStack, MonadUnliftIO m) => UserJournalWatcher -> UserId -> Locale -> String -> m () +assertLocaleUpdateJournaled userJournalWatcher uid loc label = + assertMessage userJournalWatcher label (userUpdateMatcher uid) (userLocaleUpdateJournaled uid loc) + +userEmailUpdateJournaled :: (HasCallStack, MonadIO m) => UserId -> Email -> String -> Maybe PU.UserEvent -> m () +userEmailUpdateJournaled uid em l (Just ev) = liftIO $ do assertEventType l PU.UserEvent'USER_UPDATE ev assertUserId l uid ev assertEmail l (Just em) ev -userEmailUpdateJournaled _ _ l Nothing = assertFailure $ l <> ": Expected 1 UserUpdate, got nothing" +userEmailUpdateJournaled _ _ l Nothing = liftIO $ assertFailure $ l <> ": Expected 1 UserUpdate, got nothing" + +assertEmailUpdateJournaled :: (HasCallStack, MonadUnliftIO m) => UserJournalWatcher -> UserId -> Email -> String -> m () +assertEmailUpdateJournaled userJournalWatcher uid em label = + assertMessage userJournalWatcher label (userUpdateMatcher uid) (userEmailUpdateJournaled uid em) -- | Check for user deletion event in journal queue. -userDeleteJournaled :: HasCallStack => UserId -> String -> Maybe PU.UserEvent -> IO () -userDeleteJournaled uid l (Just ev) = do +userDeleteJournaled :: (HasCallStack, MonadIO m) => UserId -> String -> Maybe PU.UserEvent -> m () +userDeleteJournaled uid l (Just ev) = liftIO $ do assertEventType l PU.UserEvent'USER_DELETE ev assertUserId l uid ev -userDeleteJournaled _ l Nothing = assertFailure $ l <> ": Expected 1 UserDelete, got nothing" +userDeleteJournaled _ l Nothing = liftIO $ assertFailure $ l <> ": Expected 1 UserDelete, got nothing" + +assertDeleteJournaled :: (HasCallStack, MonadUnliftIO m) => UserJournalWatcher -> UserId -> String -> m () +assertDeleteJournaled userJournalWatcher uid label = + assertMessage userJournalWatcher label (userDeleteMatcher uid) (userDeleteJournaled uid) + +userDeleteMatcher :: UserId -> PU.UserEvent -> Bool +userDeleteMatcher uid ev = + ev ^. PU.eventType == PU.UserEvent'USER_DELETE + && decodeIdFromBS (ev ^. PU.userId) == uid assertEventType :: String -> PU.UserEvent'EventType -> PU.UserEvent -> IO () assertEventType l et ev = assertEqual (l <> "eventType") et (ev ^. PU.eventType) -assertUserId :: String -> UserId -> PU.UserEvent -> IO () -assertUserId l uid ev = assertEqual (l <> "userId") uid (Id $ fromMaybe (error "failed to decode userId") $ UUID.fromByteString $ Lazy.fromStrict (ev ^. PU.userId)) +assertUserId :: HasCallStack => String -> UserId -> PU.UserEvent -> IO () +assertUserId l uid ev = assertEqual (l <> "userId") uid (decodeIdFromBS (ev ^. PU.userId)) assertTeamId :: String -> Maybe TeamId -> PU.UserEvent -> IO () assertTeamId l (Just tid) ev = assertEqual (l <> "teamId should exist") tid ((Id . fromMaybe (error "failed to parse teamId")) ((UUID.fromByteString . Lazy.fromStrict) =<< (ev ^? PU.teamId))) @@ -123,3 +137,6 @@ assertEmail l Nothing ev = assertEqual (l <> "email should not exist") Nothing ( assertLocale :: String -> Maybe Locale -> PU.UserEvent -> IO () assertLocale l (Just loc) ev = assertEqual (l <> "locale should exist") loc (fromMaybe (error "Failed to convert to locale") $ parseLocale $ Text.decodeLatin1 $ fromMaybe "failed to decode locale value" $ fromByteString $ ev ^. PU.locale) assertLocale l Nothing ev = assertEqual (l <> "locale should not exist") Nothing (ev ^. PU.maybe'locale) + +decodeIdFromBS :: ByteString -> Id a +decodeIdFromBS = Id . fromMaybe (error "failed to decode userId") . UUID.fromByteString . Lazy.fromStrict diff --git a/services/cannon/cannon.cabal b/services/cannon/cannon.cabal index 6bdca915d3..2c76e00e2c 100644 --- a/services/cannon/cannon.cabal +++ b/services/cannon/cannon.cabal @@ -26,7 +26,6 @@ library Cannon.Run Cannon.Types Cannon.WS - Main other-modules: Paths_cannon hs-source-dirs: src @@ -73,6 +72,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: aeson >=2.0.1.0 @@ -102,7 +102,6 @@ library , servant-conduit , servant-server , strict >=0.3.2 - , swagger >=0.2 , text >=1.1 , tinylog >=0.10 , types-common >=0.16 @@ -122,7 +121,7 @@ library default-language: Haskell2010 executable cannon - main-is: src/Main.hs + main-is: exec/Main.hs other-modules: Paths_cannon default-extensions: NoImplicitPrelude @@ -168,7 +167,7 @@ executable cannon -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -threaded -rtsopts -with-rtsopts=-N -with-rtsopts=-T - -with-rtsopts=-M1g -with-rtsopts=-ki4k + -with-rtsopts=-M1g -with-rtsopts=-ki4k -Wredundant-constraints build-depends: base @@ -233,7 +232,7 @@ test-suite cannon-tests ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -with-rtsopts=-N + -threaded -with-rtsopts=-N -Wredundant-constraints build-depends: async @@ -307,7 +306,7 @@ benchmark cannon-bench ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -with-rtsopts=-N + -threaded -with-rtsopts=-N -Wredundant-constraints build-depends: async diff --git a/services/cannon/cannon.integration.yaml b/services/cannon/cannon.integration.yaml index 5bd5d2a706..f64f3c104f 100644 --- a/services/cannon/cannon.integration.yaml +++ b/services/cannon/cannon.integration.yaml @@ -21,5 +21,5 @@ drainOpts: millisecondsBetweenBatches: 500 minBatchSize: 5 -logLevel: Info +logLevel: Warn logNetStrings: false diff --git a/services/cannon/default.nix b/services/cannon/default.nix index ef66129761..9c2ef7af21 100644 --- a/services/cannon/default.nix +++ b/services/cannon/default.nix @@ -35,7 +35,6 @@ , servant-conduit , servant-server , strict -, swagger , tasty , tasty-hunit , tasty-quickcheck @@ -89,7 +88,6 @@ mkDerivation { servant-conduit servant-server strict - swagger text tinylog types-common diff --git a/services/cannon/src/Main.hs b/services/cannon/exec/Main.hs similarity index 100% rename from services/cannon/src/Main.hs rename to services/cannon/exec/Main.hs diff --git a/services/cannon/src/Cannon/App.hs b/services/cannon/src/Cannon/App.hs index 23988f8c9f..3456deaa64 100644 --- a/services/cannon/src/Cannon/App.hs +++ b/services/cannon/src/Cannon/App.hs @@ -76,7 +76,7 @@ wsapp k c e pc = runWS e (go `catches` ioErrors k) clock <- getClock continue ws clock k `finally` terminate k ws -continue :: (MonadLogger m, MonadUnliftIO m, MonadIO m) => Websocket -> Clock -> Key -> m () +continue :: (MonadLogger m, MonadUnliftIO m) => Websocket -> Clock -> Key -> m () continue ws clock k = do runInIO <- askRunInIO liftIO $ do @@ -161,7 +161,7 @@ rejectOnError p x = do _ -> pure () throwM x -ioErrors :: (MonadLogger m, MonadIO m) => Key -> [Handler m ()] +ioErrors :: MonadLogger m => Key -> [Handler m ()] ioErrors k = let f s = Logger.err $ client (key2bytes k) . msg s in [ Handler $ \(x :: HandshakeException) -> f (show x), diff --git a/services/cannon/src/Cannon/Dict.hs b/services/cannon/src/Cannon/Dict.hs index a937db8569..921c6fe6a4 100644 --- a/services/cannon/src/Cannon/Dict.hs +++ b/services/cannon/src/Cannon/Dict.hs @@ -49,28 +49,28 @@ empty w = then Dict <$> V.generateM w (const $ newIORef SHM.empty) else error "Dict.empty: slice number out of range [1, 8191]" -insert :: (Eq a, Hashable a, MonadIO m) => a -> b -> Dict a b -> m () +insert :: (Hashable a, MonadIO m) => a -> b -> Dict a b -> m () insert k v = mutDict (SHM.insert k v) . getSlice k -add :: (Eq a, Hashable a, MonadIO m) => a -> b -> Dict a b -> m Bool +add :: (Hashable a, MonadIO m) => a -> b -> Dict a b -> m Bool add k v d = liftIO . atomicModifyIORef' (getSlice k d) $ \m -> if k `elem` SHM.keys m then (m, False) else (SHM.insert k v m, True) -remove :: (Eq a, Hashable a, MonadIO m) => a -> Dict a b -> m Bool +remove :: (Hashable a, MonadIO m) => a -> Dict a b -> m Bool remove = removeIf (const True) -removeIf :: (Eq a, Hashable a, MonadIO m) => (Maybe b -> Bool) -> a -> Dict a b -> m Bool +removeIf :: (Hashable a, MonadIO m) => (Maybe b -> Bool) -> a -> Dict a b -> m Bool removeIf f k d = liftIO . atomicModifyIORef' (getSlice k d) $ \m -> if f (SHM.lookup k m) then (SHM.delete k m, True) else (m, False) -lookup :: (Eq a, Hashable a, MonadIO m) => a -> Dict a b -> m (Maybe b) +lookup :: (Hashable a, MonadIO m) => a -> Dict a b -> m (Maybe b) lookup k = liftIO . fmap (SHM.lookup k) . readIORef . getSlice k -toList :: (MonadIO m, Hashable a) => Dict a b -> m [(a, b)] +toList :: MonadIO m => Dict a b -> m [(a, b)] toList = fmap (mconcat . V.toList) . V.mapM (fmap SHM.toList . readIORef) @@ -86,5 +86,5 @@ mutDict :: m () mutDict f d = liftIO . atomicModifyIORef' d $ \m -> (f m, ()) -getSlice :: (Hashable a) => a -> Dict a b -> IORef (SizedHashMap a b) +getSlice :: Hashable a => a -> Dict a b -> IORef (SizedHashMap a b) getSlice k (Dict m) = m ! (hash k `mod` V.length m) diff --git a/services/cargohold/cargohold.cabal b/services/cargohold/cargohold.cabal index 74ebc915c5..c5fa83a620 100644 --- a/services/cargohold/cargohold.cabal +++ b/services/cargohold/cargohold.cabal @@ -33,7 +33,6 @@ library CargoHold.Run CargoHold.S3 CargoHold.Util - Main other-modules: Paths_cargohold hs-source-dirs: src @@ -80,6 +79,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -fplugin=TransitiveAnns.Plugin -Wredundant-constraints build-depends: aeson >=2.0.1.0 @@ -120,10 +120,10 @@ library , safe >=0.3 , servant , servant-server - , swagger >=0.2 , text >=1.1 , time >=1.4 , tinylog >=0.10 + , transitive-anns , types-common >=0.16 , types-common-aws , unliftio @@ -140,7 +140,7 @@ library default-language: Haskell2010 executable cargohold - main-is: src/Main.hs + main-is: exec/Main.hs other-modules: Paths_cargohold default-extensions: NoImplicitPrelude @@ -185,7 +185,7 @@ executable cargohold ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -rtsopts -with-rtsopts=-T + -threaded -rtsopts -with-rtsopts=-T -Wredundant-constraints build-depends: aeson >=2.0.1.0 @@ -271,6 +271,7 @@ executable cargohold-integration ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: aeson >=2.0.1.0 diff --git a/services/cargohold/cargohold.integration.yaml b/services/cargohold/cargohold.integration.yaml index d43b06f019..0f85c2b42c 100644 --- a/services/cargohold/cargohold.integration.yaml +++ b/services/cargohold/cargohold.integration.yaml @@ -24,5 +24,5 @@ settings: downloadLinkTTL: 300 # Seconds federationDomain: example.com -logLevel: Info +logLevel: Warn logNetStrings: false diff --git a/services/cargohold/default.nix b/services/cargohold/default.nix index 082cf037e3..017b1fae70 100644 --- a/services/cargohold/default.nix +++ b/services/cargohold/default.nix @@ -51,13 +51,13 @@ , servant-client , servant-client-core , servant-server -, swagger , tagged , tasty , tasty-hunit , text , time , tinylog +, transitive-anns , types-common , types-common-aws , unliftio @@ -116,10 +116,10 @@ mkDerivation { safe servant servant-server - swagger text time tinylog + transitive-anns types-common types-common-aws unliftio diff --git a/services/cargohold/src/Main.hs b/services/cargohold/exec/Main.hs similarity index 100% rename from services/cargohold/src/Main.hs rename to services/cargohold/exec/Main.hs diff --git a/services/cargohold/src/CargoHold/API/Public.hs b/services/cargohold/src/CargoHold/API/Public.hs index 67821235c9..5581785271 100644 --- a/services/cargohold/src/CargoHold/API/Public.hs +++ b/services/cargohold/src/CargoHold/API/Public.hs @@ -28,6 +28,7 @@ import Data.ByteString.Builder import qualified Data.ByteString.Lazy as LBS import Data.Domain import Data.Id +import Data.Kind import Data.Qualified import Imports hiding (head) import qualified Network.HTTP.Types as HTTP @@ -59,13 +60,13 @@ servantSitemap = providerAPI = uploadAssetV3 @tag :<|> downloadAssetV3 @tag :<|> deleteAssetV3 @tag legacyAPI = legacyDownloadPlain :<|> legacyDownloadPlain :<|> legacyDownloadOtr qualifiedAPI :: ServerT QualifiedAPI Handler - qualifiedAPI = callsFed downloadAssetV4 :<|> deleteAssetV4 + qualifiedAPI = callsFed (exposeAnnotations downloadAssetV4) :<|> deleteAssetV4 mainAPI :: ServerT MainAPI Handler mainAPI = renewTokenV3 :<|> deleteTokenV3 :<|> uploadAssetV3 @'UserPrincipalTag - :<|> callsFed downloadAssetV4 + :<|> callsFed (exposeAnnotations downloadAssetV4) :<|> deleteAssetV4 internalSitemap :: ServerT InternalAPI Handler @@ -96,7 +97,7 @@ instance HasLocation 'ProviderPrincipalTag where assetKeyToText (tUnqualified key) ] -class HasLocation tag => MakePrincipal (tag :: PrincipalTag) (id :: *) | id -> tag, tag -> id where +class HasLocation tag => MakePrincipal (tag :: PrincipalTag) (id :: Type) | id -> tag, tag -> id where mkPrincipal :: id -> V3.Principal instance MakePrincipal 'UserPrincipalTag (Local UserId) where @@ -150,7 +151,7 @@ downloadAssetV3 usr key tok1 tok2 = do AssetLocation <$$> V3.download (mkPrincipal usr) key (tok1 <|> tok2) downloadAssetV4 :: - (CallsFed 'Cargohold "get-asset", CallsFed 'Cargohold "stream-asset") => + () => Local UserId -> Qualified AssetKey -> Maybe AssetToken -> diff --git a/services/cargohold/src/CargoHold/Federation.hs b/services/cargohold/src/CargoHold/Federation.hs index 94a8bebc7e..8cb7880f86 100644 --- a/services/cargohold/src/CargoHold/Federation.hs +++ b/services/cargohold/src/CargoHold/Federation.hs @@ -48,7 +48,7 @@ import Wire.API.Federation.Error -- is streamed back through our outward federator, as well as the remote one. downloadRemoteAsset :: - (CallsFed 'Cargohold "get-asset", CallsFed 'Cargohold "stream-asset") => + () => Local UserId -> Remote AssetKey -> Maybe AssetToken -> diff --git a/services/federator/default.nix b/services/federator/default.nix index f274290059..3c46834024 100644 --- a/services/federator/default.nix +++ b/services/federator/default.nix @@ -25,6 +25,7 @@ , filepath , gitignoreSource , hinotify +, HsOpenSSL , hspec , http-client , http-client-openssl @@ -104,6 +105,7 @@ mkDerivation { extended filepath hinotify + HsOpenSSL http-client http-client-openssl http-media @@ -166,6 +168,7 @@ mkDerivation { extended filepath hinotify + HsOpenSSL hspec http-client http-client-openssl @@ -233,6 +236,7 @@ mkDerivation { extended filepath hinotify + HsOpenSSL http-client http-client-openssl http-media diff --git a/services/federator/exec/Main.hs b/services/federator/exec/Main.hs index bc35cda243..f0badd532e 100644 --- a/services/federator/exec/Main.hs +++ b/services/federator/exec/Main.hs @@ -22,10 +22,11 @@ where import Federator.Run (run) import Imports +import OpenSSL import Util.Options (getOptions) main :: IO () -main = do +main = withOpenSSL $ do let desc = "Federation Service" defaultPath = "/etc/wire/federator/conf/federator.yaml" options <- getOptions desc Nothing defaultPath diff --git a/services/federator/federator.cabal b/services/federator/federator.cabal index 547d3d7536..c72fbcb7a9 100644 --- a/services/federator/federator.cabal +++ b/services/federator/federator.cabal @@ -96,6 +96,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: aeson @@ -115,6 +116,7 @@ library , extended , filepath , hinotify + , HsOpenSSL , http-client , http-client-openssl , http-media @@ -206,65 +208,14 @@ executable federator -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -threaded -with-rtsopts=-N1 -with-rtsopts=-T -rtsopts + -Wredundant-constraints build-depends: - aeson - , async - , base - , bilge - , binary - , bytestring - , bytestring-conversion - , constraints - , containers - , data-default - , dns - , dns-util - , either - , exceptions - , extended + base , federator - , filepath - , hinotify - , http-client - , http-client-openssl - , http-media - , http-types - , http2 + , HsOpenSSL , 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 default-language: Haskell2010 @@ -321,6 +272,7 @@ executable federator-integration ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints -threaded -with-rtsopts=-N1 build-depends: aeson @@ -344,6 +296,7 @@ executable federator-integration , federator , filepath , hinotify + , HsOpenSSL , hspec , http-client , http-client-openssl @@ -453,7 +406,7 @@ test-suite federator-tests ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -with-rtsopts=-N + -threaded -with-rtsopts=-N -Wredundant-constraints build-depends: aeson @@ -475,6 +428,7 @@ test-suite federator-tests , federator , filepath , hinotify + , HsOpenSSL , http-client , http-client-openssl , http-media diff --git a/services/federator/federator.integration.yaml b/services/federator/federator.integration.yaml index 42e08d35c5..9562b697e1 100644 --- a/services/federator/federator.integration.yaml +++ b/services/federator/federator.integration.yaml @@ -14,7 +14,7 @@ galley: host: 0.0.0.0 port: 8085 -logLevel: Debug +logLevel: Warn logNetStrings: false optSettings: diff --git a/services/federator/src/Federator/App.hs b/services/federator/src/Federator/App.hs index bc3abfdaf3..80caae1baf 100644 --- a/services/federator/src/Federator/App.hs +++ b/services/federator/src/Federator/App.hs @@ -74,7 +74,7 @@ instance MonadUnliftIO m => MonadUnliftIO (AppT m) where instance MonadTrans AppT where lift = AppT . lift -instance (Monad m, MonadIO m) => MonadHttp (AppT m) where +instance MonadIO m => MonadHttp (AppT m) where handleRequestWithCont req handler = do manager <- view httpManager <$> ask liftIO $ withResponse req manager handler @@ -82,7 +82,12 @@ instance (Monad m, MonadIO m) => MonadHttp (AppT m) where runAppT :: forall m a. Env -> AppT m a -> m a runAppT e (AppT ma) = runReaderT ma e -embedApp :: Members '[Embed m, Input Env] r => AppT m a -> Sem r a +embedApp :: + ( Member (Embed m) r, + Member (Input Env) r + ) => + AppT m a -> + Sem r a embedApp (AppT action) = do env <- input embed $ runReaderT action env diff --git a/services/federator/src/Federator/Discovery.hs b/services/federator/src/Federator/Discovery.hs index 384d696717..57c7e6c521 100644 --- a/services/federator/src/Federator/Discovery.hs +++ b/services/federator/src/Federator/Discovery.hs @@ -64,18 +64,27 @@ data DiscoverFederator m a where makeSem ''DiscoverFederator discoverFederatorWithError :: - Members '[DiscoverFederator, Polysemy.Error DiscoveryFailure] r => + ( Member DiscoverFederator r, + Member (Polysemy.Error DiscoveryFailure) r + ) => Domain -> Sem r SrvTarget discoverFederatorWithError = Polysemy.fromEither <=< discoverFederator discoverAllFederatorsWithError :: - Members '[DiscoverFederator, Polysemy.Error DiscoveryFailure] r => + ( Member DiscoverFederator r, + Member (Polysemy.Error DiscoveryFailure) r + ) => Domain -> Sem r (NonEmpty SrvTarget) discoverAllFederatorsWithError = Polysemy.fromEither <=< discoverAllFederators -runFederatorDiscovery :: Members '[DNSLookup, TinyLog] r => Sem (DiscoverFederator ': r) a -> Sem r a +runFederatorDiscovery :: + ( Member DNSLookup r, + Member TinyLog r + ) => + Sem (DiscoverFederator ': r) a -> + Sem r a runFederatorDiscovery = interpret $ \case DiscoverFederator d -> -- FUTUREWORK(federation): orderSrvResult and try the list in order this @@ -89,7 +98,12 @@ runFederatorDiscovery = interpret $ \case -- (https://wearezeta.atlassian.net/browse/SQCORE-912) domainSrv d = cs $ "_wire-server-federator._tcp." <> domainText d -lookupDomainByDNS :: Members '[DNSLookup, TinyLog] r => ByteString -> Sem r (Either DiscoveryFailure (NonEmpty SrvTarget)) +lookupDomainByDNS :: + ( Member DNSLookup r, + Member TinyLog r + ) => + ByteString -> + Sem r (Either DiscoveryFailure (NonEmpty SrvTarget)) lookupDomainByDNS domainSrv = do res <- Lookup.lookupSRV domainSrv case res of diff --git a/services/federator/src/Federator/Env.hs b/services/federator/src/Federator/Env.hs index 1ce2a6c162..3ffb798c9b 100644 --- a/services/federator/src/Federator/Env.hs +++ b/services/federator/src/Federator/Env.hs @@ -24,21 +24,15 @@ module Federator.Env where import Bilge (RequestId) import Control.Lens (makeLenses) import Data.Metrics (Metrics) -import Data.X509.CertificateStore import Federator.Options (RunSettings) import Imports import Network.DNS.Resolver (Resolver) import qualified Network.HTTP.Client as HTTP -import qualified Network.TLS as TLS +import OpenSSL.Session (SSLContext) import qualified System.Logger.Class as LC import Util.Options import Wire.API.Federation.Component -data TLSSettings = TLSSettings - { _caStore :: CertificateStore, - _creds :: TLS.Credential - } - data Env = Env { _metrics :: Metrics, _applog :: LC.Logger, @@ -47,8 +41,7 @@ data Env = Env _runSettings :: RunSettings, _service :: Component -> Endpoint, _httpManager :: HTTP.Manager, - _tls :: IORef TLSSettings + _sslContext :: IORef SSLContext } -makeLenses ''TLSSettings makeLenses ''Env diff --git a/services/federator/src/Federator/ExternalServer.hs b/services/federator/src/Federator/ExternalServer.hs index 68da960ac6..65444d252e 100644 --- a/services/federator/src/Federator/ExternalServer.hs +++ b/services/federator/src/Federator/ExternalServer.hs @@ -44,17 +44,15 @@ import Wire.API.Federation.Domain -- FUTUREWORK(federation): Versioning of the federation API. callInward :: - Members - '[ ServiceStreaming, - Embed IO, - TinyLog, - DiscoverFederator, - Error ValidationError, - Error DiscoveryFailure, - Error ServerError, - Input RunSettings - ] - r => + ( Member ServiceStreaming r, + Member (Embed IO) r, + Member TinyLog r, + Member DiscoverFederator r, + Member (Error ValidationError) r, + Member (Error DiscoveryFailure) r, + Member (Error ServerError) r, + Member (Input RunSettings) r + ) => Wai.Request -> Sem r Wai.Response callInward wreq = do @@ -99,7 +97,9 @@ data RequestData = RequestData -- -- FUTUREWORK: use higher-level effects parseRequestData :: - Members '[Error ServerError, Embed IO] r => + ( Member (Error ServerError) r, + Member (Embed IO) r + ) => Wai.Request -> Sem r RequestData parseRequestData req = do diff --git a/services/federator/src/Federator/InternalServer.hs b/services/federator/src/Federator/InternalServer.hs index 084907c6eb..76d0fdd443 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 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, @@ -82,7 +46,9 @@ data RequestData = RequestData } parseRequestData :: - Members '[Error ServerError, Embed IO] r => + ( Member (Error ServerError) r, + Member (Embed IO) r + ) => Wai.Request -> Sem r RequestData parseRequestData req = do @@ -112,7 +78,12 @@ parseRequestData req = do } callOutward :: - Members '[Remote, Embed IO, Error ValidationError, Error ServerError, Input RunSettings] r => + ( Member Remote r, + Member (Embed IO) r, + Member (Error ValidationError) r, + Member (Error ServerError) r, + Member (Input RunSettings) r + ) => Wai.Request -> Sem r Wai.Response callOutward req = do diff --git a/services/federator/src/Federator/Monitor.hs b/services/federator/src/Federator/Monitor.hs index af5049d2f8..6a6bcce8df 100644 --- a/services/federator/src/Federator/Monitor.hs +++ b/services/federator/src/Federator/Monitor.hs @@ -23,20 +23,20 @@ module Federator.Monitor where import Control.Exception (bracket, throw) -import Federator.Env (TLSSettings (..)) import Federator.Monitor.Internal import Federator.Options (RunSettings (..)) import Imports +import OpenSSL.Session (SSLContext) import qualified Polysemy import qualified Polysemy.Error as Polysemy import System.Logger (Logger) -mkTLSSettingsOrThrow :: RunSettings -> IO TLSSettings -mkTLSSettingsOrThrow = Polysemy.runM . runEither . Polysemy.runError @FederationSetupError . mkTLSSettings +mkTLSSettingsOrThrow :: RunSettings -> IO SSLContext +mkTLSSettingsOrThrow = Polysemy.runM . runEither . Polysemy.runError @FederationSetupError . mkSSLContext where runEither = (either (Polysemy.embed @IO . throw) pure =<<) -withMonitor :: Logger -> IORef TLSSettings -> RunSettings -> IO a -> IO a +withMonitor :: Logger -> IORef SSLContext -> RunSettings -> IO a -> IO a withMonitor logger tlsVar rs action = bracket ( runSemDefault diff --git a/services/federator/src/Federator/Monitor/Internal.hs b/services/federator/src/Federator/Monitor/Internal.hs index 4d38328bdc..d49248472f 100644 --- a/services/federator/src/Federator/Monitor/Internal.hs +++ b/services/federator/src/Federator/Monitor/Internal.hs @@ -24,14 +24,12 @@ import qualified Data.Set as Set import qualified Data.Text as Text import qualified Data.Text.Encoding as Text import qualified Data.Text.Encoding.Error as Text -import qualified Data.X509 as X509 -import Data.X509.CertificateStore -import Federator.Env (TLSSettings (..)) import Federator.Options (RunSettings (..)) import GHC.Foreign (peekCStringLen, withCStringLen) import GHC.IO.Encoding (getFileSystemEncoding) import Imports -import qualified Network.TLS as TLS +import OpenSSL.Session (SSLContext) +import qualified OpenSSL.Session as SSL import Polysemy (Embed, Member, Members, Sem, embed) import qualified Polysemy import qualified Polysemy.Error as Polysemy @@ -45,13 +43,12 @@ import System.Logger (Logger) import qualified System.Logger.Message as Log import System.Posix.ByteString (RawFilePath) import System.Posix.Files -import System.X509 import Wire.Arbitrary import qualified Wire.Sem.Logger.TinyLog as Log data Monitor = Monitor { monINotify :: INotify, - monTLS :: IORef TLSSettings, + monTLS :: IORef SSLContext, monWatches :: IORef Watches, monSettings :: RunSettings, monHandler :: WatchedPath -> Event -> IO (), @@ -108,7 +105,9 @@ runSemDefault :: Logger -> Sem '[TinyLog, Embed IO, Final IO] a -> IO a runSemDefault logger = Polysemy.runFinal . Polysemy.embedToFinal . Log.loggerToTinyLog logger logErrors :: - Members '[TinyLog, Polysemy.Error FederationSetupError] r => + ( Member TinyLog r, + Member (Polysemy.Error FederationSetupError) r + ) => Sem r a -> Sem r a logErrors action = Polysemy.catch action $ \err -> do @@ -124,7 +123,10 @@ logAndIgnoreErrors :: logAndIgnoreErrors = void . Polysemy.runError . logErrors delMonitor :: - (Members '[TinyLog, Embed IO, Final IO] r) => + ( Member TinyLog r, + Member (Embed IO) r, + Member (Final IO) r + ) => Monitor -> Sem r () delMonitor monitor = Polysemy.resourceToIOFinal @@ -139,21 +141,24 @@ delMonitor monitor = Polysemy.resourceToIOFinal stop (wd, _) = do -- ignore exceptions when removing watches embed . void . try @IOException $ removeWatch wd - Log.debug $ + Log.trace $ Log.msg ("stopped watching file" :: Text) . Log.field "descriptor" (show wd) mkMonitor :: - ( Members '[TinyLog, Embed IO] r, - Members '[TinyLog, Embed IO, Polysemy.Error FederationSetupError] r1 + ( Member TinyLog r, + Member (Embed IO) r, + Member TinyLog r1, + Member (Embed IO) r1, + Member (Polysemy.Error FederationSetupError) r1 ) => (Sem r1 () -> IO ()) -> - IORef TLSSettings -> + IORef SSLContext -> RunSettings -> Sem r Monitor mkMonitor runSem tlsVar rs = do inotify <- embed initINotify - Log.debug $ + Log.trace $ Log.msg ("inotify initialized" :: Text) . Log.field "inotify" (show inotify) @@ -178,7 +183,10 @@ data Action = ReplaceWatch RawFilePath | ReloadSettings deriving (Eq, Ord, Show) handleEvent :: - Members '[TinyLog, Embed IO, Polysemy.Error FederationSetupError] r => + ( Member TinyLog r, + Member (Embed IO) r, + Member (Polysemy.Error FederationSetupError) r + ) => (Sem r () -> IO ()) -> Monitor -> WatchedPath -> @@ -208,14 +216,17 @@ getActions (WatchedDir dir paths) (Created _ path) getActions _ _ = [] applyAction :: - (Members '[TinyLog, Embed IO, Polysemy.Error FederationSetupError] r) => + ( Member TinyLog r, + Member (Embed IO) r, + Member (Polysemy.Error FederationSetupError) r + ) => Monitor -> Action -> Sem r () applyAction monitor ReloadSettings = do - tls' <- mkTLSSettings (monSettings monitor) + sslCtx' <- mkSSLContext (monSettings monitor) Log.info $ Log.msg ("updating TLS settings" :: Text) - embed @IO $ atomicWriteIORef (monTLS monitor) tls' + embed @IO $ atomicWriteIORef (monTLS monitor) sslCtx' applyAction monitor (ReplaceWatch path) = do watches <- readIORef (monWatches monitor) case Map.lookup path watches of @@ -228,7 +239,9 @@ applyAction monitor (ReplaceWatch path) = do WatchedFile _ -> pure () addWatchedFile :: - Members '[TinyLog, Embed IO] r => + ( Member TinyLog r, + Member (Embed IO) r + ) => Monitor -> WatchedPath -> Sem r () @@ -244,7 +257,7 @@ addWatchedFile monitor wpath = do let pathText = Text.decodeUtf8With Text.lenientDecode (watchedPath wpath) case r of Right w -> - Log.debug $ + Log.trace $ Log.msg ("watching file" :: Text) . Log.field "descriptor" (show w) . Log.field "path" pathText @@ -323,57 +336,86 @@ watchedDirs resolve path = do pure (dirs0 ++ dirs1) data FederationSetupError - = InvalidCAStore FilePath + = InvalidCAStore FilePath String | InvalidClientCertificate String + | InvalidClientPrivateKey String + | CertificateAndPrivateKeyDoNotMatch FilePath FilePath + | SSLException SSL.SomeSSLException deriving (Show) instance Exception FederationSetupError showFederationSetupError :: FederationSetupError -> Text -showFederationSetupError (InvalidCAStore path) = "invalid CA store: " <> Text.pack path +showFederationSetupError (InvalidCAStore path msg) = "invalid CA store: " <> Text.pack path <> ", error: " <> Text.pack msg showFederationSetupError (InvalidClientCertificate msg) = Text.pack msg +showFederationSetupError (InvalidClientPrivateKey msg) = Text.pack msg +showFederationSetupError (CertificateAndPrivateKeyDoNotMatch cert key) = Text.pack $ "Certificate and private key do not match, certificate: " <> cert <> ", private key: " <> key +showFederationSetupError (SSLException exc) = Text.pack $ "Unexpected SSL Exception: " <> displayException exc -mkTLSSettings :: - Members '[Embed IO, Polysemy.Error FederationSetupError] r => - RunSettings -> - Sem r TLSSettings -mkTLSSettings settings = - TLSSettings - <$> mkCAStore settings - <*> mkCreds settings - -mkCAStore :: - Members '[Embed IO, Polysemy.Error FederationSetupError] r => - RunSettings -> - Sem r CertificateStore -mkCAStore settings = do - customCAStore <- fmap (fromRight mempty) . Polysemy.runError @() $ do - path <- maybe (Polysemy.throw ()) pure $ remoteCAStore settings - embed (readCertificateStore path) - >>= maybe (Polysemy.throw (InvalidCAStore path)) pure - systemCAStore <- - if useSystemCAStore settings - then embed getSystemCertificateStore - else pure mempty - pure (customCAStore <> systemCAStore) - -mkCreds :: - Members '[Embed IO, Polysemy.Error FederationSetupError] r => +mkSSLContext :: + ( Member (Embed IO) r, + Member (Polysemy.Error FederationSetupError) r + ) => RunSettings -> - Sem r TLS.Credential -mkCreds settings = do - creds <- - Polysemy.fromExceptionVia - @SomeException - (InvalidClientCertificate . displayException) - $ TLS.credentialLoadX509 - (clientCertificate settings) - (clientPrivateKey settings) - case creds of - Left e -> Polysemy.throw (InvalidClientCertificate e) - Right (X509.CertificateChain [], _) -> - Polysemy.throw - ( InvalidClientCertificate - "could not read client certificate" - ) - Right x -> pure x + Sem r SSLContext +mkSSLContext settings = do + ctx <- mkSSLContextWithoutCert settings + + Polysemy.fromExceptionVia @SomeException (InvalidClientCertificate . displayException) $ + SSL.contextSetCertificateFile ctx (clientCertificate settings) + + Polysemy.fromExceptionVia @SomeException (InvalidClientPrivateKey . displayException) $ + SSL.contextSetPrivateKeyFile ctx (clientPrivateKey settings) + + privateKeyCheck <- Polysemy.fromExceptionVia @SSL.SomeSSLException SSLException $ SSL.contextCheckPrivateKey ctx + unless privateKeyCheck $ do + Polysemy.throw $ CertificateAndPrivateKeyDoNotMatch (clientCertificate settings) (clientPrivateKey settings) + + pure ctx + +mkSSLContextWithoutCert :: Members '[Embed IO, Polysemy.Error FederationSetupError] r => RunSettings -> Sem r SSLContext +mkSSLContextWithoutCert settings = do + ctx <- embed $ SSL.context + embed $ do + SSL.contextAddOption ctx SSL.SSL_OP_ALL + SSL.contextAddOption ctx SSL.SSL_OP_NO_SSLv2 + SSL.contextAddOption ctx SSL.SSL_OP_NO_SSLv3 + SSL.contextAddOption ctx SSL.SSL_OP_NO_TLSv1 + + -- Settings TLS13 ciphers requires another call to openssl, this has not + -- been implemented in HsOpenSSL yet. + SSL.contextSetCiphers ctx blessedTLS12Ciphers + + SSL.contextSetALPNProtos ctx ["h2"] + + SSL.contextSetVerificationMode ctx $ + SSL.VerifyPeer + { -- vpFailIfNoPeerCert and vpClientOnce are only relevant for servers + SSL.vpFailIfNoPeerCert = False, + SSL.vpClientOnce = False, + SSL.vpCallback = Nothing + } + forM_ (remoteCAStore settings) $ \caStorePath -> + Polysemy.fromExceptionVia @SomeException (InvalidCAStore caStorePath . displayException) $ + SSL.contextSetCAFile ctx caStorePath + + when (useSystemCAStore settings) $ + embed (SSL.contextSetDefaultVerifyPaths ctx) + + pure ctx + +-- Context and possible future work see +-- https://wearezeta.atlassian.net/browse/FS-33 +-- https://wearezeta.atlassian.net/browse/FS-444 +-- https://wearezeta.atlassian.net/browse/FS-443 +-- +-- The current list is compliant with TR-02102-2 +-- https://www.bsi.bund.de/SharedDocs/Downloads/EN/BSI/Publications/TechGuidelines/TG02102/BSI-TR-02102-2.html +blessedTLS12Ciphers :: String +blessedTLS12Ciphers = + intercalate + ":" + [ -- For TLS 1.2 (copied from nginx ingress config): + "ECDHE-ECDSA-AES256-GCM-SHA384", + "ECDHE-RSA-AES256-GCM-SHA384" + ] diff --git a/services/federator/src/Federator/Remote.hs b/services/federator/src/Federator/Remote.hs index 1ea810df18..445b8e43ea 100644 --- a/services/federator/src/Federator/Remote.hs +++ b/services/federator/src/Federator/Remote.hs @@ -23,32 +23,23 @@ module Federator.Remote RemoteError (..), interpretRemote, discoverAndCall, - blessedCiphers, ) where import qualified Control.Exception as E -import Control.Lens ((^.)) import Control.Monad.Codensity import Data.Binary.Builder -import Data.ByteString.Conversion (toByteString') import qualified Data.ByteString.Lazy as LBS -import Data.Default (def) import Data.Domain import qualified Data.Text as Text import qualified Data.Text.Encoding as Text import qualified Data.Text.Encoding.Error as Text -import qualified Data.X509 as X509 -import qualified Data.X509.Validation as X509 import Federator.Discovery -import Federator.Env (TLSSettings, caStore, creds) import Federator.Error -import Federator.Validation import Imports import qualified Network.HTTP.Types as HTTP import qualified Network.HTTP2.Client as HTTP2 -import Network.TLS as TLS -import qualified Network.TLS.Extra.Cipher as TLS +import OpenSSL.Session (SSLContext) import Polysemy import Polysemy.Error import Polysemy.Input @@ -106,30 +97,29 @@ data Remote m a where makeSem ''Remote interpretRemote :: - Members - '[ Embed (Codensity IO), - DiscoverFederator, - Error DiscoveryFailure, - Error RemoteError, - Input TLSSettings - ] - r => + ( Member (Embed (Codensity IO)) r, + Member DiscoverFederator r, + Member (Error DiscoveryFailure) r, + Member (Error RemoteError) r, + Member (Input SSLContext) r + ) => Sem (Remote ': r) a -> Sem r a interpretRemote = interpret $ \case DiscoverAndCall domain component rpc headers body -> do target@(SrvTarget hostname port) <- discoverFederatorWithError domain - settings <- input let path = LBS.toStrict . toLazyByteString $ HTTP.encodePathSegments ["federation", componentName component, rpc] - req' = HTTP2.requestBuilder HTTP.methodPost path headers body - tlsConfig = mkTLSConfig settings hostname port + -- filter out Host header, because the HTTP2 client adds it back + headers' = filter ((/= "Host") . fst) headers + req' = HTTP2.requestBuilder HTTP.methodPost path headers' body + sslCtx <- input resp <- mapError (RemoteError target) . (fromEither @FederatorClientHTTP2Error =<<) . embed $ Codensity $ \k -> E.catch - (withHTTP2Request (Just tlsConfig) req' hostname (fromIntegral port) (k . Right)) + (withHTTP2Request (Just sslCtx) req' hostname (fromIntegral port) (k . Right)) (k . Left) unless (HTTP.statusIsSuccessful (responseStatusCode resp)) $ do @@ -140,46 +130,3 @@ interpretRemote = interpret $ \case (responseStatusCode resp) (toLazyByteString bdy) pure resp - -mkTLSConfig :: TLSSettings -> ByteString -> Word16 -> TLS.ClientParams -mkTLSConfig settings hostname port = - ( defaultParamsClient - (Text.unpack (Text.decodeUtf8With Text.lenientDecode hostname)) - (toByteString' port) - ) - { TLS.clientSupported = - def - { TLS.supportedCiphers = blessedCiphers, - -- FUTUREWORK: Figure out if we can drop TLS 1.2 - TLS.supportedVersions = [TLS.TLS12, TLS.TLS13] - }, - TLS.clientHooks = - def - { TLS.onServerCertificate = - X509.validate - X509.HashSHA256 - X509.defaultHooks {X509.hookValidateName = validateDomainName} - X509.defaultChecks {X509.checkLeafKeyPurpose = [X509.KeyUsagePurpose_ServerAuth]}, - TLS.onCertificateRequest = \_ -> pure (Just (settings ^. creds)), - TLS.onSuggestALPN = pure (Just ["h2"]) -- we only support HTTP2 - }, - TLS.clientShared = def {TLS.sharedCAStore = settings ^. caStore} - } - --- Context and possible future work see --- https://wearezeta.atlassian.net/browse/FS-33 --- https://wearezeta.atlassian.net/browse/FS-444 --- https://wearezeta.atlassian.net/browse/FS-443 --- --- The current list is compliant to TR-02102-2 --- https://www.bsi.bund.de/SharedDocs/Downloads/EN/BSI/Publications/TechGuidelines/TG02102/BSI-TR-02102-2.html -blessedCiphers :: [Cipher] -blessedCiphers = - [ TLS.cipher_TLS13_AES128CCM8_SHA256, - TLS.cipher_TLS13_AES128CCM_SHA256, - TLS.cipher_TLS13_AES128GCM_SHA256, - TLS.cipher_TLS13_AES256GCM_SHA384, - -- For TLS 1.2 (copied from default nginx ingress config): - TLS.cipher_ECDHE_ECDSA_AES256GCM_SHA384, - TLS.cipher_ECDHE_RSA_AES256GCM_SHA384 - ] diff --git a/services/federator/src/Federator/Response.hs b/services/federator/src/Federator/Response.hs index 7cc670ad3d..0c6c30b649 100644 --- a/services/federator/src/Federator/Response.hs +++ b/services/federator/src/Federator/Response.hs @@ -27,6 +27,7 @@ where import Control.Lens import Control.Monad.Codensity import Data.ByteString.Builder +import Data.Kind import Federator.Discovery import Federator.Env import Federator.Error @@ -41,6 +42,7 @@ import qualified Network.Wai as Wai import qualified Network.Wai.Handler.Warp as Warp import qualified Network.Wai.Utilities.Error as Wai import qualified Network.Wai.Utilities.Server as Wai +import OpenSSL.Session (SSLContext) import Polysemy import Polysemy.Embed import Polysemy.Error @@ -55,7 +57,7 @@ import Wire.Sem.Logger.TinyLog defaultHeaders :: [HTTP.Header] defaultHeaders = [("Content-Type", "application/json")] -class ErrorEffects (ee :: [*]) r where +class ErrorEffects (ee :: [Type]) r where type Row ee :: EffectRow runWaiErrors :: Sem (Append (Row ee) r) Wai.Response -> @@ -86,7 +88,12 @@ runWaiError = . mapError toWai . raiseUnder where - logError :: Members '[Error Wai.Error, TinyLog] r => Wai.Error -> Sem r a + logError :: + ( Member (Error Wai.Error) r, + Member TinyLog r + ) => + Wai.Error -> + Sem r a logError e = do err $ Wai.logErrorMsg e throw e @@ -111,7 +118,7 @@ type AllEffects = DNSLookup, -- needed by DiscoverFederator ServiceStreaming, Input RunSettings, - Input TLSSettings, -- needed by Remote + Input SSLContext, -- needed by Remote Input Env, -- needed by Service Error ValidationError, Error RemoteError, @@ -136,7 +143,7 @@ runFederator env = DiscoveryFailure ] . runInputConst env - . runInputSem (embed @IO (readIORef (view tls env))) + . runInputSem (embed @IO (readIORef (view sslContext env))) . runInputConst (view runSettings env) . interpretServiceHTTP . runDNSLookupWithResolver (view dnsResolver env) diff --git a/services/federator/src/Federator/Run.hs b/services/federator/src/Federator/Run.hs index 550113aff3..8251db5fce 100644 --- a/services/federator/src/Federator/Run.hs +++ b/services/federator/src/Federator/Run.hs @@ -64,7 +64,7 @@ run opts = do bracket (newEnv opts res) closeEnv $ \env -> do let externalServer = serveInward env portExternal internalServer = serveOutward env portInternal - withMonitor (env ^. applog) (env ^. tls) (optSettings opts) $ do + withMonitor (env ^. applog) (env ^. sslContext) (optSettings opts) $ do internalServerThread <- async internalServer externalServerThread <- async externalServer void $ waitAnyCancel [internalServerThread, externalServerThread] @@ -97,7 +97,7 @@ newEnv o _dnsResolver = do _service Galley = Opt.galley o _service Cargohold = Opt.cargohold o _httpManager <- initHttpManager - _tls <- mkTLSSettingsOrThrow _runSettings >>= newIORef + _sslContext <- mkTLSSettingsOrThrow _runSettings >>= newIORef pure Env {..} closeEnv :: Env -> IO () diff --git a/services/federator/src/Federator/Service.hs b/services/federator/src/Federator/Service.hs index 8a563aaa1e..3a779581af 100644 --- a/services/federator/src/Federator/Service.hs +++ b/services/federator/src/Federator/Service.hs @@ -42,7 +42,6 @@ import Network.HTTP.Client import qualified Network.HTTP.Types as HTTP import Polysemy import Polysemy.Input -import Polysemy.TinyLog import qualified Servant.Client.Core as Servant import Servant.Types.SourceT import Util.Options @@ -76,7 +75,9 @@ bodyReaderToStreamT action = fromStepT go -- FUTUREWORK: unify this interpretation with similar ones in Galley -- interpretServiceHTTP :: - Members '[Embed (Codensity IO), Input Env, TinyLog] r => + ( Member (Embed (Codensity IO)) r, + Member (Input Env) r + ) => Sem (ServiceStreaming ': r) a -> Sem r a interpretServiceHTTP = interpret $ \case diff --git a/services/federator/src/Federator/Validation.hs b/services/federator/src/Federator/Validation.hs index fc43fa1c94..27a5245e28 100644 --- a/services/federator/src/Federator/Validation.hs +++ b/services/federator/src/Federator/Validation.hs @@ -92,7 +92,9 @@ validationErrorStatus _ = HTTP.status403 -- | Validates an already-parsed domain against the allowList using the federator -- startup configuration. ensureCanFederateWith :: - Members '[Input RunSettings, Error ValidationError] r => + ( Member (Input RunSettings) r, + Member (Error ValidationError) r + ) => Domain -> Sem r () ensureCanFederateWith targetDomain = do @@ -137,13 +139,11 @@ parseDomainText domain = -- federator startup configuration and checks that it matches the names reported -- by the client certificate validateDomain :: - Members - '[ Input RunSettings, - Error ValidationError, - Error DiscoveryFailure, - DiscoverFederator - ] - r => + ( Member (Input RunSettings) r, + Member (Error ValidationError) r, + Member (Error DiscoveryFailure) r, + Member DiscoverFederator r + ) => Maybe ByteString -> ByteString -> Sem r Domain diff --git a/services/federator/test/integration/Main.hs b/services/federator/test/integration/Main.hs index b0e144f9ac..d8b4561577 100644 --- a/services/federator/test/integration/Main.hs +++ b/services/federator/test/integration/Main.hs @@ -22,6 +22,7 @@ where import Data.String.Conversions import Imports +import OpenSSL (withOpenSSL) import System.Environment (withArgs) import qualified Test.Federator.IngressSpec import qualified Test.Federator.InwardSpec @@ -29,7 +30,7 @@ import Test.Federator.Util (TestEnv, mkEnvFromOptions) import Test.Hspec main :: IO () -main = do +main = withOpenSSL $ do (wireArgs, hspecArgs) <- partitionArgs <$> getArgs env <- withArgs wireArgs mkEnvFromOptions -- withArgs hspecArgs . hspec $ do diff --git a/services/federator/test/integration/Test/Federator/IngressSpec.hs b/services/federator/test/integration/Test/Federator/IngressSpec.hs index c580be531c..806f57e53e 100644 --- a/services/federator/test/integration/Test/Federator/IngressSpec.hs +++ b/services/federator/test/integration/Test/Federator/IngressSpec.hs @@ -18,6 +18,7 @@ module Test.Federator.IngressSpec where import Control.Lens (view) +import Control.Monad.Catch (throwM) import Control.Monad.Codensity import qualified Data.Aeson as Aeson import Data.Binary.Builder @@ -26,12 +27,13 @@ import Data.Handle import Data.LegalHold (UserLegalHoldStatus (UserLegalHoldNoConsent)) import Data.String.Conversions (cs) import qualified Data.Text.Encoding as Text -import qualified Data.X509 as X509 import Federator.Discovery -import Federator.Env +import Federator.Monitor (FederationSetupError) +import Federator.Monitor.Internal (mkSSLContextWithoutCert) import Federator.Remote import Imports import qualified Network.HTTP.Types as HTTP +import OpenSSL.Session (SSLContext) import Polysemy import Polysemy.Embed import Polysemy.Error @@ -59,17 +61,18 @@ spec env = do _ <- putHandle brig (userId user) hdl let expectedProfile = (publicProfile user UserLegalHoldNoConsent) {profileHandle = Just (Handle hdl)} - resp <- - runTestSem - . assertNoError @RemoteError - $ inwardBrigCallViaIngress - "get-user-by-handle" - (Aeson.fromEncoding (Aeson.toEncoding hdl)) - liftIO $ do - bdy <- streamingResponseStrictBody resp - let actualProfile = Aeson.decode (toLazyByteString bdy) - responseStatusCode resp `shouldBe` HTTP.status200 - actualProfile `shouldBe` Just expectedProfile + runTestSem $ do + resp <- + liftToCodensity + . assertNoError @RemoteError + $ inwardBrigCallViaIngress + "get-user-by-handle" + (Aeson.fromEncoding (Aeson.toEncoding hdl)) + embed . lift @Codensity $ do + bdy <- streamingResponseStrictBody resp + let actualProfile = Aeson.decode (toLazyByteString bdy) + responseStatusCode resp `shouldBe` HTTP.status200 + actualProfile `shouldBe` Just expectedProfile -- @SF.Federation @TSFI.RESTfulAPI @S2 @S3 @S7 -- @@ -87,34 +90,37 @@ spec env = do hdl <- randomHandle _ <- putHandle brig (userId user) hdl - -- Remove client certificate from settings - tlsSettings0 <- view teTLSSettings - let tlsSettings = - tlsSettings0 - { _creds = case _creds tlsSettings0 of - (_, privkey) -> (X509.CertificateChain [], privkey) - } - r <- - runTestSem - . runError @RemoteError - $ inwardBrigCallViaIngressWithSettings - tlsSettings - "get-user-by-handle" - (Aeson.fromEncoding (Aeson.toEncoding hdl)) - liftIO $ case r of - Right _ -> expectationFailure "Expected client certificate error, got response" - Left (RemoteError _ _) -> - expectationFailure "Expected client certificate error, got remote error" - Left (RemoteErrorResponse _ status _) -> status `shouldBe` HTTP.status400 + settings <- view teSettings + sslCtxWithoutCert <- + either (throwM @_ @FederationSetupError) pure + <=< runM + . runEmbedded (liftIO @(TestFederator IO)) + . runError + $ mkSSLContextWithoutCert settings + runTestSem $ do + r <- + runError @RemoteError $ + inwardBrigCallViaIngressWithSettings + sslCtxWithoutCert + "get-user-by-handle" + (Aeson.fromEncoding (Aeson.toEncoding hdl)) + liftToCodensity . embed $ case r of + Right _ -> expectationFailure "Expected client certificate error, got response" + Left (RemoteError _ _) -> + expectationFailure "Expected client certificate error, got remote error" + Left (RemoteErrorResponse _ status _) -> status `shouldBe` HTTP.status400 -- FUTUREWORK: ORMOLU_DISABLE -- @END -- ORMOLU_ENABLE -runTestSem :: Sem '[Input TestEnv, Embed IO] a -> TestFederator IO a +liftToCodensity :: Member (Embed (Codensity IO)) r => Sem (Embed IO ': r) a -> Sem r a +liftToCodensity = runEmbedded @IO @(Codensity IO) lift + +runTestSem :: Sem '[Input TestEnv, Embed (Codensity IO)] a -> TestFederator IO a runTestSem action = do e <- ask - liftIO . runM . runInputConst e $ action + liftIO . lowerCodensity . runM . runInputConst e $ action discoverConst :: SrvTarget -> Sem (DiscoverFederator ': r) a -> Sem r a discoverConst target = interpret $ \case @@ -122,29 +128,29 @@ discoverConst target = interpret $ \case DiscoverAllFederators _ -> pure (Right (pure target)) inwardBrigCallViaIngress :: - Members [Input TestEnv, Embed IO, Error RemoteError] r => + Members [Input TestEnv, Embed (Codensity IO), Error RemoteError] r => Text -> Builder -> Sem r StreamingResponse inwardBrigCallViaIngress path payload = do - tlsSettings <- inputs (view teTLSSettings) - inwardBrigCallViaIngressWithSettings tlsSettings path payload + sslCtx <- inputs (view teSSLContext) + inwardBrigCallViaIngressWithSettings sslCtx path payload inwardBrigCallViaIngressWithSettings :: - Members [Input TestEnv, Embed IO, Error RemoteError] r => - TLSSettings -> + Members [Input TestEnv, Embed (Codensity IO), Error RemoteError] r => + SSLContext -> Text -> Builder -> Sem r StreamingResponse -inwardBrigCallViaIngressWithSettings tlsSettings requestPath payload = +inwardBrigCallViaIngressWithSettings sslCtx requestPath payload = do Endpoint ingressHost ingressPort <- cfgNginxIngress . view teTstOpts <$> input originDomain <- cfgOriginDomain . view teTstOpts <$> input let target = SrvTarget (cs ingressHost) ingressPort headers = [(originDomainHeaderName, Text.encodeUtf8 originDomain)] - runInputConst tlsSettings + liftToCodensity + . runInputConst sslCtx . assertNoError @DiscoveryFailure . discoverConst target - . runEmbedded @(Codensity IO) @IO lowerCodensity . interpretRemote $ discoverAndCall (Domain "example.com") Brig requestPath headers payload diff --git a/services/federator/test/integration/Test/Federator/InwardSpec.hs b/services/federator/test/integration/Test/Federator/InwardSpec.hs index 2d6c692bfc..f742a84522 100644 --- a/services/federator/test/integration/Test/Federator/InwardSpec.hs +++ b/services/federator/test/integration/Test/Federator/InwardSpec.hs @@ -1,3 +1,6 @@ +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH diff --git a/services/federator/test/integration/Test/Federator/Util.hs b/services/federator/test/integration/Test/Federator/Util.hs index fef77bef6d..e520abadaa 100644 --- a/services/federator/test/integration/Test/Federator/Util.hs +++ b/services/federator/test/integration/Test/Federator/Util.hs @@ -2,6 +2,8 @@ {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- @@ -41,12 +43,12 @@ import qualified Data.Text as Text import qualified Data.UUID as UUID import qualified Data.UUID.V4 as UUID import qualified Data.Yaml as Yaml -import Federator.Env import Federator.Options import Federator.Run import Imports import qualified Network.Connection import Network.HTTP.Client.TLS +import OpenSSL.Session (SSLContext) import qualified Options.Applicative as OPA import Polysemy import Polysemy.Error @@ -89,13 +91,15 @@ runTestFederator env = flip runReaderT env . unwrapTestFederator -- | See 'mkEnv' about what's in here. data TestEnv = TestEnv { _teMgr :: Manager, - _teTLSSettings :: TLSSettings, + _teSSLContext :: SSLContext, _teBrig :: BrigReq, _teCargohold :: CargoholdReq, -- | federator config _teOpts :: Opts, -- | integration test config - _teTstOpts :: IntegrationConfig + _teTstOpts :: IntegrationConfig, + -- | Settings passed to the federator + _teSettings :: RunSettings } type Select = TestEnv -> (Request -> Request) @@ -151,7 +155,9 @@ mkEnv _teTstOpts _teOpts = do _teMgr :: Manager <- newManager managerSettings let _teBrig = endpointToReq (cfgBrig _teTstOpts) _teCargohold = endpointToReq (cfgCargohold _teTstOpts) - _teTLSSettings <- mkTLSSettingsOrThrow (optSettings _teOpts) + -- _teTLSSettings <- mkTLSSettingsOrThrow (optSettings _teOpts) + _teSSLContext <- mkTLSSettingsOrThrow (optSettings _teOpts) + let _teSettings = optSettings _teOpts pure TestEnv {..} destroyEnv :: HasCallStack => TestEnv -> IO () @@ -251,7 +257,7 @@ postUserWithEmail hasPassword validateBody name email havePhone ssoid teamid bri post (brig . path "/i/users" . bdy) putHandle :: - (MonadIO m, MonadHttp m, HasCallStack) => + (MonadHttp m, HasCallStack) => BrigReq -> UserId -> Text -> diff --git a/services/federator/test/unit/Main.hs b/services/federator/test/unit/Main.hs index 160cbbe28e..fa936d392f 100644 --- a/services/federator/test/unit/Main.hs +++ b/services/federator/test/unit/Main.hs @@ -21,6 +21,7 @@ module Main where import Imports +import OpenSSL (withOpenSSL) import qualified Test.Federator.Client import qualified Test.Federator.ExternalServer import qualified Test.Federator.InternalServer @@ -33,15 +34,16 @@ import Test.Tasty main :: IO () main = - defaultMain $ - testGroup - "Tests" - [ Test.Federator.Options.tests, - Test.Federator.Validation.tests, - Test.Federator.Client.tests, - Test.Federator.InternalServer.tests, - Test.Federator.ExternalServer.tests, - Test.Federator.Monitor.tests, - Test.Federator.Remote.tests, - Test.Federator.Response.tests - ] + withOpenSSL $ + defaultMain $ + testGroup + "Tests" + [ Test.Federator.Options.tests, + Test.Federator.Validation.tests, + Test.Federator.Client.tests, + Test.Federator.InternalServer.tests, + Test.Federator.ExternalServer.tests, + Test.Federator.Monitor.tests, + Test.Federator.Remote.tests, + Test.Federator.Response.tests + ] diff --git a/services/federator/test/unit/Test/Federator/Client.hs b/services/federator/test/unit/Test/Federator/Client.hs index 0a99e08f15..2d4d47fd61 100644 --- a/services/federator/test/unit/Test/Federator/Client.hs +++ b/services/federator/test/unit/Test/Federator/Client.hs @@ -47,11 +47,10 @@ import Test.Tasty.HUnit import Util.Options import Wire.API.Federation.API import Wire.API.Federation.Client -import Wire.API.Federation.Component import Wire.API.Federation.Error import Wire.API.User (UserProfile) -instance CallsFed comp name +instance AddAnnotation loc comp name x targetDomain :: Domain targetDomain = Domain "target.example.com" @@ -86,7 +85,6 @@ newtype ResponseFailure = ResponseFailure Wai.Error deriving (Show) withMockFederatorClient :: - KnownComponent c => [HTTP.Header] -> (FederatedRequest -> IO (MediaType, LByteString)) -> FederatorClient c a -> diff --git a/services/federator/test/unit/Test/Federator/Monitor.hs b/services/federator/test/unit/Test/Federator/Monitor.hs index e448f62f2e..1f5740fa07 100644 --- a/services/federator/test/unit/Test/Federator/Monitor.hs +++ b/services/federator/test/unit/Test/Federator/Monitor.hs @@ -19,17 +19,15 @@ module Test.Federator.Monitor (tests) where import Control.Concurrent.Chan import Control.Exception (bracket) -import Control.Lens (view) import Control.Monad.Trans.Cont import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as B8 import qualified Data.Set as Set -import Data.X509 (CertificateChain (..)) -import Federator.Env (TLSSettings (..), creds) import Federator.Monitor import Federator.Monitor.Internal import Federator.Options import Imports +import OpenSSL.Session (SSLContext) import qualified Polysemy import qualified Polysemy.Error as Polysemy import System.FilePath @@ -116,7 +114,7 @@ withKubernetesSettings = do withSilentMonitor :: Chan (Maybe FederationSetupError) -> RunSettings -> - ContT r IO (IORef TLSSettings) + ContT r IO (IORef SSLContext) withSilentMonitor reloads settings = do tlsVar <- liftIO $ newIORef (error "TLSSettings not updated before being read") void . ContT $ @@ -136,7 +134,7 @@ testMonitorChangeUpdate = reloads <- newChan evalContT $ do settings <- withSettings - tlsVar <- withSilentMonitor reloads settings + _ <- withSilentMonitor reloads settings liftIO $ do appendFile (clientCertificate settings) "" result <- timeout timeoutMicroseconds (readChan reloads) @@ -146,11 +144,6 @@ testMonitorChangeUpdate = assertFailure ("unexpected exception " <> displayException err) _ -> pure () - tls <- readIORef tlsVar - case view creds tls of - (CertificateChain [], _) -> - assertFailure "expected non-empty certificate chain" - _ -> pure () testMonitorReplacedChangeUpdate :: TestTree testMonitorReplacedChangeUpdate = @@ -158,12 +151,18 @@ testMonitorReplacedChangeUpdate = reloads <- newChan evalContT $ do settings <- withSettings - tlsVar <- withSilentMonitor reloads settings + _ <- withSilentMonitor reloads settings liftIO $ do -- first replace file with a different one copyFile "test/resources/unit/localhost-dot.pem" (clientCertificate settings) + -- This will always fail because now the certificate doesn't match the + -- private key + _result0 <- timeout timeoutMicroseconds (readChan reloads) + copyFile + "test/resources/unit/localhost-dot-key.pem" + (clientPrivateKey settings) result1 <- timeout timeoutMicroseconds (readChan reloads) case result1 of Nothing -> @@ -184,11 +183,6 @@ testMonitorReplacedChangeUpdate = assertFailure ("unexpected exception " <> displayException err) _ -> pure () - tls <- readIORef tlsVar - case view creds tls of - (CertificateChain [], _) -> - assertFailure "expected non-empty certificate chain" - _ -> pure () testMonitorOverwriteUpdate :: TestTree testMonitorOverwriteUpdate = @@ -196,11 +190,18 @@ testMonitorOverwriteUpdate = reloads <- newChan evalContT $ do settings <- withSettings - tlsVar <- withSilentMonitor reloads settings + _ <- withSilentMonitor reloads settings liftIO $ do copyFile "test/resources/unit/localhost-dot.pem" (clientCertificate settings) + -- This will always fail because now the certificate doesn't match the + -- private key + _result0 <- timeout timeoutMicroseconds (readChan reloads) + + copyFile + "test/resources/unit/localhost-dot-key.pem" + (clientPrivateKey settings) result <- timeout timeoutMicroseconds (readChan reloads) case result of Nothing -> assertFailure "certificate not updated within the allotted time" @@ -208,11 +209,6 @@ testMonitorOverwriteUpdate = assertFailure ("unexpected exception " <> displayException err) _ -> pure () - tls <- readIORef tlsVar - case view creds tls of - (CertificateChain [], _) -> - assertFailure "expected non-empty certificate chain" - _ -> pure () testMonitorSymlinkUpdate :: TestTree testMonitorSymlinkUpdate = @@ -220,13 +216,22 @@ testMonitorSymlinkUpdate = reloads <- newChan evalContT $ do settings <- withSymlinkSettings - tlsVar <- withSilentMonitor reloads settings + _ <- withSilentMonitor reloads settings liftIO $ do - removeFile (clientCertificate settings) wd <- getWorkingDirectory + + removeFile (clientCertificate settings) createSymbolicLink (wd "test/resources/unit/localhost-dot.pem") (clientCertificate settings) + -- This will always fail because now the certificate doesn't match the + -- private key + _result0 <- timeout timeoutMicroseconds (readChan reloads) + + removeFile (clientPrivateKey settings) + createSymbolicLink + (wd "test/resources/unit/localhost-dot-key.pem") + (clientPrivateKey settings) result <- timeout timeoutMicroseconds (readChan reloads) case result of Nothing -> assertFailure "certificate not updated within the allotted time" @@ -234,11 +239,6 @@ testMonitorSymlinkUpdate = assertFailure ("unexpected exception " <> displayException err) _ -> pure () - tls <- readIORef tlsVar - case view creds tls of - (CertificateChain [], _) -> - assertFailure "expected non-empty certificate chain" - _ -> pure () testMonitorNestedUpdate :: TestTree testMonitorNestedUpdate = @@ -246,7 +246,7 @@ testMonitorNestedUpdate = reloads <- newChan evalContT $ do settings <- withNestedSettings 1 - tlsVar <- withSilentMonitor reloads settings + _ <- withSilentMonitor reloads settings liftIO $ do -- make a new directory with other credentials let parent = takeDirectory (clientCertificate settings) @@ -268,11 +268,6 @@ testMonitorNestedUpdate = assertFailure ("unexpected exception " <> displayException err) _ -> pure () - tls <- readIORef tlsVar - case view creds tls of - (CertificateChain [], _) -> - assertFailure "expected non-empty certificate chain" - _ -> pure () testMonitorDeepUpdate :: TestTree testMonitorDeepUpdate = @@ -280,7 +275,7 @@ testMonitorDeepUpdate = reloads <- newChan evalContT $ do settings <- withNestedSettings 2 - tlsVar <- withSilentMonitor reloads settings + _ <- withSilentMonitor reloads settings liftIO $ do -- make a new directory with other credentials let root = takeDirectory (takeDirectory (takeDirectory (clientCertificate settings))) @@ -311,19 +306,13 @@ testMonitorDeepUpdate = ("unexpected exception " <> displayException err) _ -> pure () - tls <- readIORef tlsVar - case view creds tls of - (CertificateChain [], _) -> - assertFailure "expected non-empty certificate chain" - _ -> pure () - testMonitorKubernetesUpdate :: TestTree testMonitorKubernetesUpdate = do testCase "monitor updates on a kubernetes secret mount" $ do reloads <- newChan evalContT $ do settings <- withKubernetesSettings - tlsVar <- withSilentMonitor reloads settings + _ <- withSilentMonitor reloads settings liftIO $ do let root = takeDirectory (clientCertificate settings) createDirectory (root "..foo2") @@ -340,12 +329,6 @@ testMonitorKubernetesUpdate = do ("unexpected exception " <> displayException err) _ -> pure () - tls <- readIORef tlsVar - case view creds tls of - (CertificateChain [], _) -> - assertFailure "expected non-empty certificate chain" - _ -> pure () - testMonitorError :: TestTree testMonitorError = testCase "monitor returns an error when settings cannot be updated" $ do diff --git a/services/federator/test/unit/Test/Federator/Options.hs b/services/federator/test/unit/Test/Federator/Options.hs index e121cd20f7..4c3b62e2b9 100644 --- a/services/federator/test/unit/Test/Federator/Options.hs +++ b/services/federator/test/unit/Test/Federator/Options.hs @@ -22,7 +22,6 @@ module Test.Federator.Options where import Control.Exception (try) -import Control.Lens import Data.Aeson (FromJSON) import qualified Data.Aeson as Aeson import qualified Data.ByteString.Char8 as B8 @@ -30,7 +29,6 @@ import Data.ByteString.Lazy (toStrict) import Data.Domain (Domain (..), mkDomain) import Data.String.Interpolate as QQ import qualified Data.Yaml as Yaml -import Federator.Env import Federator.Options import Federator.Run import Imports @@ -167,10 +165,8 @@ testSettings = assertFailure $ "expected invalid client certificate exception, got: " <> show e - Right tlsSettings -> - assertFailure $ - "expected failure for non-existing client certificate, got: " - <> show (tlsSettings ^. creds), + Right _ -> + assertFailure "expected failure for non-existing client certificate, got success", -- @SF.Federation @TSFI.Federate @S3 @S7 testCase "failToStartWithInvalidServerCredentials" $ do let settings = @@ -190,10 +186,8 @@ testSettings = assertFailure $ "expected invalid client certificate exception, got: " <> show e - Right tlsSettings -> - assertFailure $ - "expected failure for invalid client certificate, got: " - <> show (tlsSettings ^. creds), + Right _ -> + assertFailure "expected failure for invalid client certificate, got success", -- @END testCase "fail on invalid private key" $ do let settings = @@ -208,15 +202,13 @@ testSettings = clientCertificate: test/resources/unit/localhost.pem clientPrivateKey: test/resources/unit/invalid.pem|] try @FederationSetupError (mkTLSSettingsOrThrow settings) >>= \case - Left (InvalidClientCertificate _) -> pure () + Left (InvalidClientPrivateKey _) -> pure () Left e -> assertFailure $ "expected invalid client certificate exception, got: " <> show e - Right tlsSettings -> - assertFailure $ - "expected failure for invalid private key, got: " - <> show (tlsSettings ^. creds) + Right _ -> + assertFailure "expected failure for invalid private key, got success" ] assertParsesAs :: (HasCallStack, Eq a, FromJSON a, Show a) => a -> ByteString -> Assertion diff --git a/services/federator/test/unit/Test/Federator/Remote.hs b/services/federator/test/unit/Test/Federator/Remote.hs index 5a4b84ea75..6222516069 100644 --- a/services/federator/test/unit/Test/Federator/Remote.hs +++ b/services/federator/test/unit/Test/Federator/Remote.hs @@ -21,7 +21,6 @@ import Control.Exception (bracket) import Control.Monad.Codensity import Data.Domain import Federator.Discovery -import Federator.Env (TLSSettings) import Federator.Options import Federator.Remote import Federator.Run (mkTLSSettingsOrThrow) @@ -31,6 +30,7 @@ import Network.Wai import qualified Network.Wai.Handler.Warp as Warp import qualified Network.Wai.Handler.WarpTLS as Warp import Network.Wai.Utilities.MockServer (startMockServer) +import OpenSSL.Session (SSLContext) import Polysemy import Polysemy.Embed import Polysemy.Error @@ -62,30 +62,29 @@ settings = remoteCAStore = Just "test/resources/unit/unit-ca.pem" } -discoverLocalhost :: Int -> Sem (DiscoverFederator ': r) a -> Sem r a -discoverLocalhost port = interpret $ \case +discoverLocalhost :: ByteString -> Int -> Sem (DiscoverFederator ': r) a -> Sem r a +discoverLocalhost hostname port = interpret $ \case DiscoverAllFederators (Domain "localhost") -> - pure (Right (pure (SrvTarget "localhost" (fromIntegral port)))) + pure (Right (pure (SrvTarget hostname (fromIntegral port)))) DiscoverAllFederators _ -> pure (Left (DiscoveryFailureSrvNotAvailable "only localhost is supported")) DiscoverFederator (Domain "localhost") -> - pure (Right (SrvTarget "localhost" (fromIntegral port))) + pure (Right (SrvTarget hostname (fromIntegral port))) DiscoverFederator _ -> pure (Left (DiscoveryFailureSrvNotAvailable "only localhost is supported")) -assertNoRemoteError :: IO (Either RemoteError x) -> IO x -assertNoRemoteError action = - action >>= \case - Left err -> assertFailure $ "Unexpected remote error: " <> show err - Right x -> pure x +assertNoRemoteError :: Either RemoteError x -> IO x +assertNoRemoteError = \case + Left err -> assertFailure $ "Unexpected remote error: " <> show err + Right x -> pure x -mkTestCall :: TLSSettings -> Int -> IO (Either RemoteError ()) -mkTestCall tlsSettings port = +mkTestCall :: SSLContext -> ByteString -> Int -> Codensity IO (Either RemoteError ()) +mkTestCall sslCtx hostname port = runM + . runEmbedded @IO @(Codensity IO) liftIO . runError @RemoteError . void - . runInputConst tlsSettings - . discoverLocalhost port + . runInputConst sslCtx + . discoverLocalhost hostname port . assertNoError @DiscoveryFailure - . runEmbedded @(Codensity IO) @IO lowerCodensity . interpretRemote $ discoverAndCall (Domain "localhost") Brig "test" [] mempty @@ -105,17 +104,23 @@ testValidatesCertificateSuccess = [ testCase "when hostname=localhost and certificate-for=localhost" $ withMockServer certForLocalhost $ \port -> do tlsSettings <- mkTLSSettingsOrThrow settings - assertNoRemoteError (mkTestCall tlsSettings port), + runCodensity (mkTestCall tlsSettings "localhost" port) assertNoRemoteError, testCase "when hostname=localhost. and certificate-for=localhost" $ withMockServer certForLocalhost $ \port -> do tlsSettings <- mkTLSSettingsOrThrow settings - assertNoRemoteError (mkTestCall tlsSettings port), - -- This is a limitation of the TLS library, this test just exists to document that. + runCodensity (mkTestCall tlsSettings "localhost." port) assertNoRemoteError, + -- It is not very clear how to handle this, this test just exists to + -- document what we do. + -- Some discussion from author of curl: + -- https://lists.w3.org/Archives/Public/ietf-http-wg/2016JanMar/0430.html + -- + -- Perhaps it is also not possible to get a publically verifiable + -- certificate like this from any of the CAs: + -- https://github.com/certbot/certbot/issues/3718 testCase "when hostname=localhost. and certificate-for=localhost." $ withMockServer certForLocalhostDot $ \port -> do tlsSettings <- mkTLSSettingsOrThrow settings - eitherClient <- mkTestCall tlsSettings port - case eitherClient of + runCodensity (mkTestCall tlsSettings "localhost." port) $ \case Left _ -> pure () Right _ -> assertFailure "Congratulations, you fixed a known issue!" ] @@ -133,16 +138,14 @@ testValidatesCertificateWrongHostname = [ testCase "when the server's certificate doesn't match the hostname" $ withMockServer certForWrongDomain $ \port -> do tlsSettings <- mkTLSSettingsOrThrow settings - eitherClient <- mkTestCall tlsSettings port - case eitherClient of + runCodensity (mkTestCall tlsSettings "localhost" port) $ \case Left (RemoteError _ (FederatorClientTLSException _)) -> pure () Left x -> assertFailure $ "Expected TLS failure, got: " <> show x Right _ -> assertFailure "Expected connection with the server to fail", testCase "when the server's certificate does not have the server key usage flag" $ withMockServer certWithoutServerKeyUsage $ \port -> do tlsSettings <- mkTLSSettingsOrThrow settings - eitherClient <- mkTestCall tlsSettings port - case eitherClient of + runCodensity (mkTestCall tlsSettings "localhost" port) $ \case Left (RemoteError _ (FederatorClientTLSException _)) -> pure () Left x -> assertFailure $ "Expected TLS failure, got: " <> show x Right _ -> assertFailure "Expected connection with the server to fail" @@ -153,8 +156,7 @@ testValidatesCertificateWrongHostname = testConnectionError :: TestTree testConnectionError = testCase "connection failures are reported correctly" $ do tlsSettings <- mkTLSSettingsOrThrow settings - result <- mkTestCall tlsSettings 1 - case result of + runCodensity (mkTestCall tlsSettings "localhost" 1) $ \case Left (RemoteError _ (FederatorClientConnectionError _)) -> pure () Left x -> assertFailure $ "Expected connection error, got: " <> show x Right _ -> assertFailure "Expected connection with the server to fail" diff --git a/services/galley/default.nix b/services/galley/default.nix index 6b48dee10b..5f5ac943ce 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -18,6 +18,7 @@ , brig-types , bytestring , bytestring-conversion +, call-stack , case-insensitive , cassandra-util , cassava @@ -61,6 +62,7 @@ , metrics-core , metrics-wai , mtl +, network , optparse-applicative , pem , polysemy @@ -86,12 +88,13 @@ , servant-swagger , servant-swagger-ui , singletons +, singletons-th , sop-core , split , ssl-util , stm +, streaming-commons , string-conversions -, swagger , swagger2 , tagged , tasty @@ -105,6 +108,7 @@ , tinylog , tls , transformers +, transitive-anns , types-common , types-common-aws , types-common-journal @@ -206,12 +210,12 @@ mkDerivation { servant-swagger servant-swagger-ui singletons + singletons-th sop-core split ssl-util stm string-conversions - swagger swagger2 tagged text @@ -219,6 +223,7 @@ mkDerivation { tinylog tls transformers + transitive-anns types-common types-common-aws types-common-journal @@ -251,6 +256,7 @@ mkDerivation { brig-types bytestring bytestring-conversion + call-stack case-insensitive cassandra-util cassava @@ -288,6 +294,7 @@ mkDerivation { memory metrics-wai mtl + network optparse-applicative pem process @@ -307,8 +314,10 @@ mkDerivation { servant-server servant-swagger singletons + singletons-th sop-core ssl-util + streaming-commons string-conversions tagged tasty @@ -321,6 +330,7 @@ mkDerivation { tls transformers types-common + types-common-aws types-common-journal unix unliftio diff --git a/services/galley/src/Main.hs b/services/galley/exec/Main.hs similarity index 100% rename from services/galley/src/Main.hs rename to services/galley/exec/Main.hs diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index c7c42b7a87..ac1d204da6 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -54,6 +54,7 @@ library Galley.API.Public.Team Galley.API.Public.TeamConversation Galley.API.Public.TeamMember + Galley.API.Public.TeamNotification Galley.API.Push Galley.API.Query Galley.API.Teams @@ -137,7 +138,6 @@ library Galley.Types.ToUserRole Galley.Types.UserList Galley.Validation - Main other-modules: Paths_galley hs-source-dirs: src @@ -184,6 +184,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -fplugin=TransitiveAnns.Plugin -Wredundant-constraints build-depends: aeson >=2.0.1.0 @@ -257,12 +258,12 @@ library , servant-swagger , servant-swagger-ui , singletons + , singletons-th , sop-core , split >=0.2 , ssl-util >=0.1 , stm >=2.4 , string-conversions - , swagger >=0.1 , swagger2 , tagged , text >=0.11 @@ -270,6 +271,7 @@ library , tinylog >=0.10 , tls >=1.3.10 , transformers + , transitive-anns , types-common >=0.16 , types-common-aws , types-common-journal >=0.1 @@ -292,7 +294,7 @@ library default-language: Haskell2010 executable galley - main-is: src/Main.hs + main-is: exec/Main.hs other-modules: Paths_galley default-extensions: NoImplicitPrelude @@ -337,7 +339,7 @@ executable galley ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -with-rtsopts=-T -rtsopts + -threaded -with-rtsopts=-T -rtsopts -Wredundant-constraints build-depends: base @@ -383,6 +385,7 @@ executable galley-integration API.Teams.Feature API.Teams.LegalHold API.Teams.LegalHold.DisabledByDefault + API.Teams.LegalHold.Util API.Util API.Util.TeamFeature Main @@ -433,7 +436,7 @@ executable galley-integration ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded + -threaded -with-rtsopts=-N -Wredundant-constraints build-depends: aeson @@ -448,6 +451,7 @@ executable galley-integration , brig-types , bytestring , bytestring-conversion + , call-stack , case-insensitive , cassandra-util , cassava @@ -485,6 +489,7 @@ executable galley-integration , memory , metrics-wai , mtl + , network , optparse-applicative , pem , process @@ -504,8 +509,10 @@ executable galley-integration , servant-server , servant-swagger , singletons + , singletons-th , sop-core , ssl-util + , streaming-commons , string-conversions , tagged , tasty >=0.8 @@ -518,6 +525,7 @@ executable galley-integration , tls >=1.3.8 , transformers , types-common + , types-common-aws , types-common-journal , unix , unliftio @@ -593,6 +601,7 @@ executable galley-migrate-data ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: base @@ -692,6 +701,8 @@ executable galley-schema V75_MLSGroupInfo V76_ProposalOrigin V77_MLSGroupMemberClient + V78_TeamFeatureOutlookCalIntegration + V79_TeamFeatureMlsE2EId hs-source-dirs: schema/src default-extensions: @@ -737,6 +748,7 @@ executable galley-schema ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: base @@ -818,7 +830,7 @@ test-suite galley-tests ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -with-rtsopts=-N + -threaded -with-rtsopts=-N -Wredundant-constraints build-depends: base diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index a7759cad19..de77bd75c6 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -68,8 +68,18 @@ settings: conferenceCalling: defaults: status: enabled + outlookCalIntegration: + defaults: + status: disabled + lockStatus: locked + mlsE2EId: + defaults: + status: disabled + config: + verificationExpiration: null + lockStatus: unlocked -logLevel: Info +logLevel: Warn logNetStrings: false journal: # if set, journals; if not set, disables journaling diff --git a/services/galley/schema/src/Main.hs b/services/galley/schema/src/Main.hs index bb3642744b..5f493498f8 100644 --- a/services/galley/schema/src/Main.hs +++ b/services/galley/schema/src/Main.hs @@ -80,6 +80,8 @@ import qualified V74_ExposeInvitationsToTeamAdmin import qualified V75_MLSGroupInfo import qualified V76_ProposalOrigin import qualified V77_MLSGroupMemberClient +import qualified V78_TeamFeatureOutlookCalIntegration +import qualified V79_TeamFeatureMlsE2EId main :: IO () main = do @@ -145,7 +147,9 @@ main = do V74_ExposeInvitationsToTeamAdmin.migration, V75_MLSGroupInfo.migration, V76_ProposalOrigin.migration, - V77_MLSGroupMemberClient.migration + V77_MLSGroupMemberClient.migration, + V78_TeamFeatureOutlookCalIntegration.migration, + V79_TeamFeatureMlsE2EId.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/brig/schema/src/V15.hs b/services/galley/schema/src/V78_TeamFeatureOutlookCalIntegration.hs similarity index 74% rename from services/brig/schema/src/V15.hs rename to services/galley/schema/src/V78_TeamFeatureOutlookCalIntegration.hs index 485304b56e..cd52a49ec4 100644 --- a/services/brig/schema/src/V15.hs +++ b/services/galley/schema/src/V78_TeamFeatureOutlookCalIntegration.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V15 +module V78_TeamFeatureOutlookCalIntegration ( migration, ) where @@ -25,9 +25,10 @@ import Imports import Text.RawString.QQ migration :: Migration -migration = Migration 15 "Add user.tracking_id" $ do - void $ - schema' - [r| - alter columnfamily user add tracking_id uuid; - |] +migration = Migration 78 "Add feature config for team feature outlook calendar integration" $ do + schema' + [r| ALTER TABLE team_features ADD ( + outlook_cal_integration_status int, + outlook_cal_integration_lock_status int + ) + |] diff --git a/services/brig/schema/src/V33.hs b/services/galley/schema/src/V79_TeamFeatureMlsE2EId.hs similarity index 76% rename from services/brig/schema/src/V33.hs rename to services/galley/schema/src/V79_TeamFeatureMlsE2EId.hs index 4daf85ddab..9a544ab9b1 100644 --- a/services/brig/schema/src/V33.hs +++ b/services/galley/schema/src/V79_TeamFeatureMlsE2EId.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module V33 +module V79_TeamFeatureMlsE2EId ( migration, ) where @@ -25,15 +25,11 @@ import Imports import Text.RawString.QQ migration :: Migration -migration = Migration 33 "Add user.assets column" $ do +migration = Migration 79 "Add feature config for team feature MLS MlsE2EId" $ do schema' - [r| - create type if not exists asset - ( typ int - , key text - ); - |] - schema' - [r| - alter columnfamily user add assets list>; - |] + [r| ALTER TABLE team_features ADD ( + mls_e2eid_status int, + mls_e2eid_lock_status int, + mls_e2eid_ver_exp timestamp + ) + |] diff --git a/services/galley/src/Galley/API.hs b/services/galley/src/Galley/API.hs index fe28713db2..045458ee13 100644 --- a/services/galley/src/Galley/API.hs +++ b/services/galley/src/Galley/API.hs @@ -21,7 +21,6 @@ module Galley.API ) where -import qualified Data.Swagger.Build.Api as Doc import Galley.API.Internal import qualified Galley.API.Public as Public import Galley.API.Public.Servant @@ -29,8 +28,7 @@ import Galley.App (GalleyEffects) import Network.Wai.Routing (Routes) import Polysemy -sitemap :: Routes Doc.ApiBuilder (Sem GalleyEffects) () +sitemap :: Routes () (Sem GalleyEffects) () sitemap = do Public.sitemap - Public.apiDocs internalSitemap diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index 03a344533b..f81190f798 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -28,6 +28,8 @@ module Galley.API.Action updateLocalConversationUnchecked, NoChanges (..), LocalConversationUpdate (..), + notifyTypingIndicator, + pushTypingIndicatorEvents, -- * Utilities ensureConversationActionAllowed, @@ -43,6 +45,7 @@ import Control.Lens import Data.ByteString.Conversion (toByteString') import Data.Id import Data.Kind +import qualified Data.List as List import Data.List.NonEmpty (nonEmpty) import qualified Data.Map as Map import Data.Misc @@ -65,9 +68,11 @@ import qualified Galley.Effects.CodeStore as E import qualified Galley.Effects.ConversationStore as E import qualified Galley.Effects.FederatorAccess as E import qualified Galley.Effects.FireAndForget as E +import Galley.Effects.GundeckAccess import qualified Galley.Effects.MemberStore as E import Galley.Effects.ProposalStore import qualified Galley.Effects.TeamStore as E +import Galley.Intra.Push import Galley.Options import Galley.Types.Conversations.Members import Galley.Types.UserList @@ -83,10 +88,11 @@ import Wire.API.Conversation hiding (Conversation, Member) import Wire.API.Conversation.Action import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role +import Wire.API.Conversation.Typing import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Event.Conversation -import Wire.API.Federation.API (CallsFed, Component (Galley), fedClient) +import Wire.API.Federation.API (Component (Galley), fedClient) import Wire.API.Federation.API.Galley import Wire.API.Federation.Error import Wire.API.Team.LegalHold @@ -97,84 +103,92 @@ data NoChanges = NoChanges type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Constraint where HasConversationActionEffects 'ConversationJoinTag r = - Members - '[ BrigAccess, - Error FederationError, - Error InternalError, - ErrorS 'NotATeamMember, - ErrorS 'NotConnected, - ErrorS ('ActionDenied 'LeaveConversation), - ErrorS ('ActionDenied 'AddConversationMember), - ErrorS 'InvalidOperation, - ErrorS 'ConvAccessDenied, - ErrorS 'ConvNotFound, - ErrorS 'TooManyMembers, - ErrorS 'MissingLegalholdConsent, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input Opts, - Input UTCTime, - LegalHoldStore, - MemberStore, - ProposalStore, - TeamStore, - TinyLog, - ConversationStore, - Error NoChanges - ] - r + ( Member BrigAccess r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'NotConnected) r, + Member (ErrorS ('ActionDenied 'LeaveConversation)) r, + Member (ErrorS ('ActionDenied 'AddConversationMember)) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'ConvAccessDenied) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'TooManyMembers) r, + Member (ErrorS 'MissingLegalholdConsent) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member LegalHoldStore r, + Member MemberStore r, + Member ProposalStore r, + Member TeamStore r, + Member TinyLog r, + Member ConversationStore r, + Member (Error NoChanges) r + ) HasConversationActionEffects 'ConversationLeaveTag r = - ( Members - '[ MemberStore, - Error InternalError, - Error NoChanges, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input UTCTime, - Input Env, - ProposalStore, - TinyLog - ] - r + ( Member MemberStore r, + Member (Error InternalError) r, + Member (Error NoChanges) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input UTCTime) r, + Member (Input Env) r, + Member ProposalStore r, + Member TinyLog r ) HasConversationActionEffects 'ConversationRemoveMembersTag r = - (Members '[MemberStore, Error NoChanges] r) + ( Member MemberStore r, + Member (Error NoChanges) r + ) HasConversationActionEffects 'ConversationMemberUpdateTag r = - (Members '[MemberStore, ErrorS 'ConvMemberNotFound] r) + ( Member MemberStore r, + Member (ErrorS 'ConvMemberNotFound) r + ) HasConversationActionEffects 'ConversationDeleteTag r = - Members '[Error FederationError, ErrorS 'NotATeamMember, CodeStore, TeamStore, ConversationStore] r + ( Member (Error FederationError) r, + Member (ErrorS 'NotATeamMember) r, + Member CodeStore r, + Member TeamStore r, + Member ConversationStore r + ) HasConversationActionEffects 'ConversationRenameTag r = - Members '[Error InvalidInput, ConversationStore] r + ( Member (Error InvalidInput) r, + Member ConversationStore r + ) HasConversationActionEffects 'ConversationAccessDataTag r = - Members - '[ BotAccess, - BrigAccess, - CodeStore, - Error InternalError, - Error InvalidInput, - Error NoChanges, - ErrorS 'InvalidTargetAccess, - ErrorS ('ActionDenied 'RemoveConversationMember), - ExternalAccess, - FederatorAccess, - FireAndForget, - GundeckAccess, - Input Env, - MemberStore, - ProposalStore, - TeamStore, - TinyLog, - Input UTCTime, - ConversationStore - ] - r + ( Member BotAccess r, + Member BrigAccess r, + Member CodeStore r, + Member (Error InternalError) r, + Member (Error InvalidInput) r, + Member (Error NoChanges) r, + Member (ErrorS 'InvalidTargetAccess) r, + Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member FireAndForget r, + Member GundeckAccess r, + Member (Input Env) r, + Member MemberStore r, + Member ProposalStore r, + Member TeamStore r, + Member TinyLog r, + Member (Input UTCTime) r, + Member ConversationStore r + ) HasConversationActionEffects 'ConversationMessageTimerUpdateTag r = - Members '[ConversationStore, Error NoChanges] r + ( Member ConversationStore r, + Member (Error NoChanges) r + ) HasConversationActionEffects 'ConversationReceiptModeUpdateTag r = - Members '[ConversationStore, Error NoChanges] r + ( Member ConversationStore r, + Member (Error NoChanges) r + ) type family HasConversationActionGalleyErrors (tag :: ConversationActionTag) :: EffectRow where HasConversationActionGalleyErrors 'ConversationJoinTag = @@ -276,10 +290,7 @@ ensureAllowed tag loc action conv origUser = do -- and also returns the (possible modified) action that was performed performAction :: forall tag r. - ( HasConversationActionEffects tag r, - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation" + ( HasConversationActionEffects tag r ) => Sing tag -> Qualified UserId -> @@ -348,10 +359,7 @@ performAction tag origUser lconv action = do pure (bm, act) performConversationJoin :: - ( HasConversationActionEffects 'ConversationJoinTag r, - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation" + ( HasConversationActionEffects 'ConversationJoinTag r ) => Qualified UserId -> Local Conversation -> @@ -374,14 +382,12 @@ performConversationJoin qusr lconv (ConversationJoin invited role) = do conv = tUnqualified lconv checkLocals :: - Members - '[ BrigAccess, - ErrorS 'NotATeamMember, - ErrorS 'NotConnected, - ErrorS 'ConvAccessDenied, - TeamStore - ] - r => + ( Member BrigAccess r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'NotConnected) r, + Member (ErrorS 'ConvAccessDenied) r, + Member TeamStore r + ) => Local UserId -> Maybe TeamId -> [UserId] -> @@ -398,13 +404,11 @@ performConversationJoin qusr lconv (ConversationJoin invited role) = do ensureConnectedOrSameTeam lusr newUsers checkRemotes :: - Members - '[ BrigAccess, - Error FederationError, - ErrorS 'NotConnected, - FederatorAccess - ] - r => + ( Member BrigAccess r, + Member (Error FederationError) r, + Member (ErrorS 'NotConnected) r, + Member FederatorAccess r + ) => Local UserId -> [Remote UserId] -> Sem r () @@ -417,26 +421,20 @@ performConversationJoin qusr lconv (ConversationJoin invited role) = do ensureConnectedToRemotes lusr remotes checkLHPolicyConflictsLocal :: - Members - '[ ConversationStore, - Error InternalError, - ErrorS ('ActionDenied 'LeaveConversation), - ErrorS 'InvalidOperation, - ErrorS 'ConvNotFound, - ErrorS 'MissingLegalholdConsent, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input Opts, - Input UTCTime, - LegalHoldStore, - MemberStore, - ProposalStore, - TeamStore, - TinyLog - ] - r => + ( Member (Error InternalError) r, + Member (ErrorS 'MissingLegalholdConsent) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member LegalHoldStore r, + Member MemberStore r, + Member ProposalStore r, + Member TeamStore r, + Member TinyLog r + ) => [UserId] -> Sem r () checkLHPolicyConflictsLocal newUsers = do @@ -478,10 +476,7 @@ performConversationJoin qusr lconv (ConversationJoin invited role) = do checkLHPolicyConflictsRemote _remotes = pure () performConversationAccessData :: - ( HasConversationActionEffects 'ConversationAccessDataTag r, - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation" + ( HasConversationActionEffects 'ConversationAccessDataTag r ) => Qualified UserId -> Local Conversation -> @@ -524,7 +519,7 @@ performConversationAccessData qusr lconv action = do lcnv = fmap convId lconv conv = tUnqualified lconv - maybeRemoveBots :: Member BrigAccess r => BotsAndMembers -> Sem r BotsAndMembers + maybeRemoveBots :: BotsAndMembers -> Sem r BotsAndMembers maybeRemoveBots bm = if Set.member ServiceAccessRole (cupAccessRoles action) then pure bm @@ -566,24 +561,16 @@ data LocalConversationUpdate = LocalConversationUpdate updateLocalConversation :: forall tag r. - ( Members - '[ ConversationStore, - Error NoChanges, - ErrorS ('ActionDenied (ConversationActionPermission tag)), - ErrorS 'InvalidOperation, - ErrorS 'ConvNotFound, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input UTCTime - ] - r, + ( Member ConversationStore r, + Member (ErrorS ('ActionDenied (ConversationActionPermission tag))) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'ConvNotFound) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input UTCTime) r, HasConversationActionEffects tag r, - SingI tag, - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Galley "on-conversation-updated" + SingI tag ) => Local ConvId -> Qualified UserId -> @@ -620,10 +607,7 @@ updateLocalConversationUnchecked :: Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, - HasConversationActionEffects tag r, - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Galley "on-conversation-updated" + HasConversationActionEffects tag r ) => Local Conversation -> Qualified UserId -> @@ -660,11 +644,9 @@ ensureConversationActionAllowed :: forall tag mem x r. ( IsConvMember mem, HasConversationActionEffects tag r, - Members - '[ ErrorS ('ActionDenied (ConversationActionPermission tag)), - ErrorS 'InvalidOperation - ] - r + ( Member (ErrorS ('ActionDenied (ConversationActionPermission tag))) r, + Member (ErrorS 'InvalidOperation) r + ) ) => Sing tag -> Local x -> @@ -686,7 +668,9 @@ ensureConversationActionAllowed tag loc action conv self = do -- | Add users to a conversation without performing any checks. Return extra -- notification targets and the action performed. addMembersToLocalConversation :: - Members '[MemberStore, Error NoChanges] r => + ( Member MemberStore r, + Member (Error NoChanges) r + ) => Local ConvId -> UserList UserId -> RoleName -> @@ -699,9 +683,10 @@ addMembersToLocalConversation lcnv users role = do notifyConversationAction :: forall tag r. - ( Members '[FederatorAccess, ExternalAccess, GundeckAccess, Input UTCTime] r, - CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Galley "on-conversation-updated" + ( Member FederatorAccess r, + Member ExternalAccess r, + Member GundeckAccess r, + Member (Input UTCTime) r ) => Sing tag -> Qualified UserId -> @@ -760,14 +745,11 @@ notifyConversationAction tag quid notifyOrigDomain con lconv targets action = do -- | Notify all local members about a remote conversation update that originated -- from a local user notifyRemoteConversationAction :: - Members - '[ FederatorAccess, - ExternalAccess, - GundeckAccess, - MemberStore, - P.TinyLog - ] - r => + ( Member ExternalAccess r, + Member GundeckAccess r, + Member MemberStore r, + Member P.TinyLog r + ) => Local x -> Remote ConversationUpdate -> Maybe ConnId -> @@ -818,10 +800,7 @@ kickMember :: Member (Input UTCTime) r, Member (Input Env) r, Member MemberStore r, - Member TinyLog r, - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation" + Member TinyLog r ) => Qualified UserId -> Local Conversation -> @@ -843,3 +822,54 @@ kickMember qusr lconv targets victim = void . runError @NoChanges $ do lconv (targets <> extraTargets) (pure victim) + +notifyTypingIndicator :: + ( Member (Input UTCTime) r, + Member (Input (Local ())) r, + Member GundeckAccess r, + Member FederatorAccess r + ) => + Conversation -> + Qualified UserId -> + Maybe ConnId -> + TypingStatus -> + Sem r TypingDataUpdated +notifyTypingIndicator conv qusr mcon ts = do + let origDomain = qDomain qusr + now <- input + lconv <- qualifyLocal (Data.convId conv) + + pushTypingIndicatorEvents qusr now (fmap lmId (Data.convLocalMembers conv)) mcon (tUntagged lconv) ts + + let (remoteMemsOrig, remoteMemsOther) = List.partition ((origDomain ==) . tDomain . rmId) (Data.convRemoteMembers conv) + let tdu users = + TypingDataUpdated + { tudTime = now, + tudOrigUserId = qusr, + tudConvId = Data.convId conv, + tudUsersInConv = users, + tudTypingStatus = ts + } + + void $ E.runFederatedConcurrentlyEither (fmap rmId remoteMemsOther) $ \rmems -> do + fedClient @'Galley @"on-typing-indicator-updated" (tdu (tUnqualified rmems)) + + pure (tdu (fmap (tUnqualified . rmId) remoteMemsOrig)) + +pushTypingIndicatorEvents :: + (Member GundeckAccess r) => + Qualified UserId -> + UTCTime -> + [UserId] -> + Maybe ConnId -> + Qualified ConvId -> + TypingStatus -> + Sem r () +pushTypingIndicatorEvents qusr tEvent users mcon qcnv ts = do + let e = Event qcnv Nothing qusr tEvent (EdTyping ts) + for_ (newPushLocal ListComplete (qUnqualified qusr) (ConvEvent e) (userRecipient <$> users)) $ \p -> + push1 $ + p + & pushConn .~ mcon + & pushRoute .~ RouteDirect + & pushTransient .~ True diff --git a/services/galley/src/Galley/API/Clients.hs b/services/galley/src/Galley/API/Clients.hs index d671b33621..25cc308737 100644 --- a/services/galley/src/Galley/API/Clients.hs +++ b/services/galley/src/Galley/API/Clients.hs @@ -54,19 +54,25 @@ import qualified Polysemy.TinyLog as P import qualified System.Logger as Log import Wire.API.Conversation hiding (Member) import Wire.API.Federation.API +import Wire.API.Federation.API.Common (EmptyResponse) import Wire.API.Federation.API.Galley (ClientRemovedRequest (ClientRemovedRequest)) +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Routes.MultiTablePaging import Wire.Sem.Paging.Cassandra (CassandraPaging) getClientsH :: - Members '[BrigAccess, ClientStore] r => + ( Member BrigAccess r, + Member ClientStore r + ) => UserId -> Sem r Response getClientsH usr = do json <$> getClients usr getClients :: - Members '[BrigAccess, ClientStore] r => + ( Member BrigAccess r, + Member ClientStore r + ) => UserId -> Sem r [ClientId] getClients usr = do @@ -88,25 +94,21 @@ addClientH (usr ::: clt) = do rmClientH :: forall p1 r. ( p1 ~ CassandraPaging, - Members - '[ ClientStore, - ConversationStore, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input (Local ()), - Input UTCTime, - ListItems p1 ConvId, - ListItems p1 (Remote ConvId), - MemberStore, - Error InternalError, - ProposalStore, - P.TinyLog - ] - r, - CallsFed 'Galley "on-client-removed", - CallsFed 'Galley "on-mls-message-sent" + ( Member ClientStore r, + Member ConversationStore r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ListItems p1 ConvId) r, + Member (ListItems p1 (Remote ConvId)) r, + Member MemberStore r, + Member (Error InternalError) r, + Member ProposalStore r, + Member P.TinyLog r + ) ) => UserId ::: ClientId -> Sem r Response @@ -119,7 +121,9 @@ rmClientH (usr ::: cid) = do E.deleteClient usr cid pure empty where + rpc :: ClientRemovedRequest -> FederatorClient 'Galley EmptyResponse rpc = fedClient @'Galley @"on-client-removed" + goConvs :: Range 1 1000 Int32 -> ConvIdsPage -> Local UserId -> Sem r () goConvs range page lusr = do let (localConvs, remoteConvs) = partitionQualified lusr (mtpResults page) diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index 2995ca3f07..3c97d90909 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -71,7 +71,6 @@ import Wire.API.Conversation.Protocol import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Event.Conversation -import Wire.API.Federation.API import Wire.API.Federation.Error import Wire.API.Routes.Public.Galley.Conversation import Wire.API.Routes.Public.Util @@ -85,36 +84,34 @@ import Wire.API.Team.Permission hiding (self) -- | The public-facing endpoint for creating group conversations. createGroupConversation :: - ( Members - '[ BrigAccess, - ConversationStore, - MemberStore, - ErrorS 'ConvAccessDenied, - Error InternalError, - Error InvalidInput, - ErrorS 'NotATeamMember, - ErrorS OperationDenied, - ErrorS 'NotConnected, - ErrorS 'MLSNotEnabled, - ErrorS 'MLSNonEmptyMemberList, - ErrorS 'MissingLegalholdConsent, - FederatorAccess, - GundeckAccess, - Input Env, - Input Opts, - Input UTCTime, - LegalHoldStore, - TeamStore, - P.TinyLog - ] - r, - CallsFed 'Galley "on-conversation-created" + ( Member BrigAccess r, + Member ConversationStore r, + Member MemberStore r, + Member (ErrorS 'ConvAccessDenied) r, + Member (Error InternalError) r, + Member (Error InvalidInput) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS OperationDenied) r, + Member (ErrorS 'NotConnected) r, + Member (ErrorS 'MLSNotEnabled) r, + Member (ErrorS 'MLSNonEmptyMemberList) r, + Member (ErrorS 'MLSMissingSenderClient) r, + Member (ErrorS 'MissingLegalholdConsent) r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member LegalHoldStore r, + Member TeamStore r, + Member P.TinyLog r ) => Local UserId -> + Maybe ClientId -> ConnId -> NewConv -> Sem r ConversationResponse -createGroupConversation lusr conn newConv = do +createGroupConversation lusr mCreatorClient conn newConv = do (nc, fromConvSize -> allUsers) <- newRegularConversation lusr newConv let tinfo = newConvTeam newConv checkCreateConvPermissions lusr newConv tinfo allUsers @@ -129,15 +126,18 @@ createGroupConversation lusr conn newConv = do ProtocolProteusTag -> pure () lcnv <- traverse (const E.createConversationId) lusr + -- FUTUREWORK: Invoke the creating a conversation action only once + -- protocol-specific validation is successful. Otherwise we might write the + -- conversation to the database, and throw a validation error when the + -- conversation is already in the database. conv <- E.createConversation lcnv nc -- set creator client for MLS conversations - case (convProtocol conv, newConvCreatorClient newConv) of + case (convProtocol conv, mCreatorClient) of (ProtocolProteus, _) -> pure () (ProtocolMLS mlsMeta, Just c) -> E.addMLSClients (cnvmlsGroupId mlsMeta) (tUntagged lusr) (Set.singleton (c, nullKeyPackageRef)) - (ProtocolMLS _mlsMeta, Nothing) -> - throw (InvalidPayload "Missing creator_client field when creating an MLS conversation") + (ProtocolMLS _mlsMeta, Nothing) -> throwS @'MLSMissingSenderClient now <- input -- NOTE: We only send (conversation) events to members of the conversation @@ -145,7 +145,11 @@ createGroupConversation lusr conn newConv = do conversationCreated lusr conv ensureNoLegalholdConflicts :: - Members '[ErrorS 'MissingLegalholdConsent, Input Opts, LegalHoldStore, TeamStore] r => + ( Member (ErrorS 'MissingLegalholdConsent) r, + Member (Input Opts) r, + Member LegalHoldStore r, + Member TeamStore r + ) => UserList UserId -> Sem r () ensureNoLegalholdConflicts (UserList locals remotes) = do @@ -155,15 +159,13 @@ ensureNoLegalholdConflicts (UserList locals remotes) = do throwS @'MissingLegalholdConsent checkCreateConvPermissions :: - Members - '[ BrigAccess, - ErrorS 'ConvAccessDenied, - ErrorS 'NotATeamMember, - ErrorS OperationDenied, - ErrorS 'NotConnected, - TeamStore - ] - r => + ( Member BrigAccess r, + Member (ErrorS 'ConvAccessDenied) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS OperationDenied) r, + Member (ErrorS 'NotConnected) r, + Member TeamStore r + ) => Local UserId -> NewConv -> Maybe ConvTeamInfo -> @@ -200,7 +202,10 @@ checkCreateConvPermissions lusr newConv (Just tinfo) allUsers = do createProteusSelfConversation :: forall r. - Members '[ConversationStore, Error InternalError, P.TinyLog] r => + ( Member ConversationStore r, + Member (Error InternalError) r, + Member P.TinyLog r + ) => Local UserId -> Sem r ConversationResponse createProteusSelfConversation lusr = do @@ -221,30 +226,23 @@ createProteusSelfConversation lusr = do createOne2OneConversation :: forall r. - ( Members - '[ BrigAccess, - ConversationStore, - ErrorS 'ConvAccessDenied, - Error FederationError, - Error InternalError, - Error InvalidInput, - ErrorS 'ConvAccessDenied, - ErrorS 'NotATeamMember, - ErrorS 'NonBindingTeam, - ErrorS 'NoBindingTeamMembers, - ErrorS OperationDenied, - ErrorS 'TeamNotFound, - ErrorS 'InvalidOperation, - ErrorS 'NotConnected, - ErrorS 'MissingLegalholdConsent, - FederatorAccess, - GundeckAccess, - Input UTCTime, - TeamStore, - P.TinyLog - ] - r, - CallsFed 'Galley "on-conversation-created" + ( Member BrigAccess r, + Member ConversationStore r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member (Error InvalidInput) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'NonBindingTeam) r, + Member (ErrorS 'NoBindingTeamMembers) r, + Member (ErrorS OperationDenied) r, + Member (ErrorS 'TeamNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'NotConnected) r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input UTCTime) r, + Member TeamStore r, + Member P.TinyLog r ) => Local UserId -> ConnId -> @@ -290,17 +288,13 @@ createOne2OneConversation lusr zcon j = do Nothing -> throwS @'TeamNotFound createLegacyOne2OneConversationUnchecked :: - ( Members - '[ ConversationStore, - Error InternalError, - Error InvalidInput, - FederatorAccess, - GundeckAccess, - Input UTCTime, - P.TinyLog - ] - r, - CallsFed 'Galley "on-conversation-created" + ( Member ConversationStore r, + Member (Error InternalError) r, + Member (Error InvalidInput) r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input UTCTime) r, + Member P.TinyLog r ) => Local UserId -> ConnId -> @@ -331,18 +325,13 @@ createLegacyOne2OneConversationUnchecked self zcon name mtid other = do conversationCreated self c createOne2OneConversationUnchecked :: - ( Members - '[ ConversationStore, - Error FederationError, - Error InternalError, - ErrorS 'MissingLegalholdConsent, - FederatorAccess, - GundeckAccess, - Input UTCTime, - P.TinyLog - ] - r, - CallsFed 'Galley "on-conversation-created" + ( Member ConversationStore r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input UTCTime) r, + Member P.TinyLog r ) => Local UserId -> ConnId -> @@ -359,17 +348,12 @@ createOne2OneConversationUnchecked self zcon name mtid other = do create (one2OneConvId (tUntagged self) other) self zcon name mtid other createOne2OneConversationLocally :: - ( Members - '[ ConversationStore, - Error InternalError, - ErrorS 'MissingLegalholdConsent, - FederatorAccess, - GundeckAccess, - Input UTCTime, - P.TinyLog - ] - r, - CallsFed 'Galley "on-conversation-created" + ( Member ConversationStore r, + Member (Error InternalError) r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input UTCTime) r, + Member P.TinyLog r ) => Local ConvId -> Local UserId -> @@ -412,22 +396,17 @@ createOne2OneConversationRemotely _ _ _ _ _ _ = throw FederationNotImplemented createConnectConversation :: - ( Members - '[ ConversationStore, - ErrorS 'ConvNotFound, - Error FederationError, - Error InternalError, - Error InvalidInput, - ErrorS 'InvalidOperation, - ErrorS 'NotConnected, - FederatorAccess, - GundeckAccess, - Input UTCTime, - MemberStore, - P.TinyLog - ] - r, - CallsFed 'Galley "on-conversation-created" + ( Member ConversationStore r, + Member (ErrorS 'ConvNotFound) r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member (Error InvalidInput) r, + Member (ErrorS 'InvalidOperation) r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input UTCTime) r, + Member MemberStore r, + Member P.TinyLog r ) => Local UserId -> Maybe ConnId -> @@ -510,7 +489,10 @@ createConnectConversation lusr conn j = do -- | Return a 'NewConversation' record suitable for creating a group conversation. newRegularConversation :: - Members '[ErrorS 'MLSNonEmptyMemberList, Error InvalidInput, Input Opts] r => + ( Member (ErrorS 'MLSNonEmptyMemberList) r, + Member (Error InvalidInput) r, + Member (Input Opts) r + ) => Local UserId -> NewConv -> Sem r (NewConversation, ConvSizeChecked UserList UserId) @@ -544,15 +526,20 @@ newRegularConversation lusr newConv = do -- Helpers conversationCreated :: - Members '[Error InternalError, P.TinyLog] r => + ( Member (Error InternalError) r, + Member P.TinyLog r + ) => Local UserId -> Data.Conversation -> Sem r ConversationResponse conversationCreated lusr cnv = Created <$> conversationView lusr cnv notifyCreatedConversation :: - ( Members '[Error InternalError, FederatorAccess, GundeckAccess, Input UTCTime, P.TinyLog] r, - CallsFed 'Galley "on-conversation-created" + ( Member (Error InternalError) r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input UTCTime) r, + Member P.TinyLog r ) => Maybe UTCTime -> Local UserId -> diff --git a/services/galley/src/Galley/API/CustomBackend.hs b/services/galley/src/Galley/API/CustomBackend.hs index 596102ab71..89c0053309 100644 --- a/services/galley/src/Galley/API/CustomBackend.hs +++ b/services/galley/src/Galley/API/CustomBackend.hs @@ -41,7 +41,9 @@ import Wire.API.Error.Galley -- PUBLIC --------------------------------------------------------------------- getCustomBackendByDomain :: - Members '[CustomBackendStore, ErrorS 'CustomBackendNotFound] r => + ( Member CustomBackendStore r, + Member (ErrorS 'CustomBackendNotFound) r + ) => Domain -> Sem r Public.CustomBackend getCustomBackendByDomain domain = @@ -52,7 +54,9 @@ getCustomBackendByDomain domain = -- INTERNAL ------------------------------------------------------------------- internalPutCustomBackendByDomainH :: - Members '[CustomBackendStore, WaiRoutes] r => + ( Member CustomBackendStore r, + Member WaiRoutes r + ) => Domain ::: JsonRequest CustomBackend -> Sem r Response internalPutCustomBackendByDomainH (domain ::: req) = do diff --git a/services/galley/src/Galley/API/Error.hs b/services/galley/src/Galley/API/Error.hs index 0beb260031..9f145a482b 100644 --- a/services/galley/src/Galley/API/Error.hs +++ b/services/galley/src/Galley/API/Error.hs @@ -62,14 +62,12 @@ data InvalidInput | InvalidRange LText | InvalidUUID4 | InvalidPayload LText - | InvalidTeamNotificationId instance APIError InvalidInput where toWai CustomRolesNotSupported = badRequest "Custom roles not supported" toWai (InvalidRange t) = invalidRange t toWai InvalidUUID4 = invalidUUID4 toWai (InvalidPayload t) = invalidPayload t - toWai InvalidTeamNotificationId = invalidTeamNotificationId ---------------------------------------------------------------------------- -- Other errors diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 0572690474..7850852ab2 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -107,35 +107,31 @@ federationSitemap = :<|> Named @"on-new-remote-conversation" onNewRemoteConversation :<|> Named @"get-conversations" getConversations :<|> Named @"on-conversation-updated" onConversationUpdated - :<|> Named @"leave-conversation" (callsFed leaveConversation) + :<|> Named @"leave-conversation" (callsFed (exposeAnnotations leaveConversation)) :<|> Named @"on-message-sent" onMessageSent - :<|> Named @"send-message" (callsFed sendMessage) - :<|> Named @"on-user-deleted-conversations" (callsFed onUserDeleted) - :<|> Named @"update-conversation" (callsFed updateConversation) + :<|> Named @"send-message" (callsFed (exposeAnnotations sendMessage)) + :<|> Named @"on-user-deleted-conversations" (callsFed (exposeAnnotations onUserDeleted)) + :<|> Named @"update-conversation" (callsFed (exposeAnnotations updateConversation)) :<|> Named @"mls-welcome" mlsSendWelcome :<|> Named @"on-mls-message-sent" onMLSMessageSent - :<|> Named @"send-mls-message" (callsFed sendMLSMessage) - :<|> Named @"send-mls-commit-bundle" (callsFed sendMLSCommitBundle) + :<|> Named @"send-mls-message" (callsFed (exposeAnnotations sendMLSMessage)) + :<|> Named @"send-mls-commit-bundle" (callsFed (exposeAnnotations sendMLSCommitBundle)) :<|> Named @"query-group-info" queryGroupInfo - :<|> Named @"on-client-removed" (callsFed onClientRemoved) + :<|> Named @"on-client-removed" (callsFed (exposeAnnotations onClientRemoved)) + :<|> Named @"update-typing-indicator" (callsFed (exposeAnnotations updateTypingIndicator)) :<|> Named @"on-typing-indicator-updated" onTypingIndicatorUpdated onClientRemoved :: - ( Members - '[ ConversationStore, - Error InternalError, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input (Local ()), - Input UTCTime, - MemberStore, - ProposalStore, - TinyLog - ] - r, - CallsFed 'Galley "on-mls-message-sent" + ( Member ConversationStore r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member MemberStore r, + Member ProposalStore r, + Member TinyLog r ) => Domain -> ClientRemovedRequest -> @@ -151,16 +147,13 @@ onClientRemoved domain req = do pure EmptyResponse onConversationCreated :: - Members - '[ BrigAccess, - ConversationStore, - GundeckAccess, - ExternalAccess, - Input (Local ()), - MemberStore, - P.TinyLog - ] - r => + ( Member BrigAccess r, + Member GundeckAccess r, + Member ExternalAccess r, + Member (Input (Local ())) r, + Member MemberStore r, + Member P.TinyLog r + ) => Domain -> F.ConversationCreated ConvId -> Sem r () @@ -210,7 +203,9 @@ onNewRemoteConversation domain nrc = do pure EmptyResponse getConversations :: - Members '[ConversationStore, Input (Local ())] r => + ( Member ConversationStore r, + Member (Input (Local ())) r + ) => Domain -> F.GetConversationsRequest -> Sem r F.GetConversationsResponse @@ -227,15 +222,13 @@ getLocalUsers localDomain = map qUnqualified . filter ((== localDomain) . qDomai -- | Update the local database with information on conversation members joining -- or leaving. Finally, push out notifications to local users. onConversationUpdated :: - Members - '[ BrigAccess, - GundeckAccess, - ExternalAccess, - Input (Local ()), - MemberStore, - P.TinyLog - ] - r => + ( Member BrigAccess r, + Member GundeckAccess r, + Member ExternalAccess r, + Member (Input (Local ())) r, + Member MemberStore r, + Member P.TinyLog r + ) => Domain -> F.ConversationUpdate -> Sem r () @@ -305,7 +298,10 @@ onConversationUpdated requestingDomain cu = do pushConversationEvent Nothing event (qualifyAs loc targets) [] addLocalUsersToRemoteConv :: - Members '[BrigAccess, MemberStore, P.TinyLog] r => + ( Member BrigAccess r, + Member MemberStore r, + Member P.TinyLog r + ) => Remote ConvId -> Qualified UserId -> [UserId] -> @@ -332,24 +328,17 @@ addLocalUsersToRemoteConv remoteConvId qAdder localUsers = do -- as of now this will not generate the necessary events on the leaver's domain leaveConversation :: - ( Members - '[ ConversationStore, - Error InternalError, - Error InvalidInput, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input (Local ()), - Input UTCTime, - MemberStore, - ProposalStore, - TinyLog - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member ConversationStore r, + Member (Error InternalError) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member MemberStore r, + Member ProposalStore r, + Member TinyLog r ) => Domain -> F.LeaveConversationRequest -> @@ -397,7 +386,12 @@ leaveConversation requestingDomain lc = do -- FUTUREWORK: error handling for missing / mismatched clients -- FUTUREWORK: support bots onMessageSent :: - Members '[GundeckAccess, ExternalAccess, MemberStore, Input (Local ()), P.TinyLog] r => + ( Member GundeckAccess r, + Member ExternalAccess r, + Member MemberStore r, + Member (Input (Local ())) r, + Member P.TinyLog r + ) => Domain -> F.RemoteMessage ConvId -> Sem r () @@ -439,24 +433,18 @@ onMessageSent domain rmUnqualified = do (Map.filterWithKey (\(uid, _) _ -> Set.member uid members) msgs) sendMessage :: - ( Members - '[ BrigAccess, - ClientStore, - ConversationStore, - Error InvalidInput, - FederatorAccess, - GundeckAccess, - Input (Local ()), - Input Opts, - Input UTCTime, - ExternalAccess, - MemberStore, - TeamStore, - P.TinyLog - ] - r, - CallsFed 'Galley "on-message-sent", - CallsFed 'Brig "get-user-clients" + ( Member BrigAccess r, + Member ClientStore r, + Member ConversationStore r, + Member (Error InvalidInput) r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input (Local ())) r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member ExternalAccess r, + Member TeamStore r, + Member P.TinyLog r ) => Domain -> F.ProteusMessageSendRequest -> @@ -470,24 +458,17 @@ sendMessage originDomain msr = do throwErr = throw . InvalidPayload . LT.pack onUserDeleted :: - ( Members - '[ ConversationStore, - FederatorAccess, - FireAndForget, - ExternalAccess, - GundeckAccess, - Error InternalError, - Input (Local ()), - Input UTCTime, - Input Env, - MemberStore, - ProposalStore, - TinyLog - ] - r, - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation" + ( Member ConversationStore r, + Member FederatorAccess r, + Member FireAndForget r, + Member ExternalAccess r, + Member GundeckAccess r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (Input Env) r, + Member MemberStore r, + Member ProposalStore r, + Member TinyLog r ) => Domain -> F.UserDeletedConversationsNotification -> @@ -529,32 +510,26 @@ onUserDeleted origDomain udcn = do updateConversation :: forall r. - ( Members - '[ BrigAccess, - CodeStore, - BotAccess, - FireAndForget, - Error FederationError, - Error InvalidInput, - ExternalAccess, - FederatorAccess, - Error InternalError, - GundeckAccess, - Input Env, - Input Opts, - Input UTCTime, - LegalHoldStore, - MemberStore, - ProposalStore, - TeamStore, - TinyLog, - ConversationStore, - Input (Local ()) - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member BrigAccess r, + Member CodeStore r, + Member BotAccess r, + Member FireAndForget r, + Member (Error FederationError) r, + Member (Error InvalidInput) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member (Error InternalError) r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member LegalHoldStore r, + Member MemberStore r, + Member ProposalStore r, + Member TeamStore r, + Member TinyLog r, + Member ConversationStore r, + Member (Input (Local ())) r ) => Domain -> F.ConversationUpdateRequest -> @@ -617,32 +592,23 @@ updateConversation origDomain updateRequest = do toResponse (Right (Right update)) = F.ConversationUpdateResponseUpdate update sendMLSCommitBundle :: - ( Members - [ BrigAccess, - ConversationStore, - ExternalAccess, - Error FederationError, - Error InternalError, - FederatorAccess, - GundeckAccess, - Input (Local ()), - Input Env, - Input Opts, - Input UTCTime, - LegalHoldStore, - MemberStore, - Resource, - TeamStore, - P.TinyLog, - ProposalStore - ] - r, - CallsFed 'Galley "mls-welcome", - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Galley "send-mls-commit-bundle", - CallsFed 'Brig "get-mls-clients" + ( Member BrigAccess r, + Member ConversationStore r, + Member ExternalAccess r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input (Local ())) r, + Member (Input Env) r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member LegalHoldStore r, + Member MemberStore r, + Member Resource r, + Member TeamStore r, + Member P.TinyLog r, + Member ProposalStore r ) => Domain -> F.MLSMessageSendRequest -> @@ -667,31 +633,23 @@ sendMLSCommitBundle remoteDomain msr = <$> postMLSCommitBundle loc (tUntagged sender) Nothing qcnv Nothing bundle sendMLSMessage :: - ( Members - [ BrigAccess, - ConversationStore, - ExternalAccess, - Error FederationError, - Error InternalError, - FederatorAccess, - GundeckAccess, - Input (Local ()), - Input Env, - Input Opts, - Input UTCTime, - LegalHoldStore, - MemberStore, - Resource, - TeamStore, - P.TinyLog, - ProposalStore - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Galley "send-mls-message", - CallsFed 'Brig "get-mls-clients" + ( Member BrigAccess r, + Member ConversationStore r, + Member ExternalAccess r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input (Local ())) r, + Member (Input Env) r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member LegalHoldStore r, + Member MemberStore r, + Member Resource r, + Member TeamStore r, + Member P.TinyLog r, + Member ProposalStore r ) => Domain -> F.MLSMessageSendRequest -> @@ -740,15 +698,13 @@ instance Right res -> pure res mlsSendWelcome :: - Members - '[ BrigAccess, - Error InternalError, - GundeckAccess, - Input Env, - Input (Local ()), - Input UTCTime - ] - r => + ( Member BrigAccess r, + Member (Error InternalError) r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input (Local ())) r, + Member (Input UTCTime) r + ) => Domain -> F.MLSWelcomeRequest -> Sem r F.MLSWelcomeResponse @@ -774,15 +730,13 @@ mlsSendWelcome _origDomain (fromBase64ByteString . F.unMLSWelcomeRequest -> rawW sendLocalWelcomes Nothing now rawWelcome lrcpts onMLSMessageSent :: - Members - '[ ExternalAccess, - GundeckAccess, - Input (Local ()), - Input Env, - MemberStore, - P.TinyLog - ] - r => + ( Member ExternalAccess r, + Member GundeckAccess r, + Member (Input (Local ())) r, + Member (Input Env) r, + Member MemberStore r, + Member P.TinyLog r + ) => Domain -> F.RemoteMLSMessage -> Sem r F.RemoteMLSMessageResponse @@ -818,12 +772,9 @@ onMLSMessageSent domain rmm = foldMap mkPush recipients queryGroupInfo :: - ( Members - '[ ConversationStore, - Input (Local ()), - Input Env - ] - r, + ( Member ConversationStore r, + Member (Input (Local ())) r, + Member (Input Env) r, Member MemberStore r ) => Domain -> @@ -843,24 +794,35 @@ queryGroupInfo origDomain req = . unOpaquePublicGroupState $ state -onTypingIndicatorUpdated :: - ( Members - '[ ConversationStore, - MemberStore, - GundeckAccess, - Input UTCTime, - Input (Local ()) - ] - r +updateTypingIndicator :: + ( Member GundeckAccess r, + Member FederatorAccess r, + Member ConversationStore r, + Member (Input UTCTime) r, + Member (Input (Local ())) r ) => Domain -> - TypingDataUpdateRequest -> - Sem r EmptyResponse -onTypingIndicatorUpdated origDomain TypingDataUpdateRequest {..} = do + F.TypingDataUpdateRequest -> + Sem r F.TypingDataUpdateResponse +updateTypingIndicator origDomain TypingDataUpdateRequest {..} = do let qusr = Qualified tdurUserId origDomain lcnv <- qualifyLocal tdurConvId - -- FUTUREWORK: Consider if we should throw exceptions from this kind of function - void $ - runError @(Tagged 'ConvNotFound ()) $ - isTyping qusr Nothing lcnv tdurTypingStatus + + ret <- runError + . mapToRuntimeError @'ConvNotFound ConvNotFound + $ do + (conv, _) <- getConversationAndMemberWithError @'ConvNotFound qusr lcnv + notifyTypingIndicator conv qusr Nothing tdurTypingStatus + + pure (either TypingDataUpdateError TypingDataUpdateSuccess ret) + +onTypingIndicatorUpdated :: + ( Member GundeckAccess r + ) => + Domain -> + TypingDataUpdated -> + Sem r EmptyResponse +onTypingIndicatorUpdated origDomain TypingDataUpdated {..} = do + let qcnv = Qualified tudConvId origDomain + pushTypingIndicatorEvents tudOrigUserId tudTime tudUsersInConv Nothing qcnv tudTypingStatus pure EmptyResponse diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 972248483c..cef9264c17 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -33,7 +33,6 @@ import Data.Range import Data.Singletons import Data.String.Conversions (cs) import Data.Time -import GHC.TypeLits (AppendSymbol) import qualified Galley.API.Clients as Clients import qualified Galley.API.Create as Create import qualified Galley.API.CustomBackend as CustomBackend @@ -68,9 +67,7 @@ import Galley.Options import qualified Galley.Queue as Q import Galley.Types.Bot (AddBot, RemoveBot) import Galley.Types.Bot.Service -import Galley.Types.Conversations.Intra (UpsertOne2OneConversationRequest (..), UpsertOne2OneConversationResponse (..)) import Galley.Types.Conversations.Members (RemoteMember (rmId)) -import Galley.Types.Teams.Intra import Galley.Types.UserList import Imports hiding (head) import Network.Wai.Predicate hiding (Error, err) @@ -83,13 +80,10 @@ import Polysemy.Error import Polysemy.Input import qualified Polysemy.TinyLog as P import Servant hiding (JSON, WithStatus) -import qualified Servant hiding (WithStatus) import System.Logger.Class hiding (Path, name) import qualified System.Logger.Class as Log -import Wire.API.ApplyMods import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Action -import Wire.API.Conversation.Role import Wire.API.CustomBackend import Wire.API.Error import Wire.API.Error.Galley @@ -99,381 +93,20 @@ import Wire.API.Federation.API.Galley import Wire.API.Federation.Error import Wire.API.Provider.Service hiding (Service) import Wire.API.Routes.API -import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti +import Wire.API.Routes.Internal.Galley +import Wire.API.Routes.Internal.Galley.TeamsIntra 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.Conversation -import Wire.API.Routes.Public.Galley.Feature -import Wire.API.Team import Wire.API.Team.Feature import Wire.API.Team.Member -import Wire.API.Team.SearchVisibility import Wire.Sem.Paging import Wire.Sem.Paging.Cassandra -type LegalHoldFeatureStatusChangeErrors = - '( 'ActionDenied 'RemoveConversationMember, - '( AuthenticationError, - '( 'CannotEnableLegalHoldServiceLargeTeam, - '( 'LegalHoldNotEnabled, - '( 'LegalHoldDisableUnimplemented, - '( 'LegalHoldServiceNotRegistered, - '( 'UserLegalHoldIllegalOperation, - '( 'LegalHoldCouldNotBlockConnections, '()) - ) - ) - ) - ) - ) - ) - ) - -type LegalHoldFeaturesStatusChangeFederatedCalls = - '[ MakesFederatedCall 'Galley "on-conversation-updated", - MakesFederatedCall 'Galley "on-mls-message-sent", - MakesFederatedCall 'Galley "on-new-remote-conversation" - ] - -type IFeatureAPI = - -- SSOConfig - IFeatureStatusGet SSOConfig - :<|> IFeatureStatusPut '[] '() SSOConfig - :<|> IFeatureStatusPatch '[] '() SSOConfig - -- LegalholdConfig - :<|> IFeatureStatusGet LegalholdConfig - :<|> IFeatureStatusPut - LegalHoldFeaturesStatusChangeFederatedCalls - LegalHoldFeatureStatusChangeErrors - LegalholdConfig - :<|> IFeatureStatusPatch - LegalHoldFeaturesStatusChangeFederatedCalls - LegalHoldFeatureStatusChangeErrors - LegalholdConfig - -- SearchVisibilityAvailableConfig - :<|> IFeatureStatusGet SearchVisibilityAvailableConfig - :<|> IFeatureStatusPut '[] '() SearchVisibilityAvailableConfig - :<|> IFeatureStatusPatch '[] '() SearchVisibilityAvailableConfig - -- ValidateSAMLEmailsConfig - :<|> IFeatureStatusGet ValidateSAMLEmailsConfig - :<|> IFeatureStatusPut '[] '() ValidateSAMLEmailsConfig - :<|> IFeatureStatusPatch '[] '() ValidateSAMLEmailsConfig - -- DigitalSignaturesConfig - :<|> IFeatureStatusGet DigitalSignaturesConfig - :<|> IFeatureStatusPut '[] '() DigitalSignaturesConfig - :<|> IFeatureStatusPatch '[] '() DigitalSignaturesConfig - -- AppLockConfig - :<|> IFeatureStatusGet AppLockConfig - :<|> IFeatureStatusPut '[] '() AppLockConfig - :<|> IFeatureStatusPatch '[] '() AppLockConfig - -- FileSharingConfig - :<|> IFeatureStatusGet FileSharingConfig - :<|> IFeatureStatusPut '[] '() FileSharingConfig - :<|> IFeatureStatusLockStatusPut FileSharingConfig - :<|> IFeatureStatusPatch '[] '() FileSharingConfig - -- ConferenceCallingConfig - :<|> IFeatureStatusGet ConferenceCallingConfig - :<|> IFeatureStatusPut '[] '() ConferenceCallingConfig - :<|> IFeatureStatusPatch '[] '() ConferenceCallingConfig - -- SelfDeletingMessagesConfig - :<|> IFeatureStatusGet SelfDeletingMessagesConfig - :<|> IFeatureStatusPut '[] '() SelfDeletingMessagesConfig - :<|> IFeatureStatusLockStatusPut SelfDeletingMessagesConfig - :<|> IFeatureStatusPatch '[] '() SelfDeletingMessagesConfig - -- GuestLinksConfig - :<|> IFeatureStatusGet GuestLinksConfig - :<|> IFeatureStatusPut '[] '() GuestLinksConfig - :<|> IFeatureStatusLockStatusPut GuestLinksConfig - :<|> IFeatureStatusPatch '[] '() GuestLinksConfig - -- SndFactorPasswordChallengeConfig - :<|> IFeatureStatusGet SndFactorPasswordChallengeConfig - :<|> IFeatureStatusPut '[] '() SndFactorPasswordChallengeConfig - :<|> IFeatureStatusLockStatusPut SndFactorPasswordChallengeConfig - :<|> IFeatureStatusPatch '[] '() SndFactorPasswordChallengeConfig - -- SearchVisibilityInboundConfig - :<|> IFeatureStatusGet SearchVisibilityInboundConfig - :<|> IFeatureStatusPut '[] '() SearchVisibilityInboundConfig - :<|> IFeatureStatusPatch '[] '() SearchVisibilityInboundConfig - :<|> IFeatureNoConfigMultiGet SearchVisibilityInboundConfig - -- ClassifiedDomainsConfig - :<|> IFeatureStatusGet ClassifiedDomainsConfig - -- MLSConfig - :<|> IFeatureStatusGet MLSConfig - :<|> IFeatureStatusPut '[] '() MLSConfig - :<|> IFeatureStatusPatch '[] '() MLSConfig - -- ExposeInvitationURLsToTeamAdminConfig - :<|> IFeatureStatusGet ExposeInvitationURLsToTeamAdminConfig - :<|> IFeatureStatusPut '[] '() ExposeInvitationURLsToTeamAdminConfig - :<|> IFeatureStatusPatch '[] '() ExposeInvitationURLsToTeamAdminConfig - -- SearchVisibilityInboundConfig - :<|> IFeatureStatusGet SearchVisibilityInboundConfig - :<|> IFeatureStatusPut '[] '() SearchVisibilityInboundConfig - :<|> IFeatureStatusPatch '[] '() SearchVisibilityInboundConfig - -- all feature configs - :<|> Named - "feature-configs-internal" - ( Summary "Get all feature configs (for user/team; if n/a fall back to site config)." - :> "feature-configs" - :> CanThrow OperationDenied - :> CanThrow 'NotATeamMember - :> CanThrow 'TeamNotFound - :> QueryParam' - [ Optional, - Strict, - Description "Optional user id" - ] - "user_id" - UserId - :> Get '[Servant.JSON] AllFeatureConfigs - ) - -type InternalAPI = "i" :> InternalAPIBase - -type InternalAPIBase = - Named - "status" - ( "status" :> MultiVerb 'GET '[Servant.JSON] '[RespondEmpty 200 "OK"] () - ) - -- This endpoint can lead to the following events being sent: - -- - MemberLeave event to members for all conversations the user was in - :<|> Named - "delete-user" - ( Summary - "Remove a user from their teams and conversations and erase their clients" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-user-deleted-conversations" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> ZLocalUser - :> ZOptConn - :> "user" - :> MultiVerb 'DELETE '[Servant.JSON] '[RespondEmpty 200 "Remove a user from Galley"] () - ) - -- This endpoint can lead to the following events being sent: - -- - ConvCreate event to self, if conversation did not exist before - -- - ConvConnect event to self, if other didn't join the connect conversation before - :<|> Named - "connect" - ( Summary "Create a connect conversation (deprecated)" - :> MakesFederatedCall 'Galley "on-conversation-created" - :> CanThrow 'ConvNotFound - :> CanThrow 'InvalidOperation - :> CanThrow 'NotConnected - :> ZLocalUser - :> ZOptConn - :> "conversations" - :> "connect" - :> ReqBody '[Servant.JSON] Connect - :> ConversationVerb - ) - :<|> Named - "guard-legalhold-policy-conflicts" - ( "guard-legalhold-policy-conflicts" - :> CanThrow 'MissingLegalholdConsent - :> ReqBody '[Servant.JSON] GuardLegalholdPolicyConflicts - :> MultiVerb1 'PUT '[Servant.JSON] (RespondEmpty 200 "Guard Legalhold Policy") - ) - :<|> ILegalholdWhitelistedTeamsAPI - :<|> ITeamsAPI - :<|> Named - "upsert-one2one" - ( Summary "Create or Update a connect or one2one conversation." - :> "conversations" - :> "one2one" - :> "upsert" - :> ReqBody '[Servant.JSON] UpsertOne2OneConversationRequest - :> Post '[Servant.JSON] UpsertOne2OneConversationResponse - ) - :<|> IFeatureAPI - -type ILegalholdWhitelistedTeamsAPI = - "legalhold" - :> "whitelisted-teams" - :> Capture "tid" TeamId - :> ILegalholdWhitelistedTeamsAPIBase - -type ILegalholdWhitelistedTeamsAPIBase = - Named - "set-team-legalhold-whitelisted" - (MultiVerb1 'PUT '[Servant.JSON] (RespondEmpty 200 "Team Legalhold Whitelisted")) - :<|> Named - "unset-team-legalhold-whitelisted" - (MultiVerb1 'DELETE '[Servant.JSON] (RespondEmpty 204 "Team Legalhold un-Whitelisted")) - :<|> Named - "get-team-legalhold-whitelisted" - ( MultiVerb - 'GET - '[Servant.JSON] - '[ RespondEmpty 404 "Team not Legalhold Whitelisted", - RespondEmpty 200 "Team Legalhold Whitelisted" - ] - Bool - ) - -type ITeamsAPI = "teams" :> Capture "tid" TeamId :> ITeamsAPIBase - -type ITeamsAPIBase = - Named "get-team-internal" (CanThrow 'TeamNotFound :> Get '[Servant.JSON] TeamData) - :<|> Named - "create-binding-team" - ( ZUser - :> ReqBody '[Servant.JSON] BindingNewTeam - :> MultiVerb1 - 'PUT - '[Servant.JSON] - ( WithHeaders - '[Header "Location" TeamId] - TeamId - (RespondEmpty 201 "OK") - ) - ) - :<|> Named - "delete-binding-team" - ( CanThrow 'NoBindingTeam - :> CanThrow 'NotAOneMemberTeam - :> CanThrow 'DeleteQueueFull - :> CanThrow 'TeamNotFound - :> QueryFlag "force" - :> MultiVerb1 'DELETE '[Servant.JSON] (RespondEmpty 202 "OK") - ) - :<|> Named "get-team-name" ("name" :> CanThrow 'TeamNotFound :> Get '[Servant.JSON] TeamName) - :<|> Named - "update-team-status" - ( "status" - :> CanThrow 'TeamNotFound - :> CanThrow 'InvalidTeamStatusUpdate - :> ReqBody '[Servant.JSON] TeamStatusUpdate - :> MultiVerb1 'PUT '[Servant.JSON] (RespondEmpty 200 "OK") - ) - :<|> "members" - :> ( Named - "unchecked-add-team-member" - ( CanThrow 'TooManyTeamMembers - :> CanThrow 'TooManyTeamMembersOnTeamWithLegalhold - :> ReqBody '[Servant.JSON] NewTeamMember - :> MultiVerb1 'POST '[Servant.JSON] (RespondEmpty 200 "OK") - ) - :<|> Named - "unchecked-get-team-members" - ( QueryParam' '[Strict] "maxResults" (Range 1 HardTruncationLimit Int32) - :> Get '[Servant.JSON] TeamMemberList - ) - :<|> Named - "unchecked-get-team-member" - ( Capture "uid" UserId - :> CanThrow 'TeamMemberNotFound - :> Get '[Servant.JSON] TeamMember - ) - :<|> Named - "can-user-join-team" - ( "check" - :> 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" - ( "is-team-owner" - :> Capture "uid" UserId - :> CanThrow 'AccessDenied - :> CanThrow 'TeamMemberNotFound - :> CanThrow 'NotATeamMember - :> MultiVerb1 'GET '[Servant.JSON] (RespondEmpty 200 "User is team owner") - ) - :<|> "search-visibility" - :> ( Named "get-search-visibility-internal" (Get '[Servant.JSON] TeamSearchVisibilityView) - :<|> Named - "set-search-visibility-internal" - ( CanThrow 'TeamSearchVisibilityNotEnabled - :> CanThrow OperationDenied - :> CanThrow 'NotATeamMember - :> CanThrow 'TeamNotFound - :> ReqBody '[Servant.JSON] TeamSearchVisibilityView - :> MultiVerb1 'PUT '[Servant.JSON] (RespondEmpty 204 "OK") - ) - ) - -type IFeatureStatusGet f = Named '("iget", f) (FeatureStatusBaseGet f) - -type IFeatureStatusPut calls errs f = Named '("iput", f) (ApplyMods calls (FeatureStatusBasePutInternal errs f)) - -type IFeatureStatusPatch calls errs f = Named '("ipatch", f) (ApplyMods calls (FeatureStatusBasePatchInternal errs f)) - -type FeatureStatusBasePutInternal errs featureConfig = - FeatureStatusBaseInternal - (AppendSymbol "Put config for " (FeatureSymbol featureConfig)) - errs - featureConfig - ( ReqBody '[Servant.JSON] (WithStatusNoLock featureConfig) - :> Put '[Servant.JSON] (WithStatus featureConfig) - ) - -type FeatureStatusBasePatchInternal errs featureConfig = - FeatureStatusBaseInternal - (AppendSymbol "Patch config for " (FeatureSymbol featureConfig)) - errs - featureConfig - ( ReqBody '[Servant.JSON] (WithStatusPatch featureConfig) - :> Patch '[Servant.JSON] (WithStatus featureConfig) - ) - -type FeatureStatusBaseInternal desc errs featureConfig a = - Summary desc - :> CanThrow OperationDenied - :> CanThrow 'NotATeamMember - :> CanThrow 'TeamNotFound - :> CanThrow TeamFeatureError - :> CanThrowMany errs - :> "teams" - :> Capture "tid" TeamId - :> "features" - :> FeatureSymbol featureConfig - :> a - -type IFeatureStatusLockStatusPut featureName = - Named - '("ilock", featureName) - ( Summary (AppendSymbol "(Un-)lock " (FeatureSymbol featureName)) - :> CanThrow 'NotATeamMember - :> CanThrow 'TeamNotFound - :> "teams" - :> Capture "tid" TeamId - :> "features" - :> FeatureSymbol featureName - :> Capture "lockStatus" LockStatus - :> Put '[Servant.JSON] LockStatusResponse - ) - -type FeatureNoConfigMultiGetBase featureName = - Summary - (AppendSymbol "Get team feature status in bulk for feature " (FeatureSymbol featureName)) - :> "features-multi-teams" - :> FeatureSymbol featureName - :> ReqBody '[Servant.JSON] TeamFeatureNoConfigMultiRequest - :> Post '[Servant.JSON] (TeamFeatureNoConfigMultiResponse featureName) - -type IFeatureNoConfigMultiGet f = - Named - '("igetmulti", f) - (FeatureNoConfigMultiGetBase f) - internalAPI :: API InternalAPI GalleyEffects internalAPI = hoistAPI @InternalAPIBase id $ mkNamedAPI @"status" (pure ()) - <@> mkNamedAPI @"delete-user" (callsFed rmUser) - <@> mkNamedAPI @"connect" (callsFed Create.createConnectConversation) + <@> mkNamedAPI @"delete-user" (callsFed (exposeAnnotations rmUser)) + <@> mkNamedAPI @"connect" (callsFed (exposeAnnotations Create.createConnectConversation)) <@> mkNamedAPI @"guard-legalhold-policy-conflicts" guardLegalholdPolicyConflictsH <@> legalholdWhitelistedTeamsAPI <@> iTeamsAPI @@ -515,7 +148,7 @@ iTeamsAPI = mkAPI $ \tid -> hoistAPIHandler id (base tid) <@> mkNamedAPI @"user-is-team-owner" (Teams.userIsTeamOwner tid) <@> hoistAPISegment ( mkNamedAPI @"get-search-visibility-internal" (Teams.getSearchVisibilityInternal tid) - <@> mkNamedAPI @"set-search-visibility-internal" (Teams.setSearchVisibilityInternal @Cassandra (featureEnabledForTeam @Cassandra @SearchVisibilityAvailableConfig) tid) + <@> mkNamedAPI @"set-search-visibility-internal" (Teams.setSearchVisibilityInternal (featureEnabledForTeam @Cassandra @SearchVisibilityAvailableConfig) tid) ) featureAPI :: API IFeatureAPI GalleyEffects @@ -524,8 +157,8 @@ featureAPI = <@> mkNamedAPI @'("iput", SSOConfig) (setFeatureStatusInternal @Cassandra) <@> mkNamedAPI @'("ipatch", SSOConfig) (patchFeatureStatusInternal @Cassandra) <@> mkNamedAPI @'("iget", LegalholdConfig) (getFeatureStatus @Cassandra DontDoAuth) - <@> mkNamedAPI @'("iput", LegalholdConfig) (callsFed (setFeatureStatusInternal @Cassandra)) - <@> mkNamedAPI @'("ipatch", LegalholdConfig) (callsFed (patchFeatureStatusInternal @Cassandra)) + <@> mkNamedAPI @'("iput", LegalholdConfig) (callsFed (exposeAnnotations (setFeatureStatusInternal @Cassandra))) + <@> mkNamedAPI @'("ipatch", LegalholdConfig) (callsFed (exposeAnnotations (patchFeatureStatusInternal @Cassandra))) <@> mkNamedAPI @'("iget", SearchVisibilityAvailableConfig) (getFeatureStatus @Cassandra DontDoAuth) <@> mkNamedAPI @'("iput", SearchVisibilityAvailableConfig) (setFeatureStatusInternal @Cassandra) <@> mkNamedAPI @'("ipatch", SearchVisibilityAvailableConfig) (patchFeatureStatusInternal @Cassandra) @@ -571,7 +204,15 @@ featureAPI = <@> mkNamedAPI @'("iget", SearchVisibilityInboundConfig) (getFeatureStatus @Cassandra DontDoAuth) <@> mkNamedAPI @'("iput", SearchVisibilityInboundConfig) (setFeatureStatusInternal @Cassandra) <@> mkNamedAPI @'("ipatch", SearchVisibilityInboundConfig) (patchFeatureStatusInternal @Cassandra) - <@> mkNamedAPI @"feature-configs-internal" (maybe (getAllFeatureConfigsForServer @Cassandra) (getAllFeatureConfigsForUser @Cassandra)) + <@> mkNamedAPI @'("iget", OutlookCalIntegrationConfig) (getFeatureStatus @Cassandra DontDoAuth) + <@> mkNamedAPI @'("iput", OutlookCalIntegrationConfig) (setFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("ipatch", OutlookCalIntegrationConfig) (patchFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("ilock", OutlookCalIntegrationConfig) (updateLockStatus @Cassandra @OutlookCalIntegrationConfig) + <@> mkNamedAPI @'("iget", MlsE2EIdConfig) (getFeatureStatus @Cassandra DontDoAuth) + <@> mkNamedAPI @'("iput", MlsE2EIdConfig) (setFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("ipatch", MlsE2EIdConfig) (patchFeatureStatusInternal @Cassandra) + <@> mkNamedAPI @'("ilock", MlsE2EIdConfig) (updateLockStatus @Cassandra @MlsE2EIdConfig) + <@> mkNamedAPI @"feature-configs-internal" (maybe getAllFeatureConfigsForServer (getAllFeatureConfigsForUser @Cassandra)) internalSitemap :: Routes a (Sem GalleyEffects) () internalSitemap = unsafeCallsFed @'Galley @"on-client-removed" $ unsafeCallsFed @'Galley @"on-mls-message-sent" $ do @@ -662,29 +303,24 @@ rmUser :: forall p1 p2 r. ( p1 ~ CassandraPaging, p2 ~ InternalPaging, - Members - '[ BrigAccess, - ClientStore, - ConversationStore, - Error InternalError, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input (Local ()), - Input UTCTime, - ListItems p1 ConvId, - ListItems p1 (Remote ConvId), - ListItems p2 TeamId, - MemberStore, - ProposalStore, - P.TinyLog, - TeamStore - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-user-deleted-conversations", - CallsFed 'Galley "on-mls-message-sent" + ( Member BrigAccess r, + Member ClientStore r, + Member ConversationStore r, + Member (Error InternalError) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ListItems p1 ConvId) r, + Member (ListItems p1 (Remote ConvId)) r, + Member (ListItems p2 TeamId) r, + Member MemberStore r, + Member ProposalStore r, + Member P.TinyLog r, + Member TeamStore r + ) ) => Local UserId -> Maybe ConnId -> @@ -819,15 +455,12 @@ safeForever funName action = threadDelay 60000000 -- pause to keep worst-case noise in logs manageable guardLegalholdPolicyConflictsH :: - Members - '[ BrigAccess, - Input Opts, - TeamStore, - P.TinyLog, - WaiRoutes, - ErrorS 'MissingLegalholdConsent - ] - r => + ( Member BrigAccess r, + Member (Input Opts) r, + Member TeamStore r, + Member P.TinyLog r, + Member (ErrorS 'MissingLegalholdConsent) r + ) => GuardLegalholdPolicyConflicts -> Sem r () guardLegalholdPolicyConflictsH glh = do diff --git a/services/galley/src/Galley/API/LegalHold.hs b/services/galley/src/Galley/API/LegalHold.hs index cfb7e9cbae..df76873206 100644 --- a/services/galley/src/Galley/API/LegalHold.hs +++ b/services/galley/src/Galley/API/LegalHold.hs @@ -72,7 +72,6 @@ import Wire.API.Conversation (ConvType (..)) import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley -import Wire.API.Federation.API import Wire.API.Provider.Service import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Public.Galley.LegalHold @@ -87,13 +86,11 @@ import Wire.Sem.Paging.Cassandra assertLegalHoldEnabledForTeam :: forall db r. - Members - '[ LegalHoldStore, - TeamStore, - TeamFeatureStore db, - ErrorS 'LegalHoldNotEnabled - ] - r => + ( Member LegalHoldStore r, + Member TeamStore r, + Member (TeamFeatureStore db) r, + Member (ErrorS 'LegalHoldNotEnabled) r + ) => TeamFeatures.FeaturePersistentConstraint db Public.LegalholdConfig => TeamId -> Sem r () @@ -103,7 +100,9 @@ assertLegalHoldEnabledForTeam tid = isLegalHoldEnabledForTeam :: forall db r. - ( Members '[LegalHoldStore, TeamStore, TeamFeatureStore db] r, + ( Member LegalHoldStore r, + Member TeamStore r, + Member (TeamFeatureStore db) r, TeamFeatures.FeaturePersistentConstraint db Public.LegalholdConfig ) => TeamId -> @@ -124,18 +123,16 @@ isLegalHoldEnabledForTeam tid = do createSettings :: forall db r. - Members - '[ ErrorS 'NotATeamMember, - ErrorS OperationDenied, - ErrorS 'LegalHoldNotEnabled, - ErrorS 'LegalHoldServiceInvalidKey, - ErrorS 'LegalHoldServiceBadResponse, - LegalHoldStore, - TeamFeatureStore db, - TeamStore, - P.TinyLog - ] - r => + ( Member (ErrorS 'NotATeamMember) r, + Member (ErrorS OperationDenied) r, + Member (ErrorS 'LegalHoldNotEnabled) r, + Member (ErrorS 'LegalHoldServiceInvalidKey) r, + Member (ErrorS 'LegalHoldServiceBadResponse) r, + Member LegalHoldStore r, + Member (TeamFeatureStore db) r, + Member TeamStore r, + Member P.TinyLog r + ) => TeamFeatures.FeaturePersistentConstraint db Public.LegalholdConfig => Local UserId -> TeamId -> @@ -160,14 +157,11 @@ createSettings lzusr tid newService = do getSettings :: forall db r. - Members - '[ ErrorS OperationDenied, - ErrorS 'NotATeamMember, - LegalHoldStore, - TeamFeatureStore db, - TeamStore - ] - r => + ( Member (ErrorS 'NotATeamMember) r, + Member LegalHoldStore r, + Member (TeamFeatureStore db) r, + Member TeamStore r + ) => TeamFeatures.FeaturePersistentConstraint db Public.LegalholdConfig => Local UserId -> TeamId -> @@ -185,43 +179,36 @@ getSettings lzusr tid = do removeSettingsInternalPaging :: forall db r. - ( Members - '[ BotAccess, - BrigAccess, - CodeStore, - ConversationStore, - Error AuthenticationError, - Error InternalError, - ErrorS ('ActionDenied 'RemoveConversationMember), - ErrorS 'InvalidOperation, - ErrorS 'LegalHoldCouldNotBlockConnections, - ErrorS 'LegalHoldDisableUnimplemented, - ErrorS 'LegalHoldNotEnabled, - ErrorS 'LegalHoldServiceNotRegistered, - ErrorS 'NotATeamMember, - ErrorS OperationDenied, - ErrorS 'UserLegalHoldIllegalOperation, - ExternalAccess, - FederatorAccess, - FireAndForget, - GundeckAccess, - Input Env, - Input (Local ()), - Input UTCTime, - LegalHoldStore, - ListItems LegacyPaging ConvId, - MemberStore, - ProposalStore, - P.TinyLog, - TeamFeatureStore db, - TeamMemberStore InternalPaging, - TeamStore, - WaiRoutes - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member BotAccess r, + Member BrigAccess r, + Member CodeStore r, + Member ConversationStore r, + Member (Error AuthenticationError) r, + Member (Error InternalError) r, + Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'LegalHoldCouldNotBlockConnections) r, + Member (ErrorS 'LegalHoldDisableUnimplemented) r, + Member (ErrorS 'LegalHoldNotEnabled) r, + Member (ErrorS 'LegalHoldServiceNotRegistered) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS OperationDenied) r, + Member (ErrorS 'UserLegalHoldIllegalOperation) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member FireAndForget r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member LegalHoldStore r, + Member (ListItems LegacyPaging ConvId) r, + Member MemberStore r, + Member ProposalStore r, + Member P.TinyLog r, + Member (TeamFeatureStore db) r, + Member (TeamMemberStore InternalPaging) r, + Member TeamStore r ) => TeamFeatures.FeaturePersistentConstraint db Public.LegalholdConfig => Local UserId -> @@ -234,42 +221,37 @@ removeSettings :: forall db p r. ( Paging p, Bounded (PagingBounds p TeamMember), - Members - '[ BotAccess, - BrigAccess, - CodeStore, - ConversationStore, - Error AuthenticationError, - Error InternalError, - ErrorS ('ActionDenied 'RemoveConversationMember), - ErrorS 'InvalidOperation, - ErrorS 'LegalHoldCouldNotBlockConnections, - ErrorS 'LegalHoldDisableUnimplemented, - ErrorS 'LegalHoldNotEnabled, - ErrorS 'LegalHoldServiceNotRegistered, - ErrorS 'NotATeamMember, - ErrorS OperationDenied, - ErrorS 'UserLegalHoldIllegalOperation, - ExternalAccess, - FederatorAccess, - FireAndForget, - GundeckAccess, - Input Env, - Input (Local ()), - Input UTCTime, - LegalHoldStore, - ListItems LegacyPaging ConvId, - MemberStore, - ProposalStore, - P.TinyLog, - TeamFeatureStore db, - TeamMemberStore p, - TeamStore - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member BotAccess r, + Member BrigAccess r, + Member CodeStore r, + Member ConversationStore r, + Member (Error AuthenticationError) r, + Member (Error InternalError) r, + Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'LegalHoldCouldNotBlockConnections) r, + Member (ErrorS 'LegalHoldDisableUnimplemented) r, + Member (ErrorS 'LegalHoldNotEnabled) r, + Member (ErrorS 'LegalHoldServiceNotRegistered) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS OperationDenied) r, + Member (ErrorS 'UserLegalHoldIllegalOperation) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member FireAndForget r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member LegalHoldStore r, + Member (ListItems LegacyPaging ConvId) r, + Member MemberStore r, + Member ProposalStore r, + Member P.TinyLog r, + Member (TeamFeatureStore db) r, + Member (TeamMemberStore p) r, + Member TeamStore r + ) ) => TeamFeatures.FeaturePersistentConstraint db Public.LegalholdConfig => UserId -> @@ -301,37 +283,32 @@ removeSettings' :: forall p r. ( Paging p, Bounded (PagingBounds p TeamMember), - Members - '[ BotAccess, - BrigAccess, - CodeStore, - ConversationStore, - Error InternalError, - Error AuthenticationError, - ErrorS 'NotATeamMember, - ErrorS ('ActionDenied 'RemoveConversationMember), - ErrorS 'LegalHoldServiceNotRegistered, - ErrorS 'UserLegalHoldIllegalOperation, - ErrorS 'LegalHoldCouldNotBlockConnections, - ExternalAccess, - FederatorAccess, - FireAndForget, - GundeckAccess, - Input UTCTime, - Input (Local ()), - Input Env, - LegalHoldStore, - ListItems LegacyPaging ConvId, - MemberStore, - TeamMemberStore p, - TeamStore, - ProposalStore, - P.TinyLog - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member BotAccess r, + Member BrigAccess r, + Member CodeStore r, + Member ConversationStore r, + Member (Error InternalError) r, + Member (Error AuthenticationError) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, + Member (ErrorS 'LegalHoldServiceNotRegistered) r, + Member (ErrorS 'UserLegalHoldIllegalOperation) r, + Member (ErrorS 'LegalHoldCouldNotBlockConnections) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member FireAndForget r, + Member GundeckAccess r, + Member (Input UTCTime) r, + Member (Input (Local ())) r, + Member (Input Env) r, + Member LegalHoldStore r, + Member (ListItems LegacyPaging ConvId) r, + Member MemberStore r, + Member (TeamMemberStore p) r, + Member TeamStore r, + Member ProposalStore r, + Member P.TinyLog r + ) ) => TeamId -> Sem r () @@ -359,14 +336,12 @@ removeSettings' tid = -- Note that this is accessible to ANY authenticated user, even ones outside the team getUserStatus :: forall r. - Members - '[ Error InternalError, - ErrorS 'TeamMemberNotFound, - LegalHoldStore, - TeamStore, - P.TinyLog - ] - r => + ( Member (Error InternalError) r, + Member (ErrorS 'TeamMemberNotFound) r, + Member LegalHoldStore r, + Member TeamStore r, + Member P.TinyLog r + ) => Local UserId -> TeamId -> UserId -> @@ -399,31 +374,24 @@ getUserStatus _lzusr tid uid = do -- @withdrawExplicitConsentH@ (lots of corner cases we'd have to implement for that to pan -- out). grantConsent :: - ( Members - '[ BrigAccess, - ConversationStore, - Error InternalError, - ErrorS ('ActionDenied 'RemoveConversationMember), - ErrorS 'InvalidOperation, - ErrorS 'LegalHoldCouldNotBlockConnections, - ErrorS 'TeamMemberNotFound, - ErrorS 'UserLegalHoldIllegalOperation, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input UTCTime, - LegalHoldStore, - ListItems LegacyPaging ConvId, - MemberStore, - ProposalStore, - P.TinyLog, - TeamStore - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member BrigAccess r, + Member ConversationStore r, + Member (Error InternalError) r, + Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, + Member (ErrorS 'LegalHoldCouldNotBlockConnections) r, + Member (ErrorS 'TeamMemberNotFound) r, + Member (ErrorS 'UserLegalHoldIllegalOperation) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input UTCTime) r, + Member LegalHoldStore r, + Member (ListItems LegacyPaging ConvId) r, + Member MemberStore r, + Member ProposalStore r, + Member P.TinyLog r, + Member TeamStore r ) => Local UserId -> TeamId -> @@ -442,39 +410,33 @@ grantConsent lusr tid = do -- | Request to provision a device on the legal hold service for a user requestDevice :: forall db r. - ( Members - '[ BrigAccess, - ConversationStore, - Error InternalError, - ErrorS ('ActionDenied 'RemoveConversationMember), - ErrorS 'LegalHoldCouldNotBlockConnections, - ErrorS 'LegalHoldNotEnabled, - ErrorS 'LegalHoldServiceBadResponse, - ErrorS 'LegalHoldServiceNotRegistered, - ErrorS 'NotATeamMember, - ErrorS 'NoUserLegalHoldConsent, - ErrorS OperationDenied, - ErrorS 'TeamMemberNotFound, - ErrorS 'UserLegalHoldAlreadyEnabled, - ErrorS 'UserLegalHoldIllegalOperation, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input (Local ()), - Input Env, - Input UTCTime, - LegalHoldStore, - ListItems LegacyPaging ConvId, - MemberStore, - ProposalStore, - P.TinyLog, - TeamFeatureStore db, - TeamStore - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member BrigAccess r, + Member ConversationStore r, + Member (Error InternalError) r, + Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, + Member (ErrorS 'LegalHoldCouldNotBlockConnections) r, + Member (ErrorS 'LegalHoldNotEnabled) r, + Member (ErrorS 'LegalHoldServiceBadResponse) r, + Member (ErrorS 'LegalHoldServiceNotRegistered) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'NoUserLegalHoldConsent) r, + Member (ErrorS OperationDenied) r, + Member (ErrorS 'TeamMemberNotFound) r, + Member (ErrorS 'UserLegalHoldAlreadyEnabled) r, + Member (ErrorS 'UserLegalHoldIllegalOperation) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input (Local ())) r, + Member (Input Env) r, + Member (Input UTCTime) r, + Member LegalHoldStore r, + Member (ListItems LegacyPaging ConvId) r, + Member MemberStore r, + Member ProposalStore r, + Member P.TinyLog r, + Member (TeamFeatureStore db) r, + Member TeamStore r ) => TeamFeatures.FeaturePersistentConstraint db Public.LegalholdConfig => Local UserId -> @@ -526,39 +488,33 @@ requestDevice lzusr tid uid = do -- since they are replaced if needed when registering new LH devices. approveDevice :: forall db r. - ( Members - '[ BrigAccess, - ConversationStore, - Error AuthenticationError, - Error InternalError, - ErrorS 'AccessDenied, - ErrorS ('ActionDenied 'RemoveConversationMember), - ErrorS 'LegalHoldCouldNotBlockConnections, - ErrorS 'LegalHoldNotEnabled, - ErrorS 'LegalHoldServiceNotRegistered, - ErrorS 'NoLegalHoldDeviceAllocated, - ErrorS 'NotATeamMember, - ErrorS 'UserLegalHoldAlreadyEnabled, - ErrorS 'UserLegalHoldIllegalOperation, - ErrorS 'UserLegalHoldNotPending, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input (Local ()), - Input Env, - Input UTCTime, - LegalHoldStore, - ListItems LegacyPaging ConvId, - MemberStore, - ProposalStore, - P.TinyLog, - TeamFeatureStore db, - TeamStore - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member BrigAccess r, + Member ConversationStore r, + Member (Error AuthenticationError) r, + Member (Error InternalError) r, + Member (ErrorS 'AccessDenied) r, + Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, + Member (ErrorS 'LegalHoldCouldNotBlockConnections) r, + Member (ErrorS 'LegalHoldNotEnabled) r, + Member (ErrorS 'LegalHoldServiceNotRegistered) r, + Member (ErrorS 'NoLegalHoldDeviceAllocated) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'UserLegalHoldAlreadyEnabled) r, + Member (ErrorS 'UserLegalHoldIllegalOperation) r, + Member (ErrorS 'UserLegalHoldNotPending) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input (Local ())) r, + Member (Input Env) r, + Member (Input UTCTime) r, + Member LegalHoldStore r, + Member (ListItems LegacyPaging ConvId) r, + Member MemberStore r, + Member ProposalStore r, + Member P.TinyLog r, + Member (TeamFeatureStore db) r, + Member TeamStore r ) => TeamFeatures.FeaturePersistentConstraint db Public.LegalholdConfig => Local UserId -> @@ -599,7 +555,6 @@ approveDevice lzusr connId tid uid (Public.ApproveLegalHoldForUserRequest mPassw changeLegalholdStatus tid luid userLHStatus UserLegalHoldEnabled where assertUserLHPending :: - Members '[ErrorS 'UserLegalHoldNotPending, ErrorS 'UserLegalHoldAlreadyEnabled] r => UserLegalHoldStatus -> Sem r () assertUserLHPending userLHStatus = do @@ -611,34 +566,28 @@ approveDevice lzusr connId tid uid (Public.ApproveLegalHoldForUserRequest mPassw disableForUser :: forall r. - ( Members - '[ BrigAccess, - ConversationStore, - Error AuthenticationError, - Error InternalError, - ErrorS ('ActionDenied 'RemoveConversationMember), - ErrorS 'LegalHoldCouldNotBlockConnections, - ErrorS 'LegalHoldServiceNotRegistered, - ErrorS 'NotATeamMember, - ErrorS OperationDenied, - ErrorS 'UserLegalHoldIllegalOperation, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input (Local ()), - Input UTCTime, - LegalHoldStore, - ListItems LegacyPaging ConvId, - MemberStore, - ProposalStore, - P.TinyLog, - TeamStore - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member BrigAccess r, + Member ConversationStore r, + Member (Error AuthenticationError) r, + Member (Error InternalError) r, + Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, + Member (ErrorS 'LegalHoldCouldNotBlockConnections) r, + Member (ErrorS 'LegalHoldServiceNotRegistered) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS OperationDenied) r, + Member (ErrorS 'UserLegalHoldIllegalOperation) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member LegalHoldStore r, + Member (ListItems LegacyPaging ConvId) r, + Member MemberStore r, + Member ProposalStore r, + Member P.TinyLog r, + Member TeamStore r ) => Local UserId -> TeamId -> @@ -673,29 +622,23 @@ disableForUser lzusr tid uid (Public.DisableLegalHoldForUserRequest mPassword) = -- or disabled, make sure the affected connections are screened for policy conflict (anybody -- with no-consent), and put those connections in the appropriate blocked state. changeLegalholdStatus :: - ( Members - '[ BrigAccess, - ConversationStore, - Error InternalError, - ErrorS ('ActionDenied 'RemoveConversationMember), - ErrorS 'LegalHoldCouldNotBlockConnections, - ErrorS 'UserLegalHoldIllegalOperation, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input UTCTime, - LegalHoldStore, - ListItems LegacyPaging ConvId, - MemberStore, - TeamStore, - ProposalStore, - P.TinyLog - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member BrigAccess r, + Member ConversationStore r, + Member (Error InternalError) r, + Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, + Member (ErrorS 'LegalHoldCouldNotBlockConnections) r, + Member (ErrorS 'UserLegalHoldIllegalOperation) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input UTCTime) r, + Member LegalHoldStore r, + Member (ListItems LegacyPaging ConvId) r, + Member MemberStore r, + Member TeamStore r, + Member ProposalStore r, + Member P.TinyLog r ) => TeamId -> Local UserId -> @@ -739,13 +682,11 @@ changeLegalholdStatus tid luid old new = do -- FUTUREWORK: make this async? blockNonConsentingConnections :: forall r. - Members - '[ BrigAccess, - TeamStore, - P.TinyLog, - ErrorS 'LegalHoldCouldNotBlockConnections - ] - r => + ( Member BrigAccess r, + Member TeamStore r, + Member P.TinyLog r, + Member (ErrorS 'LegalHoldCouldNotBlockConnections) r + ) => UserId -> Sem r () blockNonConsentingConnections uid = do @@ -797,25 +738,19 @@ unsetTeamLegalholdWhitelistedH tid = do -- contains the hypothetical new LH status of `uid`'s so it can be consulted instead of the -- one from the database. handleGroupConvPolicyConflicts :: - ( Members - '[ ConversationStore, - Error InternalError, - ErrorS ('ActionDenied 'RemoveConversationMember), - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input UTCTime, - ListItems LegacyPaging ConvId, - MemberStore, - ProposalStore, - P.TinyLog, - TeamStore - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member ConversationStore r, + Member (Error InternalError) r, + Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input UTCTime) r, + Member (ListItems LegacyPaging ConvId) r, + Member MemberStore r, + Member ProposalStore r, + Member P.TinyLog r, + Member TeamStore r ) => Local UserId -> UserLegalHoldStatus -> diff --git a/services/galley/src/Galley/API/LegalHold/Conflicts.hs b/services/galley/src/Galley/API/LegalHold/Conflicts.hs index f9e3e07126..28e2083f6d 100644 --- a/services/galley/src/Galley/API/LegalHold/Conflicts.hs +++ b/services/galley/src/Galley/API/LegalHold/Conflicts.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 . +{-# OPTIONS_GHC -Wno-overlapping-patterns #-} module Galley.API.LegalHold.Conflicts where @@ -46,15 +47,13 @@ import Wire.API.User.Client as Client data LegalholdConflicts = LegalholdConflicts guardQualifiedLegalholdPolicyConflicts :: - Members - '[ BrigAccess, - Error LegalholdConflicts, - Input (Local ()), - Input Opts, - TeamStore, - P.TinyLog - ] - r => + ( Member BrigAccess r, + Member (Error LegalholdConflicts) r, + Member (Input (Local ())) r, + Member (Input Opts) r, + Member TeamStore r, + Member P.TinyLog r + ) => LegalholdProtectee -> QualifiedUserClients -> Sem r () @@ -73,14 +72,12 @@ guardQualifiedLegalholdPolicyConflicts protectee qclients = do -- This is a fallback safeguard that shouldn't get triggered if backend and clients work as -- intended. guardLegalholdPolicyConflicts :: - Members - '[ BrigAccess, - Error LegalholdConflicts, - Input Opts, - TeamStore, - P.TinyLog - ] - r => + ( Member BrigAccess r, + Member (Error LegalholdConflicts) r, + Member (Input Opts) r, + Member TeamStore r, + Member P.TinyLog r + ) => LegalholdProtectee -> UserClients -> Sem r () @@ -96,13 +93,11 @@ guardLegalholdPolicyConflicts (ProtectedUser self) otherClients = do guardLegalholdPolicyConflictsUid :: forall r. - Members - '[ BrigAccess, - Error LegalholdConflicts, - TeamStore, - P.TinyLog - ] - r => + ( Member BrigAccess r, + Member (Error LegalholdConflicts) r, + Member TeamStore r, + Member P.TinyLog r + ) => UserId -> UserClients -> Sem r () diff --git a/services/galley/src/Galley/API/MLS/GroupInfo.hs b/services/galley/src/Galley/API/MLS/GroupInfo.hs index 46a6f530f7..fc1c4f0935 100644 --- a/services/galley/src/Galley/API/MLS/GroupInfo.hs +++ b/services/galley/src/Galley/API/MLS/GroupInfo.hs @@ -45,15 +45,11 @@ type MLSGroupInfoStaticErrors = ] getGroupInfo :: - ( Members - '[ ConversationStore, - Error FederationError, - FederatorAccess, - Input Env, - MemberStore - ] - r, - CallsFed 'Galley "query-group-info" + ( Member ConversationStore r, + Member (Error FederationError) r, + Member FederatorAccess r, + Member (Input Env) r, + Member MemberStore r ) => Members MLSGroupInfoStaticErrors r => Local UserId -> @@ -68,11 +64,9 @@ getGroupInfo lusr qcnvId = do qcnvId getGroupInfoFromLocalConv :: - Members - '[ ConversationStore, - MemberStore - ] - r => + ( Member ConversationStore r, + Member MemberStore r + ) => Members MLSGroupInfoStaticErrors r => Qualified UserId -> Local ConvId -> @@ -83,7 +77,9 @@ getGroupInfoFromLocalConv qusr lcnvId = do >>= noteS @'MLSMissingGroupInfo getGroupInfoFromRemoteConv :: - (Members '[Error FederationError, FederatorAccess] r, CallsFed 'Galley "query-group-info") => + ( Member (Error FederationError) r, + Member FederatorAccess r + ) => Members MLSGroupInfoStaticErrors r => Local UserId -> Remote ConvId -> diff --git a/services/galley/src/Galley/API/MLS/KeyPackage.hs b/services/galley/src/Galley/API/MLS/KeyPackage.hs index c5e42031a4..23fe2760c0 100644 --- a/services/galley/src/Galley/API/MLS/KeyPackage.hs +++ b/services/galley/src/Galley/API/MLS/KeyPackage.hs @@ -30,11 +30,9 @@ nullKeyPackageRef :: KeyPackageRef nullKeyPackageRef = KeyPackageRef (BS.replicate 16 0) derefKeyPackage :: - Members - '[ BrigAccess, - ErrorS 'MLSKeyPackageRefNotFound - ] - r => + ( Member BrigAccess r, + Member (ErrorS 'MLSKeyPackageRefNotFound) r + ) => KeyPackageRef -> Sem r ClientIdentity derefKeyPackage = noteS @'MLSKeyPackageRefNotFound <=< getClientByKeyPackageRef diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index b939e3ab08..891d6350a1 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -119,33 +119,26 @@ type MLSBundleStaticErrors = postMLSMessageFromLocalUserV1 :: ( HasProposalEffects r, - Members - '[ Error FederationError, - Error InternalError, - ErrorS 'ConvAccessDenied, - ErrorS 'ConvMemberNotFound, - ErrorS 'ConvNotFound, - ErrorS 'MissingLegalholdConsent, - ErrorS 'MLSClientSenderUserMismatch, - ErrorS 'MLSCommitMissingReferences, - ErrorS 'MLSGroupConversationMismatch, - ErrorS 'MLSMissingSenderClient, - ErrorS 'MLSNotEnabled, - ErrorS 'MLSProposalNotFound, - ErrorS 'MLSSelfRemovalNotAllowed, - ErrorS 'MLSStaleMessage, - ErrorS 'MLSUnsupportedMessage, - Input (Local ()), - ProposalStore, - Resource, - TinyLog - ] - r, - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "send-mls-message", - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Brig "get-mls-clients" + ( Member (Error FederationError) r, + Member (Error InternalError) r, + Member (ErrorS 'ConvAccessDenied) r, + Member (ErrorS 'ConvMemberNotFound) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'MissingLegalholdConsent) r, + Member (ErrorS 'MLSClientSenderUserMismatch) r, + Member (ErrorS 'MLSCommitMissingReferences) r, + Member (ErrorS 'MLSGroupConversationMismatch) r, + Member (ErrorS 'MLSMissingSenderClient) r, + Member (ErrorS 'MLSNotEnabled) r, + Member (ErrorS 'MLSProposalNotFound) r, + Member (ErrorS 'MLSSelfRemovalNotAllowed) r, + Member (ErrorS 'MLSStaleMessage) r, + Member (ErrorS 'MLSUnsupportedMessage) r, + Member (Input (Local ())) r, + Member ProposalStore r, + Member Resource r, + Member TinyLog r + ) ) => Local UserId -> Maybe ClientId -> @@ -162,33 +155,26 @@ postMLSMessageFromLocalUserV1 lusr mc conn smsg = do postMLSMessageFromLocalUser :: ( HasProposalEffects r, - Members - '[ Error FederationError, - Error InternalError, - ErrorS 'ConvAccessDenied, - ErrorS 'ConvMemberNotFound, - ErrorS 'ConvNotFound, - ErrorS 'MissingLegalholdConsent, - ErrorS 'MLSClientSenderUserMismatch, - ErrorS 'MLSCommitMissingReferences, - ErrorS 'MLSGroupConversationMismatch, - ErrorS 'MLSMissingSenderClient, - ErrorS 'MLSNotEnabled, - ErrorS 'MLSProposalNotFound, - ErrorS 'MLSSelfRemovalNotAllowed, - ErrorS 'MLSStaleMessage, - ErrorS 'MLSUnsupportedMessage, - Input (Local ()), - ProposalStore, - Resource, - TinyLog - ] - r, - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "send-mls-message", - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Brig "get-mls-clients" + ( Member (Error FederationError) r, + Member (Error InternalError) r, + Member (ErrorS 'ConvAccessDenied) r, + Member (ErrorS 'ConvMemberNotFound) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'MissingLegalholdConsent) r, + Member (ErrorS 'MLSClientSenderUserMismatch) r, + Member (ErrorS 'MLSCommitMissingReferences) r, + Member (ErrorS 'MLSGroupConversationMismatch) r, + Member (ErrorS 'MLSMissingSenderClient) r, + Member (ErrorS 'MLSNotEnabled) r, + Member (ErrorS 'MLSProposalNotFound) r, + Member (ErrorS 'MLSSelfRemovalNotAllowed) r, + Member (ErrorS 'MLSStaleMessage) r, + Member (ErrorS 'MLSUnsupportedMessage) r, + Member (Input (Local ())) r, + Member ProposalStore r, + Member Resource r, + Member TinyLog r + ) ) => Local UserId -> Maybe ClientId -> @@ -206,26 +192,18 @@ postMLSMessageFromLocalUser lusr mc conn msg = do postMLSCommitBundle :: ( HasProposalEffects r, Members MLSBundleStaticErrors r, - Members - '[ BrigAccess, - Error FederationError, - Error InternalError, - Error MLSProtocolError, - Input (Local ()), - Input Opts, - Input UTCTime, - MemberStore, - ProposalStore, - Resource, - TinyLog - ] - r, - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "mls-welcome", - CallsFed 'Galley "send-mls-commit-bundle", - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Brig "get-mls-clients" + ( Member BrigAccess r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member (Error MLSProtocolError) r, + Member (Input (Local ())) r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member MemberStore r, + Member ProposalStore r, + Member Resource r, + Member TinyLog r + ) ) => Local x -> Qualified UserId -> @@ -244,26 +222,18 @@ postMLSCommitBundle loc qusr mc qcnv conn rawBundle = postMLSCommitBundleFromLocalUser :: ( HasProposalEffects r, Members MLSBundleStaticErrors r, - Members - '[ BrigAccess, - Error FederationError, - Error InternalError, - ErrorS 'MLSNotEnabled, - Input (Local ()), - Input Opts, - Input UTCTime, - MemberStore, - ProposalStore, - Resource, - TinyLog - ] - r, - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "mls-welcome", - CallsFed 'Galley "send-mls-commit-bundle", - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Brig "get-mls-clients" + ( Member BrigAccess r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member (ErrorS 'MLSNotEnabled) r, + Member (Input (Local ())) r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member MemberStore r, + Member ProposalStore r, + Member Resource r, + Member TinyLog r + ) ) => Local UserId -> Maybe ClientId -> @@ -283,23 +253,16 @@ postMLSCommitBundleFromLocalUser lusr mc conn bundle = do postMLSCommitBundleToLocalConv :: ( HasProposalEffects r, Members MLSBundleStaticErrors r, - Members - '[ BrigAccess, - Error FederationError, - Error InternalError, - Error MLSProtocolError, - Input Opts, - Input UTCTime, - ProposalStore, - Resource, - TinyLog - ] - r, - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "mls-welcome", - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Brig "get-mls-clients" + ( Member BrigAccess r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member (Error MLSProtocolError) r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member ProposalStore r, + Member Resource r, + Member TinyLog r + ) ) => Qualified UserId -> Maybe ClientId -> @@ -354,18 +317,15 @@ postMLSCommitBundleToLocalConv qusr mc conn bundle lcnv = do postMLSCommitBundleToRemoteConv :: ( Members MLSBundleStaticErrors r, - Members - '[ Error FederationError, - Error MLSProtocolError, - Error MLSProposalFailure, - ExternalAccess, - FederatorAccess, - GundeckAccess, - MemberStore, - TinyLog - ] - r, - CallsFed 'Galley "send-mls-commit-bundle" + ( Member (Error FederationError) r, + Member (Error MLSProtocolError) r, + Member (Error MLSProposalFailure) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member MemberStore r, + Member TinyLog r + ) ) => Local x -> Qualified UserId -> @@ -399,33 +359,26 @@ postMLSCommitBundleToRemoteConv loc qusr con bundle rcnv = do postMLSMessage :: ( HasProposalEffects r, - Members - '[ Error FederationError, - Error InternalError, - ErrorS 'ConvAccessDenied, - ErrorS 'ConvMemberNotFound, - ErrorS 'ConvNotFound, - ErrorS 'MLSNotEnabled, - ErrorS 'MissingLegalholdConsent, - ErrorS 'MLSClientSenderUserMismatch, - ErrorS 'MLSCommitMissingReferences, - ErrorS 'MLSGroupConversationMismatch, - ErrorS 'MLSMissingSenderClient, - ErrorS 'MLSProposalNotFound, - ErrorS 'MLSSelfRemovalNotAllowed, - ErrorS 'MLSStaleMessage, - ErrorS 'MLSUnsupportedMessage, - Input (Local ()), - ProposalStore, - Resource, - TinyLog - ] - r, - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "send-mls-message", - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Brig "get-mls-clients" + ( Member (Error FederationError) r, + Member (Error InternalError) r, + Member (ErrorS 'ConvAccessDenied) r, + Member (ErrorS 'ConvMemberNotFound) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'MLSNotEnabled) r, + Member (ErrorS 'MissingLegalholdConsent) r, + Member (ErrorS 'MLSClientSenderUserMismatch) r, + Member (ErrorS 'MLSCommitMissingReferences) r, + Member (ErrorS 'MLSGroupConversationMismatch) r, + Member (ErrorS 'MLSMissingSenderClient) r, + Member (ErrorS 'MLSProposalNotFound) r, + Member (ErrorS 'MLSSelfRemovalNotAllowed) r, + Member (ErrorS 'MLSStaleMessage) r, + Member (ErrorS 'MLSUnsupportedMessage) r, + Member (Input (Local ())) r, + Member ProposalStore r, + Member Resource r, + Member TinyLog r + ) ) => Local x -> Qualified UserId -> @@ -448,12 +401,9 @@ postMLSMessage loc qusr mc qcnv con smsg = case rmValue smsg of -- -- The check is skipped in case of conversation creation and encrypted messages. getSenderClient :: - ( Members - '[ ErrorS 'MLSKeyPackageRefNotFound, - ErrorS 'MLSClientSenderUserMismatch, - BrigAccess - ] - r + ( Member (ErrorS 'MLSKeyPackageRefNotFound) r, + Member (ErrorS 'MLSClientSenderUserMismatch) r, + Member BrigAccess r ) => Qualified UserId -> SWireFormatTag tag -> @@ -473,12 +423,9 @@ getSenderClient qusr SMLSPlainText msg = case msgSender msg of -- 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 + ( Member (ErrorS 'MLSKeyPackageRefNotFound) r, + Member (ErrorS 'MLSClientSenderUserMismatch) r, + Member BrigAccess r ) => Qualified UserId -> Maybe ClientId -> @@ -495,27 +442,21 @@ getSenderIdentity qusr mc fmt msg = do postMLSMessageToLocalConv :: ( HasProposalEffects r, - Members - '[ Error FederationError, - Error InternalError, - ErrorS 'ConvNotFound, - ErrorS 'MissingLegalholdConsent, - ErrorS 'MLSClientSenderUserMismatch, - ErrorS 'MLSCommitMissingReferences, - ErrorS 'MLSMissingSenderClient, - ErrorS 'MLSProposalNotFound, - ErrorS 'MLSSelfRemovalNotAllowed, - ErrorS 'MLSStaleMessage, - ErrorS 'MLSUnsupportedMessage, - ProposalStore, - Resource, - TinyLog - ] - r, - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Brig "get-mls-clients" + ( Member (Error FederationError) r, + Member (Error InternalError) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'MissingLegalholdConsent) r, + Member (ErrorS 'MLSClientSenderUserMismatch) r, + Member (ErrorS 'MLSCommitMissingReferences) r, + Member (ErrorS 'MLSMissingSenderClient) r, + Member (ErrorS 'MLSProposalNotFound) r, + Member (ErrorS 'MLSSelfRemovalNotAllowed) r, + Member (ErrorS 'MLSStaleMessage) r, + Member (ErrorS 'MLSUnsupportedMessage) r, + Member ProposalStore r, + Member Resource r, + Member TinyLog r + ) ) => Qualified UserId -> Maybe ClientId -> @@ -553,9 +494,10 @@ postMLSMessageToLocalConv qusr senderClient con smsg lcnv = case rmValue smsg of postMLSMessageToRemoteConv :: ( Members MLSMessageStaticErrors r, - Members '[Error FederationError, TinyLog] r, - HasProposalEffects r, - CallsFed 'Galley "send-mls-message" + ( Member (Error FederationError) r, + Member TinyLog r + ), + HasProposalEffects r ) => Local x -> Qualified UserId -> @@ -642,14 +584,8 @@ paExternalInitPresent = mempty {paExternalInit = Any True} getCommitData :: ( HasProposalEffects r, Member (ErrorS 'ConvNotFound) r, - Member (Error MLSProtocolError) r, Member (ErrorS 'MLSProposalNotFound) r, - Member (ErrorS 'MLSStaleMessage) r, - Member (Input (Local ())) r, - Member (Input Env) r, - Member (Input Opts) r, - Member (Input UTCTime) r, - Member TinyLog r + Member (ErrorS 'MLSStaleMessage) r ) => Local Data.Conversation -> ConversationMLSData -> @@ -667,8 +603,6 @@ getCommitData lconv mlsMeta epoch commit = do processCommit :: ( HasProposalEffects r, - Member (Error FederationError) r, - Member (Error InternalError) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'MLSClientSenderUserMismatch) r, Member (ErrorS 'MLSCommitMissingReferences) r, @@ -677,14 +611,7 @@ processCommit :: 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, - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Brig "get-mls-clients" + Member Resource r ) => Qualified UserId -> Maybe ClientId -> @@ -702,27 +629,23 @@ processCommit qusr senderClient con lconv mlsMeta cm epoch sender commit = do 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, - CallsFed 'Galley "on-mls-message-sent" + ( Member BrigAccess r, + Member ConversationStore r, + Member (Error MLSProtocolError) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'MLSClientSenderUserMismatch) r, + Member (ErrorS 'MLSKeyPackageRefNotFound) r, + Member (ErrorS 'MLSStaleMessage) r, + Member (ErrorS 'MLSMissingSenderClient) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input UTCTime) r, + Member MemberStore r, + Member ProposalStore r, + Member Resource r, + Member TinyLog r ) => Qualified UserId -> Maybe ClientId -> @@ -815,24 +738,14 @@ processExternalCommit qusr mSenderClient lconv mlsMeta cm epoch action updatePat 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 'MLSMissingSenderClient) 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, - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Brig "get-mls-clients" + Member Resource r ) => Qualified UserId -> Maybe ClientId -> @@ -854,24 +767,13 @@ processCommitWithAction qusr senderClient con lconv mlsMeta cm epoch action send processInternalCommit :: 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 'MLSMissingSenderClient) 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, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Brig "get-mls-clients" + Member Resource r ) => Qualified UserId -> Maybe ClientId -> @@ -958,7 +860,9 @@ processInternalCommit qusr senderClient con lconv mlsMeta cm epoch action sender -- | Note: Use this only for KeyPackage that are already validated updateKeyPackageMapping :: - Members '[BrigAccess, MemberStore] r => + ( Member BrigAccess r, + Member MemberStore r + ) => Local Data.Conversation -> GroupId -> Qualified UserId -> @@ -986,13 +890,11 @@ updateKeyPackageMapping lconv groupId qusr cid mOld new = do applyProposalRef :: ( HasProposalEffects r, - Members - '[ ErrorS 'ConvNotFound, - ErrorS 'MLSProposalNotFound, - ErrorS 'MLSStaleMessage, - ProposalStore - ] - r + ( Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'MLSProposalNotFound) r, + Member (ErrorS 'MLSStaleMessage) r, + Member ProposalStore r + ) ) => Data.Conversation -> ConversationMLSData -> @@ -1056,11 +958,7 @@ applyProposal _conv _groupId (ExternalInitProposal _) = applyProposal _conv _groupId _ = pure mempty checkProposalCipherSuite :: - Members - '[ Error MLSProtocolError, - ProposalStore - ] - r => + Member (Error MLSProtocolError) r => CipherSuiteTag -> Proposal -> Sem r () @@ -1079,14 +977,9 @@ checkProposalCipherSuite _suite _prop = pure () processProposal :: HasProposalEffects r => - Members - '[ Error MLSProtocolError, - ErrorS 'ConvNotFound, - ErrorS 'MLSStaleMessage, - ProposalStore, - Input (Local ()) - ] - r => + ( Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'MLSStaleMessage) r + ) => Qualified UserId -> Data.Conversation -> ConversationMLSData -> @@ -1125,10 +1018,7 @@ processProposal qusr conv mlsMeta msg prop = do storeProposal (msgGroupId msg) (msgEpoch msg) propRef ProposalOriginClient prop checkExternalProposalSignature :: - Members - '[ ErrorS 'MLSUnsupportedProposal - ] - r => + Member (ErrorS 'MLSUnsupportedProposal) r => CipherSuiteTag -> Message 'MLSPlainText -> RawMLS Proposal -> @@ -1147,12 +1037,10 @@ isExternalProposal msg = case msgSender msg of -- check owner/subject of the key package exists and belongs to the user checkExternalProposalUser :: - Members - '[ BrigAccess, - ErrorS 'MLSUnsupportedProposal, - Input (Local ()) - ] - r => + ( Member BrigAccess r, + Member (ErrorS 'MLSUnsupportedProposal) r, + Member (Input (Local ())) r + ) => Qualified UserId -> Proposal -> Sem r () @@ -1188,9 +1076,7 @@ executeProposalAction :: Member ConversationStore r, Member (Error InternalError) r, Member (ErrorS 'ConvNotFound) r, - Member (Error FederationError) r, Member (ErrorS 'MLSClientMismatch) r, - Member (Error MLSProtocolError) r, Member (Error MLSProposalFailure) r, Member (ErrorS 'MissingLegalholdConsent) r, Member (ErrorS 'MLSUnsupportedProposal) r, @@ -1205,11 +1091,7 @@ executeProposalAction :: Member MemberStore r, Member ProposalStore r, Member TeamStore r, - Member TinyLog r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Brig "get-mls-clients" + Member TinyLog r ) => Qualified UserId -> Maybe ConnId -> @@ -1348,8 +1230,8 @@ handleNoChanges :: Monoid a => Sem (Error NoChanges ': r) a -> Sem r a handleNoChanges = fmap fold . runError getClientInfo :: - ( Members '[BrigAccess, FederatorAccess] r, - CallsFed 'Brig "get-mls-clients" + ( Member BrigAccess r, + Member FederatorAccess r ) => Local x -> Qualified UserId -> @@ -1358,8 +1240,7 @@ getClientInfo :: getClientInfo loc = foldQualified loc getLocalMLSClients getRemoteMLSClients getRemoteMLSClients :: - ( Member FederatorAccess r, - CallsFed 'Brig "get-mls-clients" + ( Member FederatorAccess r ) => Remote UserId -> SignatureSchemeTag -> @@ -1374,10 +1255,7 @@ getRemoteMLSClients rusr ss = do -- | Check if the epoch number matches that of a conversation checkEpoch :: - Members - '[ ErrorS 'MLSStaleMessage - ] - r => + Member (ErrorS 'MLSStaleMessage) r => Epoch -> ConversationMLSData -> Sem r () @@ -1437,12 +1315,9 @@ instance withCommitLock :: forall r a. - ( Members - '[ Resource, - ConversationStore, - ErrorS 'MLSStaleMessage - ] - r + ( Member Resource r, + Member ConversationStore r, + Member (ErrorS 'MLSStaleMessage) r ) => GroupId -> Epoch -> diff --git a/services/galley/src/Galley/API/MLS/Propagate.hs b/services/galley/src/Galley/API/MLS/Propagate.hs index 22ca2d9d5e..74fbf8f608 100644 --- a/services/galley/src/Galley/API/MLS/Propagate.hs +++ b/services/galley/src/Galley/API/MLS/Propagate.hs @@ -52,8 +52,7 @@ propagateMessage :: Member FederatorAccess r, Member GundeckAccess r, Member (Input UTCTime) r, - Member TinyLog r, - CallsFed 'Galley "on-mls-message-sent" + Member TinyLog r ) => Qualified UserId -> Local Data.Conversation -> diff --git a/services/galley/src/Galley/API/MLS/Removal.hs b/services/galley/src/Galley/API/MLS/Removal.hs index d06971da27..ec1b6529e5 100644 --- a/services/galley/src/Galley/API/MLS/Removal.hs +++ b/services/galley/src/Galley/API/MLS/Removal.hs @@ -28,7 +28,6 @@ import qualified Data.Map as Map import Data.Qualified import qualified Data.Set as Set import Data.Time -import Galley.API.Error import Galley.API.MLS.Keys (getMLSRemovalKey) import Galley.API.MLS.Propagate import Galley.API.MLS.Types @@ -39,12 +38,10 @@ import Galley.Effects.ProposalStore import Galley.Env import Imports import Polysemy -import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog import qualified System.Logger as Log import Wire.API.Conversation.Protocol -import Wire.API.Federation.API import Wire.API.MLS.KeyPackage import Wire.API.MLS.Message import Wire.API.MLS.Proposal @@ -52,18 +49,14 @@ import Wire.API.MLS.Serialisation -- | Send remove proposals for a set of clients to clients in the ClientMap. removeClientsWithClientMap :: - ( Members - '[ Input UTCTime, - TinyLog, - ExternalAccess, - FederatorAccess, - GundeckAccess, - ProposalStore, - Input Env - ] - r, - Traversable t, - CallsFed 'Galley "on-mls-message-sent" + ( Member (Input UTCTime) r, + Member TinyLog r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member ProposalStore r, + Member (Input Env) r, + Traversable t ) => Local Data.Conversation -> t KeyPackageRef -> @@ -93,19 +86,14 @@ removeClientsWithClientMap lc cs cm qusr = do -- | Send remove proposals for a single client of a user to the local conversation. removeClient :: - ( Members - '[ Error InternalError, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input UTCTime, - MemberStore, - ProposalStore, - TinyLog - ] - r, - CallsFed 'Galley "on-mls-message-sent" + ( Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input UTCTime) r, + Member MemberStore r, + Member ProposalStore r, + Member TinyLog r ) => Local Data.Conversation -> Qualified UserId -> @@ -121,18 +109,13 @@ removeClient lc qusr cid = do -- -- All clients of the user have to be contained in the ClientMap. removeUserWithClientMap :: - ( Members - '[ Input UTCTime, - TinyLog, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Error InternalError, - ProposalStore, - Input Env - ] - r, - CallsFed 'Galley "on-mls-message-sent" + ( Member (Input UTCTime) r, + Member TinyLog r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member ProposalStore r, + Member (Input Env) r ) => Local Data.Conversation -> ClientMap -> @@ -143,19 +126,14 @@ removeUserWithClientMap lc cm qusr = -- | Send remove proposals for all clients of the user to the local conversation. removeUser :: - ( Members - '[ Error InternalError, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input UTCTime, - MemberStore, - ProposalStore, - TinyLog - ] - r, - CallsFed 'Galley "on-mls-message-sent" + ( Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input UTCTime) r, + Member MemberStore r, + Member ProposalStore r, + Member TinyLog r ) => Local Data.Conversation -> Qualified UserId -> diff --git a/services/galley/src/Galley/API/MLS/Util.hs b/services/galley/src/Galley/API/MLS/Util.hs index 1095e1ef62..a678de4440 100644 --- a/services/galley/src/Galley/API/MLS/Util.hs +++ b/services/galley/src/Galley/API/MLS/Util.hs @@ -40,12 +40,10 @@ import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation getLocalConvForUser :: - Members - '[ ErrorS 'ConvNotFound, - ConversationStore, - MemberStore - ] - r => + ( Member (ErrorS 'ConvNotFound) r, + Member ConversationStore r, + Member MemberStore r + ) => Qualified UserId -> Local ConvId -> Sem r Data.Conversation @@ -59,7 +57,9 @@ getLocalConvForUser qusr lcnv = do pure conv getPendingBackendRemoveProposals :: - Members '[ProposalStore, TinyLog] r => + ( Member ProposalStore r, + Member TinyLog r + ) => GroupId -> Epoch -> Sem r [KeyPackageRef] diff --git a/services/galley/src/Galley/API/MLS/Welcome.hs b/services/galley/src/Galley/API/MLS/Welcome.hs index 84c67b31f9..936855d15a 100644 --- a/services/galley/src/Galley/API/MLS/Welcome.hs +++ b/services/galley/src/Galley/API/MLS/Welcome.hs @@ -55,16 +55,12 @@ import Wire.API.MLS.Welcome import Wire.API.Message postMLSWelcome :: - ( Members - '[ BrigAccess, - FederatorAccess, - GundeckAccess, - ErrorS 'MLSKeyPackageRefNotFound, - Input UTCTime, - P.TinyLog - ] - r, - CallsFed 'Galley "mls-welcome" + ( Member BrigAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (ErrorS 'MLSKeyPackageRefNotFound) r, + Member (Input UTCTime) r, + Member P.TinyLog r ) => Local x -> Maybe ConnId -> @@ -78,18 +74,14 @@ postMLSWelcome loc con wel = do sendRemoteWelcomes (rmRaw wel) remotes postMLSWelcomeFromLocalUser :: - ( Members - '[ BrigAccess, - FederatorAccess, - GundeckAccess, - ErrorS 'MLSKeyPackageRefNotFound, - ErrorS 'MLSNotEnabled, - Input UTCTime, - Input Env, - P.TinyLog - ] - r, - CallsFed 'Galley "mls-welcome" + ( Member BrigAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (ErrorS 'MLSKeyPackageRefNotFound) r, + Member (ErrorS 'MLSNotEnabled) r, + Member (Input UTCTime) r, + Member (Input Env) r, + Member P.TinyLog r ) => Local x -> ConnId -> @@ -100,11 +92,9 @@ postMLSWelcomeFromLocalUser loc con wel = do postMLSWelcome loc (Just con) wel welcomeRecipients :: - Members - '[ BrigAccess, - ErrorS 'MLSKeyPackageRefNotFound - ] - r => + ( Member BrigAccess r, + Member (ErrorS 'MLSKeyPackageRefNotFound) r + ) => Welcome -> Sem r [Qualified (UserId, ClientId)] welcomeRecipients = @@ -116,7 +106,7 @@ welcomeRecipients = . welSecrets sendLocalWelcomes :: - Members '[GundeckAccess] r => + Member GundeckAccess r => Maybe ConnId -> UTCTime -> ByteString -> @@ -135,12 +125,8 @@ sendLocalWelcomes con now rawWelcome lclients = do in newMessagePush lclients mempty con defMessageMetadata (u, c) e sendRemoteWelcomes :: - ( Members - '[ FederatorAccess, - P.TinyLog - ] - r, - CallsFed 'Galley "mls-welcome" + ( Member FederatorAccess r, + Member P.TinyLog r ) => ByteString -> [Remote (UserId, ClientId)] -> diff --git a/services/galley/src/Galley/API/Mapping.hs b/services/galley/src/Galley/API/Mapping.hs index d6bd3d4392..ac049cd17f 100644 --- a/services/galley/src/Galley/API/Mapping.hs +++ b/services/galley/src/Galley/API/Mapping.hs @@ -36,7 +36,7 @@ import Polysemy import Polysemy.Error import qualified Polysemy.TinyLog as P import System.Logger.Message (msg, val, (+++)) -import Wire.API.Conversation +import Wire.API.Conversation hiding (Member) import qualified Wire.API.Conversation as Conversation import Wire.API.Federation.API.Galley @@ -44,7 +44,9 @@ import Wire.API.Federation.API.Galley -- -- Throws @BadMemberState@ when the user is not part of the conversation. conversationView :: - Members '[Error InternalError, P.TinyLog] r => + ( Member (Error InternalError) r, + Member P.TinyLog r + ) => Local UserId -> Data.Conversation -> Sem r Conversation @@ -57,7 +59,9 @@ conversationView luid conv = do -- from pre-computing the list of @OtherMember@s in the conversation. For -- instance, creating @ConvesationView@ for more than 1 member of the same conversation. conversationViewWithCachedOthers :: - Members '[Error InternalError, P.TinyLog] r => + ( Member (Error InternalError) r, + Member P.TinyLog r + ) => [OtherMember] -> [OtherMember] -> Data.Conversation -> diff --git a/services/galley/src/Galley/API/Message.hs b/services/galley/src/Galley/API/Message.hs index 8f0f2bb178..5ed9bdbde3 100644 --- a/services/galley/src/Galley/API/Message.hs +++ b/services/galley/src/Galley/API/Message.hs @@ -77,6 +77,7 @@ import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Brig import Wire.API.Federation.API.Galley +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.Message import Wire.API.Routes.Public.Galley.Messaging @@ -214,7 +215,7 @@ checkMessageClients sender participantMap recipientMap mismatchStrat = ) getRemoteClients :: - (Member FederatorAccess r, CallsFed 'Brig "get-user-clients") => + (Member FederatorAccess r) => [RemoteMember] -> Sem r (Map (Domain, UserId) (Set ClientId)) getRemoteClients remoteMembers = @@ -222,13 +223,14 @@ getRemoteClients remoteMembers = mconcat . map tUnqualified <$> runFederatedConcurrently (map rmId remoteMembers) getRemoteClientsFromDomain where + getRemoteClientsFromDomain :: Remote [UserId] -> FederatorClient 'Brig (Map (Domain, UserId) (Set ClientId)) getRemoteClientsFromDomain (tUntagged -> Qualified uids domain) = Map.mapKeys (domain,) . fmap (Set.map pubClientId) . userMap <$> fedClient @'Brig @"get-user-clients" (GetUserClients uids) -- FUTUREWORK: sender should be Local UserId postRemoteOtrMessage :: - (Members '[FederatorAccess] r, CallsFed 'Galley "send-message") => + (Member FederatorAccess r) => Qualified UserId -> Remote ConvId -> ByteString -> @@ -244,19 +246,17 @@ postRemoteOtrMessage sender conv rawMsg = do msResponse <$> runFederated conv rpc postBroadcast :: - Members - '[ BrigAccess, - ClientStore, - ErrorS 'TeamNotFound, - ErrorS 'NonBindingTeam, - ErrorS 'BroadcastLimitExceeded, - GundeckAccess, - Input Opts, - Input UTCTime, - TeamStore, - P.TinyLog - ] - r => + ( Member BrigAccess r, + Member ClientStore r, + Member (ErrorS 'TeamNotFound) r, + Member (ErrorS 'NonBindingTeam) r, + Member (ErrorS 'BroadcastLimitExceeded) r, + Member GundeckAccess r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member TeamStore r, + Member P.TinyLog r + ) => Local UserId -> Maybe ConnId -> QualifiedNewOtrMessage -> @@ -334,7 +334,9 @@ postBroadcast lusr con msg = runError $ do pure otrResult {mssFailedToSend = failedToSend} where maybeFetchLimitedTeamMemberList :: - Members '[ErrorS 'BroadcastLimitExceeded, TeamStore] r => + ( Member (ErrorS 'BroadcastLimitExceeded) r, + Member TeamStore r + ) => Int -> TeamId -> [UserId] -> @@ -347,7 +349,9 @@ postBroadcast lusr con msg = runError $ do throwS @'BroadcastLimitExceeded selectTeamMembers tid localUserIdsToLookup maybeFetchAllMembersInTeam :: - Members '[ErrorS 'BroadcastLimitExceeded, TeamStore] r => + ( Member (ErrorS 'BroadcastLimitExceeded) r, + Member TeamStore r + ) => TeamId -> Sem r [TeamMember] maybeFetchAllMembersInTeam tid = do @@ -357,23 +361,16 @@ postBroadcast lusr con msg = runError $ do pure (mems ^. teamMembers) postQualifiedOtrMessage :: - ( Members - '[ BrigAccess, - ClientStore, - ConversationStore, - FederatorAccess, - GundeckAccess, - ExternalAccess, - Input (Local ()), -- FUTUREWORK: remove this - Input Opts, - Input UTCTime, - MemberStore, - TeamStore, - P.TinyLog - ] - r, - CallsFed 'Galley "on-message-sent", - CallsFed 'Brig "get-user-clients" + ( Member BrigAccess r, + Member ClientStore r, + Member ConversationStore r, + Member FederatorAccess r, + Member GundeckAccess r, + Member ExternalAccess r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member TeamStore r, + Member P.TinyLog r ) => UserType -> Qualified UserId -> @@ -476,8 +473,11 @@ makeUserMap keys = (<> Map.fromSet (const mempty) keys) sendMessages :: forall t r. ( t ~ 'NormalMessage, - Members '[GundeckAccess, ExternalAccess, FederatorAccess, P.TinyLog] r, - CallsFed 'Galley "on-message-sent" + ( Member GundeckAccess r, + Member ExternalAccess r, + Member FederatorAccess r, + Member P.TinyLog r + ) ) => UTCTime -> Qualified UserId -> @@ -499,7 +499,7 @@ sendMessages now sender senderClient mconn lcnv botMap metadata messages = do mkQualifiedUserClientsByDomain <$> Map.traverseWithKey send messageMap sendBroadcastMessages :: - Members '[GundeckAccess, P.TinyLog] r => + Member GundeckAccess r => Local x -> UTCTime -> Qualified UserId -> @@ -553,9 +553,14 @@ sendLocalMessages loc now sender senderClient mconn qcnv botMap metadata localMe runMessagePush @t loc qcnv (pushes ^. traversed) pure mempty +-- | Send remote messages to the backend given by the domain argument, and +-- return the set of clients for which sending has failed. In case there was no +-- failure, the empty set is returned. sendRemoteMessages :: forall r x. - (Members '[FederatorAccess, P.TinyLog] r, CallsFed 'Galley "on-message-sent") => + ( Member FederatorAccess r, + Member P.TinyLog r + ) => Remote x -> UTCTime -> Qualified UserId -> diff --git a/services/galley/src/Galley/API/One2One.hs b/services/galley/src/Galley/API/One2One.hs index dfdc2aec68..c2a98414ad 100644 --- a/services/galley/src/Galley/API/One2One.hs +++ b/services/galley/src/Galley/API/One2One.hs @@ -29,14 +29,14 @@ import Galley.Data.Conversation import Galley.Data.Conversation.Types import Galley.Effects.ConversationStore import Galley.Effects.MemberStore -import Galley.Types.Conversations.Intra (Actor (..), DesiredMembership (..), UpsertOne2OneConversationRequest (..), UpsertOne2OneConversationResponse (..)) import Galley.Types.Conversations.One2One (one2OneConvId) import Galley.Types.ToUserRole import Galley.Types.UserList import Imports import Polysemy -import Wire.API.Conversation +import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Protocol +import Wire.API.Routes.Internal.Galley.ConversationsIntra (Actor (..), DesiredMembership (..), UpsertOne2OneConversationRequest (..), UpsertOne2OneConversationResponse (..)) newConnectConversationWithRemote :: Local UserId -> @@ -54,7 +54,9 @@ newConnectConversationWithRemote creator users = iUpsertOne2OneConversation :: forall r. - Members '[ConversationStore, MemberStore] r => + ( Member ConversationStore r, + Member MemberStore r + ) => UpsertOne2OneConversationRequest -> Sem r UpsertOne2OneConversationResponse iUpsertOne2OneConversation UpsertOne2OneConversationRequest {..} = do diff --git a/services/galley/src/Galley/API/Public.hs b/services/galley/src/Galley/API/Public.hs index d4e84f32fe..845a3674b0 100644 --- a/services/galley/src/Galley/API/Public.hs +++ b/services/galley/src/Galley/API/Public.hs @@ -17,25 +17,17 @@ module Galley.API.Public ( sitemap, - apiDocs, filterMissing, -- for tests continueE, ) where -import Data.Aeson (encode) import Data.ByteString.Conversion (fromByteString, fromList) import Data.Id import qualified Data.Predicate as P import Data.Qualified -import Data.Range import qualified Data.Set as Set -import Data.Swagger.Build.Api hiding (Response, def, min) -import qualified Data.Swagger.Build.Api as Swagger -import Data.Text.Encoding (decodeLatin1) -import qualified Galley.API.Error as Error import qualified Galley.API.Query as Query -import qualified Galley.API.Teams as Teams import qualified Galley.API.Teams.Features as Features import Galley.App import Galley.Cassandra.TeamFeatures @@ -50,8 +42,6 @@ import Network.Wai.Predicate hiding (Error, or, result, setStatus) import qualified Network.Wai.Predicate as P import Network.Wai.Predicate.Request (HasQuery) import Network.Wai.Routing hiding (route) -import Network.Wai.Utilities hiding (Error) -import Network.Wai.Utilities.Swagger import Network.Wai.Utilities.ZAuth hiding (ZAuthUser) import Polysemy import Polysemy.Error @@ -62,9 +52,7 @@ import Wire.API.Error import Wire.API.Error.Galley import qualified Wire.API.Event.Team as Public () import qualified Wire.API.Message as Public -import qualified Wire.API.Notification as Public import Wire.API.Routes.API -import qualified Wire.API.Swagger as Public.Swagger (models) import Wire.API.Team.Feature -- These are all the errors that can be thrown by wai-routing handlers. @@ -108,51 +96,8 @@ continueE :: Sem r ResponseReceived continueE h = continue (interpretServerEffects @ErrorEffects . h) -errorSResponse :: forall e. KnownError (MapError e) => OperationBuilder -errorSResponse = errorResponse (toWai (dynError @(MapError e))) - -sitemap :: Routes ApiBuilder (Sem GalleyEffects) () +sitemap :: Routes () (Sem GalleyEffects) () sitemap = do - get "/teams/notifications" (continueE Teams.getTeamNotificationsH) $ - zauthUserId - .&. opt (query "since") - .&. def (unsafeRange 1000) (query "size") - .&. accept "application" "json" - document "GET" "getTeamNotifications" $ do - summary "Read recently added team members from team queue" - notes - "This is a work-around for scalability issues with gundeck user event fan-out. \ - \It does not track all team-wide events, but only `member-join`.\ - \\n\ - \Note that `/teams/notifications` behaves different from `/notifications`:\ - \\n\ - \- If there is a gap between the notification id requested with `since` and the \ - \available data, team queues respond with 200 and the data that could be found. \ - \The do NOT respond with status 404, but valid data in the body.\ - \\n\ - \- The notification with the id given via `since` is included in the \ - \response if it exists. You should remove this and only use it to decide whether \ - \there was a gap between your last request and this one.\ - \\n\ - \- If the notification id does *not* exist, you get the more recent events from the queue \ - \(instead of all of them). This can be done because a notification id is a UUIDv1, which \ - \is essentially a time stamp.\ - \\n\ - \- There is no corresponding `/last` end-point to get only the most recent event. \ - \That end-point was only useful to avoid having to pull the entire queue. In team \ - \queues, if you have never requested the queue before and \ - \have no prior notification id, just pull with timestamp 'now'." - parameter Query "since" bytes' $ do - optional - description "Notification id to start with in the response (UUIDv1)" - parameter Query "size" (int32 (Swagger.def 1000)) $ do - optional - description "Maximum number of events to return (1..10000; default: 1000)" - returns (ref Public.modelNotificationList) - response 200 "List of team notifications" end - errorSResponse @'TeamNotFound - errorResponse Error.invalidTeamNotificationId - -- Bot API ------------------------------------------------------------ get "/bot/conversation" (continueE (getBotConversationH @Cassandra)) $ @@ -180,20 +125,8 @@ getBotConversationH :: getBotConversationH arg@(bid ::: cid ::: _) = Features.guardSecondFactorDisabled @db (botUserId bid) cid (Query.getBotConversationH arg) -apiDocs :: Routes ApiBuilder (Sem r) () -apiDocs = - get "/conversations/api-docs" (continue docs) $ - accept "application" "json" - .&. query "base_url" - type JSON = Media "application" "json" -docs :: JSON ::: ByteString -> Sem r Response -docs (_ ::: url) = do - let models = Public.Swagger.models - let apidoc = encode $ mkSwaggerApi (decodeLatin1 url) models sitemap - pure $ responseLBS status200 [jsonContent] apidoc - -- FUTUREWORK: Maybe would be better to move it to wire-api? filterMissing :: HasQuery r => Predicate r P.Error Public.OtrFilterMissing filterMissing = (>>= go) <$> (query "ignore_missing" ||| query "report_missing") diff --git a/services/galley/src/Galley/API/Public/Bot.hs b/services/galley/src/Galley/API/Public/Bot.hs index 06ea1f89fa..742008a9fe 100644 --- a/services/galley/src/Galley/API/Public/Bot.hs +++ b/services/galley/src/Galley/API/Public/Bot.hs @@ -24,4 +24,4 @@ import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.Bot botAPI :: API BotAPI GalleyEffects -botAPI = mkNamedAPI @"post-bot-message-unqualified" (callsFed (callsFed postBotMessageUnqualified)) +botAPI = mkNamedAPI @"post-bot-message-unqualified" (callsFed (exposeAnnotations postBotMessageUnqualified)) diff --git a/services/galley/src/Galley/API/Public/Conversation.hs b/services/galley/src/Galley/API/Public/Conversation.hs index 800c9f4654..73bb3cb575 100644 --- a/services/galley/src/Galley/API/Public/Conversation.hs +++ b/services/galley/src/Galley/API/Public/Conversation.hs @@ -32,20 +32,20 @@ conversationAPI :: API ConversationAPI GalleyEffects conversationAPI = mkNamedAPI @"get-unqualified-conversation" getUnqualifiedConversation <@> mkNamedAPI @"get-unqualified-conversation-legalhold-alias" getUnqualifiedConversation - <@> mkNamedAPI @"get-conversation@v2" (callsFed getConversation) - <@> mkNamedAPI @"get-conversation" (callsFed getConversation) + <@> mkNamedAPI @"get-conversation@v2" (callsFed (exposeAnnotations getConversation)) + <@> mkNamedAPI @"get-conversation" (callsFed (exposeAnnotations getConversation)) <@> mkNamedAPI @"get-conversation-roles" getConversationRoles - <@> mkNamedAPI @"get-group-info" (callsFed getGroupInfo) + <@> mkNamedAPI @"get-group-info" (callsFed (exposeAnnotations 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" (callsFed listConversations) - <@> mkNamedAPI @"list-conversations@v2" (callsFed listConversations) - <@> mkNamedAPI @"list-conversations" (callsFed listConversations) + <@> mkNamedAPI @"list-conversations@v1" (callsFed (exposeAnnotations listConversations)) + <@> mkNamedAPI @"list-conversations@v2" (callsFed (exposeAnnotations listConversations)) + <@> mkNamedAPI @"list-conversations" (callsFed (exposeAnnotations listConversations)) <@> mkNamedAPI @"get-conversation-by-reusable-code" (getConversationByReusableCode @Cassandra) - <@> mkNamedAPI @"create-group-conversation@v2" (callsFed createGroupConversation) - <@> mkNamedAPI @"create-group-conversation" (callsFed createGroupConversation) + <@> mkNamedAPI @"create-group-conversation@v2" (callsFed (exposeAnnotations createGroupConversation)) + <@> mkNamedAPI @"create-group-conversation" (callsFed (exposeAnnotations createGroupConversation)) <@> mkNamedAPI @"create-self-conversation@v2" createProteusSelfConversation <@> mkNamedAPI @"create-self-conversation" createProteusSelfConversation <@> mkNamedAPI @"get-mls-self-conversation" getMLSSelfConversationWithError @@ -54,29 +54,29 @@ conversationAPI = <@> mkNamedAPI @"add-members-to-conversation-unqualified" (callsFed addMembersUnqualified) <@> mkNamedAPI @"add-members-to-conversation-unqualified2" (callsFed addMembersUnqualifiedV2) <@> mkNamedAPI @"add-members-to-conversation" (callsFed addMembers) - <@> mkNamedAPI @"join-conversation-by-id-unqualified" (callsFed (joinConversationById @Cassandra)) + <@> mkNamedAPI @"join-conversation-by-id-unqualified" (callsFed joinConversationById) <@> mkNamedAPI @"join-conversation-by-code-unqualified" (callsFed (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 @"member-typing-qualified" (callsFed isTypingQualified) - <@> mkNamedAPI @"remove-member-unqualified" (callsFed removeMemberUnqualified) - <@> mkNamedAPI @"remove-member" (callsFed removeMemberQualified) - <@> mkNamedAPI @"update-other-member-unqualified" (callsFed updateOtherMemberUnqualified) - <@> mkNamedAPI @"update-other-member" (callsFed updateOtherMember) - <@> mkNamedAPI @"update-conversation-name-deprecated" (callsFed updateUnqualifiedConversationName) - <@> mkNamedAPI @"update-conversation-name-unqualified" (callsFed updateUnqualifiedConversationName) - <@> mkNamedAPI @"update-conversation-name" (callsFed updateConversationName) - <@> mkNamedAPI @"update-conversation-message-timer-unqualified" (callsFed updateConversationMessageTimerUnqualified) - <@> mkNamedAPI @"update-conversation-message-timer" (callsFed updateConversationMessageTimer) - <@> mkNamedAPI @"update-conversation-receipt-mode-unqualified" (callsFed updateConversationReceiptModeUnqualified) - <@> mkNamedAPI @"update-conversation-receipt-mode" (callsFed updateConversationReceiptMode) - <@> mkNamedAPI @"update-conversation-access-unqualified" (callsFed updateConversationAccessUnqualified) - <@> mkNamedAPI @"update-conversation-access@v2" (callsFed updateConversationAccess) - <@> mkNamedAPI @"update-conversation-access" (callsFed updateConversationAccess) + <@> mkNamedAPI @"member-typing-unqualified" (callsFed (exposeAnnotations memberTypingUnqualified)) + <@> mkNamedAPI @"member-typing-qualified" (callsFed (exposeAnnotations memberTyping)) + <@> mkNamedAPI @"remove-member-unqualified" (callsFed (exposeAnnotations removeMemberUnqualified)) + <@> mkNamedAPI @"remove-member" (callsFed (exposeAnnotations removeMemberQualified)) + <@> mkNamedAPI @"update-other-member-unqualified" (callsFed (exposeAnnotations updateOtherMemberUnqualified)) + <@> mkNamedAPI @"update-other-member" (callsFed (exposeAnnotations updateOtherMember)) + <@> mkNamedAPI @"update-conversation-name-deprecated" (callsFed (exposeAnnotations updateUnqualifiedConversationName)) + <@> mkNamedAPI @"update-conversation-name-unqualified" (callsFed (exposeAnnotations updateUnqualifiedConversationName)) + <@> mkNamedAPI @"update-conversation-name" (callsFed (exposeAnnotations updateConversationName)) + <@> mkNamedAPI @"update-conversation-message-timer-unqualified" (callsFed (exposeAnnotations updateConversationMessageTimerUnqualified)) + <@> mkNamedAPI @"update-conversation-message-timer" (callsFed (exposeAnnotations updateConversationMessageTimer)) + <@> mkNamedAPI @"update-conversation-receipt-mode-unqualified" (callsFed (exposeAnnotations updateConversationReceiptModeUnqualified)) + <@> mkNamedAPI @"update-conversation-receipt-mode" (callsFed (exposeAnnotations updateConversationReceiptMode)) + <@> mkNamedAPI @"update-conversation-access-unqualified" (callsFed (exposeAnnotations updateConversationAccessUnqualified)) + <@> mkNamedAPI @"update-conversation-access@v2" (callsFed (exposeAnnotations updateConversationAccess)) + <@> mkNamedAPI @"update-conversation-access" (callsFed (exposeAnnotations 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/Feature.hs b/services/galley/src/Galley/API/Public/Feature.hs index 4dbc810de6..028c3cfc1a 100644 --- a/services/galley/src/Galley/API/Public/Feature.hs +++ b/services/galley/src/Galley/API/Public/Feature.hs @@ -31,13 +31,13 @@ featureAPI :: API FeatureAPI GalleyEffects featureAPI = mkNamedAPI @'("get", SSOConfig) (getFeatureStatus @Cassandra . DoAuth) <@> mkNamedAPI @'("get", LegalholdConfig) (getFeatureStatus @Cassandra . DoAuth) - <@> mkNamedAPI @'("put", LegalholdConfig) (callsFed (setFeatureStatus @Cassandra . DoAuth)) + <@> mkNamedAPI @'("put", LegalholdConfig) (callsFed (exposeAnnotations (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 @"set-search-visibility" (setSearchVisibility (featureEnabledForTeam @Cassandra @SearchVisibilityAvailableConfig)) <@> mkNamedAPI @'("get", ValidateSAMLEmailsConfig) (getFeatureStatus @Cassandra . DoAuth) <@> mkNamedAPI @'("get-deprecated", ValidateSAMLEmailsConfig) (getFeatureStatus @Cassandra . DoAuth) <@> mkNamedAPI @'("get", DigitalSignaturesConfig) (getFeatureStatus @Cassandra . DoAuth) @@ -60,6 +60,10 @@ featureAPI = <@> mkNamedAPI @'("put", ExposeInvitationURLsToTeamAdminConfig) (setFeatureStatus @Cassandra . DoAuth) <@> mkNamedAPI @'("get", SearchVisibilityInboundConfig) (getFeatureStatus @Cassandra . DoAuth) <@> mkNamedAPI @'("put", SearchVisibilityInboundConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", OutlookCalIntegrationConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("put", OutlookCalIntegrationConfig) (setFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("get", MlsE2EIdConfig) (getFeatureStatus @Cassandra . DoAuth) + <@> mkNamedAPI @'("put", MlsE2EIdConfig) (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) diff --git a/services/galley/src/Galley/API/Public/LegalHold.hs b/services/galley/src/Galley/API/Public/LegalHold.hs index 405d3ca61a..ef64ab8e4f 100644 --- a/services/galley/src/Galley/API/Public/LegalHold.hs +++ b/services/galley/src/Galley/API/Public/LegalHold.hs @@ -28,9 +28,9 @@ legalHoldAPI :: API LegalHoldAPI GalleyEffects legalHoldAPI = mkNamedAPI @"create-legal-hold-settings" (createSettings @Cassandra) <@> mkNamedAPI @"get-legal-hold-settings" (getSettings @Cassandra) - <@> mkNamedAPI @"delete-legal-hold-settings" (callsFed (callsFed (callsFed (removeSettingsInternalPaging @Cassandra)))) + <@> mkNamedAPI @"delete-legal-hold-settings" (callsFed (exposeAnnotations (removeSettingsInternalPaging @Cassandra))) <@> mkNamedAPI @"get-legal-hold" getUserStatus - <@> mkNamedAPI @"consent-to-legal-hold" (callsFed (callsFed (callsFed grantConsent))) - <@> mkNamedAPI @"request-legal-hold-device" (callsFed (callsFed (callsFed (requestDevice @Cassandra)))) - <@> mkNamedAPI @"disable-legal-hold-for-user" (callsFed (callsFed (callsFed disableForUser))) - <@> mkNamedAPI @"approve-legal-hold-device" (callsFed (callsFed (callsFed (approveDevice @Cassandra)))) + <@> mkNamedAPI @"consent-to-legal-hold" (callsFed (exposeAnnotations grantConsent)) + <@> mkNamedAPI @"request-legal-hold-device" (callsFed (exposeAnnotations (requestDevice @Cassandra))) + <@> mkNamedAPI @"disable-legal-hold-for-user" (callsFed (exposeAnnotations disableForUser)) + <@> mkNamedAPI @"approve-legal-hold-device" (callsFed (exposeAnnotations (approveDevice @Cassandra))) diff --git a/services/galley/src/Galley/API/Public/MLS.hs b/services/galley/src/Galley/API/Public/MLS.hs index 7581908ccf..73187b06da 100644 --- a/services/galley/src/Galley/API/Public/MLS.hs +++ b/services/galley/src/Galley/API/Public/MLS.hs @@ -25,8 +25,8 @@ import Wire.API.Routes.Public.Galley.MLS mlsAPI :: API MLSAPI GalleyEffects mlsAPI = - mkNamedAPI @"mls-welcome-message" (callsFed postMLSWelcomeFromLocalUser) - <@> mkNamedAPI @"mls-message-v1" (callsFed postMLSMessageFromLocalUserV1) - <@> mkNamedAPI @"mls-message" (callsFed postMLSMessageFromLocalUser) - <@> mkNamedAPI @"mls-commit-bundle" (callsFed postMLSCommitBundleFromLocalUser) + mkNamedAPI @"mls-welcome-message" (callsFed (exposeAnnotations postMLSWelcomeFromLocalUser)) + <@> mkNamedAPI @"mls-message-v1" (callsFed (exposeAnnotations postMLSMessageFromLocalUserV1)) + <@> mkNamedAPI @"mls-message" (callsFed (exposeAnnotations postMLSMessageFromLocalUser)) + <@> mkNamedAPI @"mls-commit-bundle" (callsFed (exposeAnnotations 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 index ae5a3248d9..efbbd7482f 100644 --- a/services/galley/src/Galley/API/Public/Messaging.hs +++ b/services/galley/src/Galley/API/Public/Messaging.hs @@ -25,7 +25,7 @@ import Wire.API.Routes.Public.Galley.Messaging messagingAPI :: API MessagingAPI GalleyEffects messagingAPI = - mkNamedAPI @"post-otr-message-unqualified" (callsFed postOtrMessageUnqualified) + mkNamedAPI @"post-otr-message-unqualified" (callsFed (exposeAnnotations postOtrMessageUnqualified)) <@> mkNamedAPI @"post-otr-broadcast-unqualified" postOtrBroadcastUnqualified - <@> mkNamedAPI @"post-proteus-message" (callsFed postProteusMessage) + <@> mkNamedAPI @"post-proteus-message" (callsFed (exposeAnnotations 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 e7eae6adde..86e223c522 100644 --- a/services/galley/src/Galley/API/Public/Servant.hs +++ b/services/galley/src/Galley/API/Public/Servant.hs @@ -27,6 +27,7 @@ import Galley.API.Public.Messaging import Galley.API.Public.Team import Galley.API.Public.TeamConversation import Galley.API.Public.TeamMember +import Galley.API.Public.TeamNotification import Galley.App import Wire.API.Routes.API import Wire.API.Routes.Public.Galley @@ -43,3 +44,4 @@ servantSitemap = <@> customBackendAPI <@> legalHoldAPI <@> teamMemberAPI + <@> teamNotificationAPI diff --git a/services/galley/src/Galley/API/Public/TeamConversation.hs b/services/galley/src/Galley/API/Public/TeamConversation.hs index 6aad651f3b..173d7aba61 100644 --- a/services/galley/src/Galley/API/Public/TeamConversation.hs +++ b/services/galley/src/Galley/API/Public/TeamConversation.hs @@ -28,4 +28,4 @@ teamConversationAPI = mkNamedAPI @"get-team-conversation-roles" getTeamConversationRoles <@> mkNamedAPI @"get-team-conversations" getTeamConversations <@> mkNamedAPI @"get-team-conversation" getTeamConversation - <@> mkNamedAPI @"delete-team-conversation" (callsFed deleteTeamConversation) + <@> mkNamedAPI @"delete-team-conversation" (callsFed (exposeAnnotations deleteTeamConversation)) diff --git a/services/galley/src/Galley/API/Public/TeamNotification.hs b/services/galley/src/Galley/API/Public/TeamNotification.hs new file mode 100644 index 0000000000..6e58383abf --- /dev/null +++ b/services/galley/src/Galley/API/Public/TeamNotification.hs @@ -0,0 +1,54 @@ +module Galley.API.Public.TeamNotification where + +import Data.Id +import Data.Range +import qualified Data.UUID.Util as UUID +import qualified Galley.API.Teams.Notifications as APITeamQueue +import Galley.App +import Galley.Effects +import Imports +import Polysemy +import Wire.API.Error +import Wire.API.Error.Galley +import Wire.API.Internal.Notification +import Wire.API.Routes.API +import Wire.API.Routes.Public.Galley.TeamNotification + +teamNotificationAPI :: API TeamNotificationAPI GalleyEffects +teamNotificationAPI = + mkNamedAPI @"get-team-notifications" getTeamNotifications + +type SizeRange = Range 1 10000 Int32 + +-- | See also: 'Gundeck.API.Public.paginateH', but the semantics of this end-point is slightly +-- less warped. This is a work-around because we cannot send events to all of a large team. +-- See haddocks of module "Galley.API.TeamNotifications" for details. +getTeamNotifications :: + ( Member BrigAccess r, + Member (ErrorS 'TeamNotFound) r, + Member (ErrorS 'InvalidTeamNotificationId) r, + Member TeamNotificationStore r + ) => + UserId -> + Maybe NotificationId -> + Maybe SizeRange -> + Sem r QueuedNotificationList +getTeamNotifications uid since size = do + since' <- checkSince since + APITeamQueue.getTeamNotifications + uid + since' + (fromMaybe defaultSize size) + where + checkSince :: + Member (ErrorS 'InvalidTeamNotificationId) r => + Maybe NotificationId -> + Sem r (Maybe NotificationId) + checkSince Nothing = pure Nothing + checkSince (Just nid) + | (UUID.version . toUUID) nid == 1 = + (pure . Just) nid + checkSince (Just _) = throwS @'InvalidTeamNotificationId + + defaultSize :: SizeRange + defaultSize = unsafeRange 1000 diff --git a/services/galley/src/Galley/API/Push.hs b/services/galley/src/Galley/API/Push.hs index 1e17062e67..0589c2a2f9 100644 --- a/services/galley/src/Galley/API/Push.hs +++ b/services/galley/src/Galley/API/Push.hs @@ -46,7 +46,6 @@ import Galley.Data.Services import Galley.Effects.ExternalAccess import Galley.Effects.GundeckAccess hiding (Push) import Galley.Intra.Push -import Galley.Types.Conversations.Members import Gundeck.Types.Push.V2 (RecipientClients (..)) import Imports import Polysemy @@ -83,10 +82,6 @@ newBotPush b e = NormalMessagePush {userPushes = mempty, botPushes = pure (b, e) type BotMap = Map UserId BotMember -type family LocalMemberMap (t :: MessageType) = (m :: *) | m -> t where - LocalMemberMap 'NormalMessage = Map UserId LocalMember - LocalMemberMap 'Broadcast = () - type family MessagePushEffects (t :: MessageType) :: [Effect] type instance MessagePushEffects 'NormalMessage = '[ExternalAccess, GundeckAccess, TinyLog] @@ -124,7 +119,9 @@ runMessagePush loc mqcnv mp = withSing @t $ \case SBroadcast -> push (broadcastPushes mp) where pushToBots :: - Members '[ExternalAccess, TinyLog] r => + ( Member ExternalAccess r, + Member TinyLog r + ) => [(BotMember, Event)] -> Sem r () pushToBots pushes = for_ mqcnv $ \qcnv -> diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index 9d3be75637..26290f4c1e 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -91,6 +91,7 @@ import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Federation.API import Wire.API.Federation.API.Galley +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import qualified Wire.API.Provider.Bot as Public import qualified Wire.API.Routes.MultiTablePaging as Public @@ -98,7 +99,10 @@ import Wire.API.Team.Feature as Public hiding (setStatus) import Wire.Sem.Paging.Cassandra getBotConversationH :: - Members '[ConversationStore, ErrorS 'ConvNotFound, Input (Local ())] r => + ( Member ConversationStore r, + Member (ErrorS 'ConvNotFound) r, + Member (Input (Local ())) r + ) => BotId ::: ConvId ::: JSON -> Sem r Response getBotConversationH (zbot ::: zcnv ::: _) = do @@ -106,7 +110,9 @@ getBotConversationH (zbot ::: zcnv ::: _) = do json <$> getBotConversation zbot lcnv getBotConversation :: - Members '[ConversationStore, ErrorS 'ConvNotFound] r => + ( Member ConversationStore r, + Member (ErrorS 'ConvNotFound) r + ) => BotId -> Local ConvId -> Sem r Public.BotConvView @@ -124,14 +130,12 @@ getBotConversation zbot lcnv = do Just (OtherMember (Qualified (lmId m) domain) (lmService m) (lmConvRoleName m)) getUnqualifiedConversation :: - Members - '[ ConversationStore, - ErrorS 'ConvNotFound, - ErrorS 'ConvAccessDenied, - Error InternalError, - P.TinyLog - ] - r => + ( Member ConversationStore r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'ConvAccessDenied) r, + Member (Error InternalError) r, + Member P.TinyLog r + ) => Local UserId -> ConvId -> Sem r Public.Conversation @@ -141,17 +145,13 @@ getUnqualifiedConversation lusr cnv = do getConversation :: forall r. - ( Members - '[ ConversationStore, - ErrorS 'ConvNotFound, - ErrorS 'ConvAccessDenied, - Error FederationError, - Error InternalError, - FederatorAccess, - P.TinyLog - ] - r, - CallsFed 'Galley "get-conversations" + ( Member ConversationStore r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'ConvAccessDenied) r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member FederatorAccess r, + Member P.TinyLog r ) => Local UserId -> Qualified ConvId -> @@ -173,15 +173,11 @@ getConversation lusr cnv = do _convs -> throw $ FederationUnexpectedBody "expected one conversation, got multiple" getRemoteConversations :: - ( Members - '[ ConversationStore, - Error FederationError, - ErrorS 'ConvNotFound, - FederatorAccess, - P.TinyLog - ] - r, - CallsFed 'Galley "get-conversations" + ( Member ConversationStore r, + Member (Error FederationError) r, + Member (ErrorS 'ConvNotFound) r, + Member FederatorAccess r, + Member P.TinyLog r ) => Local UserId -> [Remote ConvId] -> @@ -197,7 +193,11 @@ data FailedGetConversationReason | FailedGetConversationRemotely FederationError throwFgcrError :: - Members '[ErrorS 'ConvNotFound, Error FederationError] r => FailedGetConversationReason -> Sem r a + ( Member (ErrorS 'ConvNotFound) r, + Member (Error FederationError) r + ) => + FailedGetConversationReason -> + Sem r a throwFgcrError FailedGetConversationLocally = throwS @'ConvNotFound throwFgcrError (FailedGetConversationRemotely e) = throw e @@ -207,7 +207,11 @@ data FailedGetConversation FailedGetConversationReason throwFgcError :: - Members '[ErrorS 'ConvNotFound, Error FederationError] r => FailedGetConversation -> Sem r a + ( Member (ErrorS 'ConvNotFound) r, + Member (Error FederationError) r + ) => + FailedGetConversation -> + Sem r a throwFgcError (FailedGetConversation _ r) = throwFgcrError r failedGetConversationRemotely :: @@ -228,8 +232,9 @@ partitionGetConversationFailures = bimap concat concat . partitionEithers . map split (FailedGetConversation convs (FailedGetConversationRemotely _)) = Right convs getRemoteConversationsWithFailures :: - ( Members '[ConversationStore, FederatorAccess, P.TinyLog] r, - CallsFed 'Galley "get-conversations" + ( Member ConversationStore r, + Member FederatorAccess r, + Member P.TinyLog r ) => Local UserId -> [Remote ConvId] -> @@ -253,7 +258,8 @@ getRemoteConversationsWithFailures lusr convs = do | otherwise = [failedGetConversationLocally (map tUntagged locallyNotFound)] -- request conversations from remote backends - let rpc = fedClient @'Galley @"get-conversations" + let rpc :: GetConversationsRequest -> FederatorClient 'Galley GetConversationsResponse + rpc = fedClient @'Galley @"get-conversations" resp <- E.runFederatedConcurrentlyEither locallyFound $ \someConvs -> rpc $ GetConversationsRequest (tUnqualified lusr) (tUnqualified someConvs) @@ -262,7 +268,7 @@ getRemoteConversationsWithFailures lusr convs = do <$> traverse handleFailure resp where handleFailure :: - Members '[P.TinyLog] r => + Member P.TinyLog r => Either (Remote [ConvId], FederationError) (Remote GetConversationsResponse) -> Sem r (Either FailedGetConversation [Remote RemoteConversation]) handleFailure (Left (rcids, e)) = do @@ -273,7 +279,10 @@ getRemoteConversationsWithFailures lusr convs = do handleFailure (Right c) = pure . Right . traverse gcresConvs $ c getConversationRoles :: - Members '[ConversationStore, ErrorS 'ConvNotFound, ErrorS 'ConvAccessDenied] r => + ( Member ConversationStore r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'ConvAccessDenied) r + ) => Local UserId -> ConvId -> Sem r Public.ConversationRolesList @@ -311,15 +320,13 @@ conversationIdsPageFromUnqualified lusr start msize = do conversationIdsPageFromV2 :: forall p r. ( p ~ CassandraPaging, - Members - '[ ConversationStore, - Error InternalError, - Input Env, - ListItems p ConvId, - ListItems p (Remote ConvId), - P.TinyLog - ] - r + ( Member ConversationStore r, + Member (Error InternalError) r, + Member (Input Env) r, + Member (ListItems p ConvId) r, + Member (ListItems p (Remote ConvId)) r, + Member P.TinyLog r + ) ) => ListGlobalSelfConvs -> Local UserId -> @@ -402,15 +409,13 @@ conversationIdsPageFromV2 listGlobalSelf lusr Public.GetMultiTablePageRequest {. conversationIdsPageFrom :: forall p r. ( p ~ CassandraPaging, - Members - '[ ConversationStore, - Error InternalError, - Input Env, - ListItems p ConvId, - ListItems p (Remote ConvId), - P.TinyLog - ] - r + ( Member ConversationStore r, + Member (Error InternalError) r, + Member (Input Env) r, + Member (ListItems p ConvId) r, + Member (ListItems p (Remote ConvId)) r, + Member P.TinyLog r + ) ) => Local UserId -> Public.GetPaginatedConversationIds -> @@ -430,7 +435,11 @@ conversationIdsPageFrom lusr state = do conversationIdsPageFromV2 ListGlobalSelf lusr state getConversations :: - Members '[Error InternalError, ListItems LegacyPaging ConvId, ConversationStore, P.TinyLog] r => + ( Member (Error InternalError) r, + Member (ListItems LegacyPaging ConvId) r, + Member ConversationStore r, + Member P.TinyLog r + ) => Local UserId -> Maybe (Range 1 32 (CommaSeparatedList ConvId)) -> Maybe ConvId -> @@ -441,7 +450,9 @@ getConversations luser mids mstart msize = do flip ConversationList more <$> mapM (Mapping.conversationView luser) cs getConversationsInternal :: - Members '[ConversationStore, ListItems LegacyPaging ConvId] r => + ( Member ConversationStore r, + Member (ListItems LegacyPaging ConvId) r + ) => Local UserId -> Maybe (Range 1 32 (CommaSeparatedList ConvId)) -> Maybe ConvId -> @@ -460,7 +471,9 @@ getConversationsInternal luser mids mstart msize = do -- get ids and has_more flag getIds :: - Members '[ConversationStore, ListItems LegacyPaging ConvId] r => + ( Member ConversationStore r, + Member (ListItems LegacyPaging ConvId) r + ) => Maybe (Range 1 32 (CommaSeparatedList ConvId)) -> Sem r (Bool, [ConvId]) getIds (Just ids) = @@ -482,7 +495,11 @@ getConversationsInternal luser mids mstart msize = do | otherwise = pure True listConversations :: - (Members '[ConversationStore, Error InternalError, FederatorAccess, P.TinyLog] r, CallsFed 'Galley "get-conversations") => + ( Member ConversationStore r, + Member (Error InternalError) r, + Member FederatorAccess r, + Member P.TinyLog r + ) => Local UserId -> Public.ListConversations -> Sem r Public.ConversationsResponse @@ -535,7 +552,9 @@ listConversations luser (Public.ListConversations ids) = do pure (founds, notFounds) iterateConversations :: - Members '[ListItems LegacyPaging ConvId, ConversationStore] r => + ( Member (ListItems LegacyPaging ConvId) r, + Member ConversationStore r + ) => Local UserId -> Range 1 500 Int32 -> ([Data.Conversation] -> Sem r a) -> @@ -554,7 +573,10 @@ iterateConversations luid pageSize handleConvs = go Nothing pure $ resultHead : resultTail internalGetMemberH :: - Members '[ConversationStore, Input (Local ()), MemberStore] r => + ( Member ConversationStore r, + Member (Input (Local ())) r, + Member MemberStore r + ) => ConvId ::: UserId -> Sem r Response internalGetMemberH (cnv ::: usr) = do @@ -562,7 +584,9 @@ internalGetMemberH (cnv ::: usr) = do json <$> getLocalSelf lusr cnv getLocalSelf :: - Members '[ConversationStore, MemberStore] r => + ( Member ConversationStore r, + Member MemberStore r + ) => Local UserId -> ConvId -> Sem r (Maybe Public.Member) @@ -680,14 +704,12 @@ getConversationGuestLinksFeatureStatus mbTid = do -- the backend removal key). getMLSSelfConversationWithError :: forall r. - Members - '[ ConversationStore, - Error InternalError, - ErrorS 'MLSNotEnabled, - Input Env, - P.TinyLog - ] - r => + ( Member ConversationStore r, + Member (Error InternalError) r, + Member (ErrorS 'MLSNotEnabled) r, + Member (Input Env) r, + Member P.TinyLog r + ) => Local UserId -> Sem r Conversation getMLSSelfConversationWithError lusr = do @@ -702,13 +724,10 @@ getMLSSelfConversationWithError lusr = do -- number. getMLSSelfConversation :: forall r. - Members - '[ ConversationStore, - Error InternalError, - Input Env, - P.TinyLog - ] - r => + ( Member ConversationStore r, + Member (Error InternalError) r, + Member P.TinyLog r + ) => Local UserId -> Sem r Conversation getMLSSelfConversation lusr = do @@ -720,7 +739,13 @@ getMLSSelfConversation lusr = do ------------------------------------------------------------------------------- -- Helpers -ensureConvAdmin :: Members '[ErrorS 'ConvAccessDenied, ErrorS 'ConvNotFound] r => [LocalMember] -> UserId -> Sem r () +ensureConvAdmin :: + ( Member (ErrorS 'ConvAccessDenied) r, + Member (ErrorS 'ConvNotFound) r + ) => + [LocalMember] -> + UserId -> + Sem r () ensureConvAdmin users uid = case find ((== uid) . lmId) users of Nothing -> throwS @'ConvNotFound diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index 20197ae4fc..393a49f45b 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -30,7 +30,6 @@ module Galley.API.Teams deleteTeam, uncheckedDeleteTeam, addTeamMember, - getTeamNotificationsH, getTeamConversationRoles, getTeamMembers, getTeamMembersCSV, @@ -83,8 +82,6 @@ import Data.Qualified import Data.Range as Range import qualified Data.Set as Set import Data.Time.Clock (UTCTime) -import qualified Data.UUID as UUID -import qualified Data.UUID.Util as UUID import Galley.API.Error as Galley import Galley.API.LegalHold import qualified Galley.API.Teams.Notifications as APITeamQueue @@ -112,11 +109,9 @@ import Galley.Intra.Push import Galley.Options import qualified Galley.Types.Conversations.Members as Conv import Galley.Types.Teams -import Galley.Types.Teams.Intra import Galley.Types.UserList import Imports hiding (forkIO) import Network.Wai -import Network.Wai.Predicate hiding (Error, or, result, setStatus) import Network.Wai.Utilities hiding (Error) import Polysemy import Polysemy.Error @@ -132,10 +127,9 @@ import Wire.API.Error import Wire.API.Error.Galley import qualified Wire.API.Event.Conversation as Conv import Wire.API.Event.Team -import Wire.API.Federation.API import Wire.API.Federation.Error import qualified Wire.API.Message as Conv -import qualified Wire.API.Notification as Public +import Wire.API.Routes.Internal.Galley.TeamsIntra import Wire.API.Routes.MultiTablePaging (MultiTablePage (MultiTablePage), MultiTablePagingState (mtpsState)) import Wire.API.Routes.Public.Galley.TeamMember import Wire.API.Team @@ -167,14 +161,18 @@ getTeamH zusr tid = maybe (throwS @'TeamNotFound) pure =<< lookupTeam zusr tid getTeamInternalH :: - Members '[ErrorS 'TeamNotFound, TeamStore] r => + ( Member (ErrorS 'TeamNotFound) r, + Member TeamStore r + ) => TeamId -> Sem r TeamData getTeamInternalH tid = E.getTeam tid >>= noteS @'TeamNotFound getTeamNameInternalH :: - Members '[ErrorS 'TeamNotFound, TeamStore] r => + ( Member (ErrorS 'TeamNotFound) r, + Member TeamStore r + ) => TeamId -> Sem r TeamName getTeamNameInternalH tid = @@ -197,7 +195,10 @@ getTeamNameInternal = fmap (fmap TeamName) . E.getTeamName -- between 1 and 100, and that will always be an upper bound of the result set of size 0 or -- one.) getManyTeams :: - (Members '[TeamStore, Queue DeleteItem, ListItems LegacyPaging TeamId] r) => + ( Member TeamStore r, + Member (Queue DeleteItem) r, + Member (ListItems LegacyPaging TeamId) r + ) => UserId -> Sem r Public.TeamList getManyTeams zusr = @@ -206,7 +207,9 @@ getManyTeams zusr = pure (Public.newTeamList (catMaybes teams) more) lookupTeam :: - Members '[TeamStore, Queue DeleteItem] r => + ( Member TeamStore r, + Member (Queue DeleteItem) r + ) => UserId -> TeamId -> Sem r (Maybe Public.Team) @@ -228,8 +231,7 @@ createNonBindingTeamH :: Member GundeckAccess r, Member (Input UTCTime) r, Member P.TinyLog r, - Member TeamStore r, - Member WaiRoutes r + Member TeamStore r ) => UserId -> ConnId -> @@ -259,7 +261,10 @@ createNonBindingTeamH zusr zcon (Public.NonBindingNewTeam body) = do pure (team ^. teamId) createBindingTeam :: - Members '[GundeckAccess, Input UTCTime, TeamStore] r => + ( Member GundeckAccess r, + Member (Input UTCTime) r, + Member TeamStore r + ) => TeamId -> UserId -> BindingNewTeam -> @@ -272,16 +277,14 @@ createBindingTeam tid zusr (BindingNewTeam body) = do pure tid updateTeamStatus :: - Members - '[ BrigAccess, - ErrorS 'InvalidTeamStatusUpdate, - ErrorS 'TeamNotFound, - Input Opts, - Input UTCTime, - P.TinyLog, - TeamStore - ] - r => + ( Member BrigAccess r, + Member (ErrorS 'InvalidTeamStatusUpdate) r, + Member (ErrorS 'TeamNotFound) r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member P.TinyLog r, + Member TeamStore r + ) => TeamId -> TeamStatusUpdate -> Sem r () @@ -315,14 +318,12 @@ updateTeamStatus tid (TeamStatusUpdate newStatus cur) = do (_, _) -> throwS @'InvalidTeamStatusUpdate updateTeamH :: - Members - '[ ErrorS 'NotATeamMember, - ErrorS ('MissingPermission ('Just 'SetTeamData)), - GundeckAccess, - Input UTCTime, - TeamStore - ] - r => + ( Member (ErrorS 'NotATeamMember) r, + Member (ErrorS ('MissingPermission ('Just 'SetTeamData))) r, + Member GundeckAccess r, + Member (Input UTCTime) r, + Member TeamStore r + ) => UserId -> ConnId -> TeamId -> @@ -346,7 +347,6 @@ deleteTeam :: forall r. ( Member BrigAccess r, Member (Error AuthenticationError) r, - Member (Error InvalidInput) r, Member (ErrorS 'DeleteQueueFull) r, Member (ErrorS 'NotATeamMember) r, Member (ErrorS OperationDenied) r, @@ -376,15 +376,13 @@ deleteTeam zusr zcon tid body = do -- This can be called by stern internalDeleteBindingTeam :: - Members - '[ ErrorS 'NoBindingTeam, - ErrorS 'TeamNotFound, - ErrorS 'NotAOneMemberTeam, - ErrorS 'DeleteQueueFull, - Queue DeleteItem, - TeamStore - ] - r => + ( Member (ErrorS 'NoBindingTeam) r, + Member (ErrorS 'TeamNotFound) r, + Member (ErrorS 'NotAOneMemberTeam) r, + Member (ErrorS 'DeleteQueueFull) r, + Member (Queue DeleteItem) r, + Member TeamStore r + ) => TeamId -> Bool -> Sem r () @@ -404,18 +402,16 @@ internalDeleteBindingTeam tid force = do -- This function is "unchecked" because it does not validate that the user has the `DeleteTeam` permission. uncheckedDeleteTeam :: forall r. - Members - '[ BrigAccess, - ExternalAccess, - GundeckAccess, - Input Opts, - Input UTCTime, - LegalHoldStore, - MemberStore, - SparAccess, - TeamStore - ] - r => + ( Member BrigAccess r, + Member ExternalAccess r, + Member GundeckAccess r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member LegalHoldStore r, + Member MemberStore r, + Member SparAccess r, + Member TeamStore r + ) => Local UserId -> Maybe ConnId -> TeamId -> @@ -480,7 +476,9 @@ uncheckedDeleteTeam lusr zcon tid = do pure (pp', ee' ++ ee) getTeamConversationRoles :: - Members '[ErrorS 'NotATeamMember, TeamStore] r => + ( Member (ErrorS 'NotATeamMember) r, + Member TeamStore r + ) => UserId -> TeamId -> Sem r Public.ConversationRolesList @@ -491,7 +489,10 @@ getTeamConversationRoles zusr tid = do pure $ Public.ConversationRolesList wireConvRoles getTeamMembers :: - Members '[ErrorS 'NotATeamMember, TeamStore, TeamMemberStore CassandraPaging] r => + ( Member (ErrorS 'NotATeamMember) r, + Member TeamStore r, + Member (TeamMemberStore CassandraPaging) r + ) => Local UserId -> TeamId -> Maybe (Range 1 Public.HardTruncationLimit Int32) -> @@ -521,7 +522,13 @@ outputToStreamingBody action = withWeavingToFinal @IO $ \state weave _inspect -> void . weave . (<$ state) $ runOutputSem writeChunk action getTeamMembersCSV :: - (Members '[BrigAccess, ErrorS 'AccessDenied, TeamMemberStore InternalPaging, TeamStore, Final IO, SparAccess] r) => + ( Member BrigAccess r, + Member (ErrorS 'AccessDenied) r, + Member (TeamMemberStore InternalPaging) r, + Member TeamStore r, + Member (Final IO) r, + Member SparAccess r + ) => Local UserId -> TeamId -> Sem r StreamingBody @@ -632,7 +639,10 @@ getTeamMembersCSV lusr tid = do -- | like 'getTeamMembers', but with an explicit list of users we are to return. bulkGetTeamMembers :: - Members '[ErrorS 'BulkGetMemberLimitExceeded, ErrorS 'NotATeamMember, TeamStore] r => + ( Member (ErrorS 'BulkGetMemberLimitExceeded) r, + Member (ErrorS 'NotATeamMember) r, + Member TeamStore r + ) => Local UserId -> TeamId -> Maybe (Range 1 HardTruncationLimit Int32) -> @@ -648,7 +658,10 @@ bulkGetTeamMembers lzusr tid mbMaxResults uids = do pure $ setOptionalPermsMany withPerms (newTeamMemberList mems hasMore) getTeamMember :: - Members '[ErrorS 'TeamMemberNotFound, ErrorS 'NotATeamMember, TeamStore] r => + ( Member (ErrorS 'TeamMemberNotFound) r, + Member (ErrorS 'NotATeamMember) r, + Member TeamStore r + ) => Local UserId -> TeamId -> UserId -> @@ -662,7 +675,9 @@ getTeamMember lzusr tid uid = do pure $ setOptionalPerms withPerms member uncheckedGetTeamMember :: - Members '[ErrorS 'TeamMemberNotFound, TeamStore] r => + ( Member (ErrorS 'TeamMemberNotFound) r, + Member TeamStore r + ) => TeamId -> UserId -> Sem r TeamMember @@ -686,29 +701,24 @@ uncheckedGetTeamMembers = E.getTeamMembersWithLimit addTeamMember :: forall db r. - ( Members - '[ BrigAccess, - GundeckAccess, - ErrorS 'InvalidPermissions, - ErrorS 'NoAddToBinding, - ErrorS 'NotATeamMember, - ErrorS 'NotConnected, - ErrorS OperationDenied, - ErrorS 'TeamNotFound, - ErrorS 'TooManyTeamMembers, - ErrorS 'UserBindingExists, - ErrorS 'TooManyTeamMembersOnTeamWithLegalhold, - Input (Local ()), - Input Opts, - Input UTCTime, - LegalHoldStore, - MemberStore, - TeamFeatureStore db, - TeamNotificationStore, - TeamStore, - P.TinyLog - ] - r, + ( Member BrigAccess r, + Member GundeckAccess r, + Member (ErrorS 'InvalidPermissions) r, + Member (ErrorS 'NoAddToBinding) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'NotConnected) r, + Member (ErrorS OperationDenied) r, + Member (ErrorS 'TeamNotFound) r, + Member (ErrorS 'TooManyTeamMembers) r, + Member (ErrorS 'UserBindingExists) r, + Member (ErrorS 'TooManyTeamMembersOnTeamWithLegalhold) r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member LegalHoldStore r, + Member (TeamFeatureStore db) r, + Member TeamNotificationStore r, + Member TeamStore r, + Member P.TinyLog r, FeaturePersistentConstraint db LegalholdConfig ) => Local UserId -> @@ -739,22 +749,17 @@ addTeamMember lzusr zcon tid nmem = do -- This function is "unchecked" because there is no need to check for user binding (invite only). uncheckedAddTeamMember :: forall db r. - ( Members - '[ BrigAccess, - GundeckAccess, - ErrorS 'TooManyTeamMembers, - Input (Local ()), - ErrorS 'TooManyTeamMembersOnTeamWithLegalhold, - Input Opts, - Input UTCTime, - MemberStore, - LegalHoldStore, - P.TinyLog, - TeamFeatureStore db, - TeamNotificationStore, - TeamStore - ] - r, + ( Member BrigAccess r, + Member GundeckAccess r, + Member (ErrorS 'TooManyTeamMembers) r, + Member (ErrorS 'TooManyTeamMembersOnTeamWithLegalhold) r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member LegalHoldStore r, + Member P.TinyLog r, + Member (TeamFeatureStore db) r, + Member TeamNotificationStore r, + Member TeamStore r, FeaturePersistentConstraint db LegalholdConfig ) => TeamId -> @@ -770,21 +775,15 @@ uncheckedAddTeamMember tid nmem = do uncheckedUpdateTeamMember :: 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 => + ( Member BrigAccess r, + Member (ErrorS 'TeamNotFound) r, + Member (ErrorS 'TeamMemberNotFound) r, + Member GundeckAccess r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member P.TinyLog r, + Member TeamStore r + ) => Maybe (Local UserId) -> Maybe ConnId -> TeamId -> @@ -834,21 +833,19 @@ uncheckedUpdateTeamMember mlzusr mZcon tid newMember = do 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 => + ( Member BrigAccess r, + Member (ErrorS 'AccessDenied) r, + Member (ErrorS 'InvalidPermissions) r, + Member (ErrorS 'TeamNotFound) r, + Member (ErrorS 'TeamMemberNotFound) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS OperationDenied) r, + Member GundeckAccess r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member P.TinyLog r, + Member TeamStore r + ) => Local UserId -> ConnId -> TeamId -> @@ -888,25 +885,23 @@ updateTeamMember lzusr zcon tid newMember = do && permissionsRole targetPermissions /= Just RoleOwner deleteTeamMember :: - Members - '[ BrigAccess, - ConversationStore, - Error AuthenticationError, - Error InvalidInput, - ErrorS 'AccessDenied, - ErrorS 'TeamMemberNotFound, - ErrorS 'TeamNotFound, - ErrorS 'NotATeamMember, - ErrorS OperationDenied, - ExternalAccess, - Input Opts, - Input UTCTime, - GundeckAccess, - MemberStore, - TeamStore, - P.TinyLog - ] - r => + ( Member BrigAccess r, + Member ConversationStore r, + Member (Error AuthenticationError) r, + Member (Error InvalidInput) r, + Member (ErrorS 'AccessDenied) r, + Member (ErrorS 'TeamMemberNotFound) r, + Member (ErrorS 'TeamNotFound) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS OperationDenied) r, + Member ExternalAccess r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member GundeckAccess r, + Member MemberStore r, + Member TeamStore r, + Member P.TinyLog r + ) => Local UserId -> ConnId -> TeamId -> @@ -916,25 +911,23 @@ deleteTeamMember :: deleteTeamMember lusr zcon tid remove body = deleteTeamMember' lusr zcon tid remove (Just body) deleteNonBindingTeamMember :: - Members - '[ BrigAccess, - ConversationStore, - Error AuthenticationError, - Error InvalidInput, - ErrorS 'AccessDenied, - ErrorS 'TeamMemberNotFound, - ErrorS 'TeamNotFound, - ErrorS 'NotATeamMember, - ErrorS OperationDenied, - ExternalAccess, - Input Opts, - Input UTCTime, - GundeckAccess, - MemberStore, - TeamStore, - P.TinyLog - ] - r => + ( Member BrigAccess r, + Member ConversationStore r, + Member (Error AuthenticationError) r, + Member (Error InvalidInput) r, + Member (ErrorS 'AccessDenied) r, + Member (ErrorS 'TeamMemberNotFound) r, + Member (ErrorS 'TeamNotFound) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS OperationDenied) r, + Member ExternalAccess r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member GundeckAccess r, + Member MemberStore r, + Member TeamStore r, + Member P.TinyLog r + ) => Local UserId -> ConnId -> TeamId -> @@ -944,25 +937,23 @@ deleteNonBindingTeamMember lusr zcon tid remove = deleteTeamMember' lusr zcon ti -- | 'TeamMemberDeleteData' is only required for binding teams deleteTeamMember' :: - Members - '[ BrigAccess, - ConversationStore, - Error AuthenticationError, - Error InvalidInput, - ErrorS 'AccessDenied, - ErrorS 'TeamMemberNotFound, - ErrorS 'TeamNotFound, - ErrorS 'NotATeamMember, - ErrorS OperationDenied, - ExternalAccess, - Input Opts, - Input UTCTime, - GundeckAccess, - MemberStore, - TeamStore, - P.TinyLog - ] - r => + ( Member BrigAccess r, + Member ConversationStore r, + Member (Error AuthenticationError) r, + Member (Error InvalidInput) r, + Member (ErrorS 'AccessDenied) r, + Member (ErrorS 'TeamMemberNotFound) r, + Member (ErrorS 'TeamNotFound) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS OperationDenied) r, + Member ExternalAccess r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member GundeckAccess r, + Member MemberStore r, + Member TeamStore r, + Member P.TinyLog r + ) => Local UserId -> ConnId -> TeamId -> @@ -1005,15 +996,13 @@ deleteTeamMember' lusr zcon tid remove mBody = do -- This function is "unchecked" because it does not validate that the user has the `RemoveTeamMember` permission. uncheckedDeleteTeamMember :: forall r. - Members - '[ ConversationStore, - GundeckAccess, - ExternalAccess, - Input UTCTime, - MemberStore, - TeamStore - ] - r => + ( Member ConversationStore r, + Member GundeckAccess r, + Member ExternalAccess r, + Member (Input UTCTime) r, + Member MemberStore r, + Member TeamStore r + ) => Local UserId -> Maybe ConnId -> TeamId -> @@ -1063,12 +1052,10 @@ uncheckedDeleteTeamMember lusr zcon tid remove mems = do E.deliverAsync (bots `zip` repeat y) getTeamConversations :: - Members - '[ ErrorS 'NotATeamMember, - ErrorS OperationDenied, - TeamStore - ] - r => + ( Member (ErrorS 'NotATeamMember) r, + Member (ErrorS OperationDenied) r, + Member TeamStore r + ) => UserId -> TeamId -> Sem r Public.TeamConversationList @@ -1081,13 +1068,11 @@ getTeamConversations zusr tid = do Public.newTeamConversationList <$> E.getTeamConversations tid getTeamConversation :: - Members - '[ ErrorS 'ConvNotFound, - ErrorS 'NotATeamMember, - ErrorS OperationDenied, - TeamStore - ] - r => + ( Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS OperationDenied) r, + Member TeamStore r + ) => UserId -> TeamId -> ConvId -> @@ -1102,26 +1087,18 @@ getTeamConversation zusr tid cid = do >>= noteS @'ConvNotFound deleteTeamConversation :: - ( Members - '[ CodeStore, - ConversationStore, - Error FederationError, - Error InvalidInput, - ErrorS 'ConvNotFound, - ErrorS 'InvalidOperation, - ErrorS 'NotATeamMember, - ErrorS ('ActionDenied 'DeleteConversation), - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input UTCTime, - TeamStore - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member CodeStore r, + Member ConversationStore r, + Member (Error FederationError) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS ('ActionDenied 'DeleteConversation)) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input UTCTime) r, + Member TeamStore r ) => Local UserId -> ConnId -> @@ -1133,13 +1110,11 @@ deleteTeamConversation lusr zcon _tid cid = do void $ API.deleteLocalConversation lusr zcon lconv getSearchVisibility :: - Members - '[ ErrorS 'NotATeamMember, - ErrorS OperationDenied, - SearchVisibilityStore, - TeamStore - ] - r => + ( Member (ErrorS 'NotATeamMember) r, + Member (ErrorS OperationDenied) r, + Member SearchVisibilityStore r, + Member TeamStore r + ) => Local UserId -> TeamId -> Sem r TeamSearchVisibilityView @@ -1149,19 +1124,12 @@ getSearchVisibility luid tid = do getSearchVisibilityInternal tid setSearchVisibility :: - forall db r. - ( Members - '[ ErrorS 'NotATeamMember, - ErrorS OperationDenied, - ErrorS 'TeamSearchVisibilityNotEnabled, - Input Opts, - SearchVisibilityStore, - TeamStore, - TeamFeatureStore db, - WaiRoutes - ] - r, - FeaturePersistentConstraint db SearchVisibilityAvailableConfig + forall r. + ( Member (ErrorS 'NotATeamMember) r, + Member (ErrorS OperationDenied) r, + Member (ErrorS 'TeamSearchVisibilityNotEnabled) r, + Member SearchVisibilityStore r, + Member TeamStore r ) => (TeamId -> Sem r Bool) -> Local UserId -> @@ -1171,7 +1139,7 @@ setSearchVisibility :: setSearchVisibility availableForTeam luid tid req = do zusrMembership <- E.getTeamMember tid (tUnqualified luid) void $ permissionCheck ChangeTeamSearchVisibility zusrMembership - setSearchVisibilityInternal @db availableForTeam tid req + setSearchVisibilityInternal availableForTeam tid req -- Internal ----------------------------------------------------------------- @@ -1206,7 +1174,12 @@ withTeamIds usr range size k = case range of k False ids {-# INLINE withTeamIds #-} -ensureUnboundUsers :: Members '[ErrorS 'UserBindingExists, TeamStore] r => [UserId] -> Sem r () +ensureUnboundUsers :: + ( Member (ErrorS 'UserBindingExists) r, + Member TeamStore r + ) => + [UserId] -> + Sem r () ensureUnboundUsers uids = do -- We check only 1 team because, by definition, users in binding teams -- can only be part of one team. @@ -1216,7 +1189,10 @@ ensureUnboundUsers uids = do throwS @'UserBindingExists ensureNonBindingTeam :: - Members '[ErrorS 'TeamNotFound, ErrorS 'NoAddToBinding, TeamStore] r => + ( Member (ErrorS 'TeamNotFound) r, + Member (ErrorS 'NoAddToBinding) r, + Member TeamStore r + ) => TeamId -> Sem r () ensureNonBindingTeam tid = do @@ -1235,7 +1211,10 @@ ensureNotElevated targetPermissions member = $ throwS @'InvalidPermissions ensureNotTooLarge :: - Members '[BrigAccess, ErrorS 'TooManyTeamMembers, Input Opts] r => + ( Member BrigAccess r, + Member (ErrorS 'TooManyTeamMembers) r, + Member (Input Opts) r + ) => TeamId -> Sem r TeamSize ensureNotTooLarge tid = do @@ -1256,13 +1235,10 @@ ensureNotTooLarge tid = do -- FUTUREWORK: Find a way around the fanout limit. ensureNotTooLargeForLegalHold :: forall db r. - ( Members - '[ LegalHoldStore, - TeamStore, - TeamFeatureStore db, - ErrorS 'TooManyTeamMembersOnTeamWithLegalhold - ] - r, + ( Member LegalHoldStore r, + Member TeamStore r, + Member (TeamFeatureStore db) r, + Member (ErrorS 'TooManyTeamMembersOnTeamWithLegalhold) r, FeaturePersistentConstraint db LegalholdConfig ) => TeamId -> @@ -1274,7 +1250,10 @@ ensureNotTooLargeForLegalHold tid teamSize = throwS @'TooManyTeamMembersOnTeamWithLegalhold ensureNotTooLargeToActivateLegalHold :: - Members '[BrigAccess, ErrorS 'CannotEnableLegalHoldServiceLargeTeam, TeamStore] r => + ( Member BrigAccess r, + Member (ErrorS 'CannotEnableLegalHoldServiceLargeTeam) r, + Member TeamStore r + ) => TeamId -> Sem r () ensureNotTooLargeToActivateLegalHold tid = do @@ -1294,19 +1273,15 @@ teamSizeBelowLimit teamSize = do pure True addTeamMemberInternal :: - Members - '[ BrigAccess, - ErrorS 'TooManyTeamMembers, - GundeckAccess, - Input (Local ()), - Input Opts, - Input UTCTime, - MemberStore, - TeamNotificationStore, - TeamStore, - P.TinyLog - ] - r => + ( Member BrigAccess r, + Member (ErrorS 'TooManyTeamMembers) r, + Member GundeckAccess r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member TeamNotificationStore r, + Member TeamStore r, + Member P.TinyLog r + ) => TeamId -> Maybe UserId -> Maybe ConnId -> @@ -1335,42 +1310,11 @@ addTeamMemberInternal tid origin originConn (ntmNewTeamMember -> new) memList = (userRecipient (n ^. userId)) (membersToRecipients Nothing (memList ^. teamMembers)) --- | See also: 'Gundeck.API.Public.paginateH', but the semantics of this end-point is slightly --- less warped. This is a work-around because we cannot send events to all of a large team. --- See haddocks of module "Galley.API.TeamNotifications" for details. -getTeamNotificationsH :: - Members - '[ BrigAccess, - ErrorS 'TeamNotFound, - Error InvalidInput, - TeamNotificationStore - ] - r => - UserId - ::: Maybe ByteString {- NotificationId -} - ::: Range 1 10000 Int32 - ::: JSON -> - Sem r Response -getTeamNotificationsH (zusr ::: sinceRaw ::: size ::: _) = do - since <- parseSince - json @Public.QueuedNotificationList - <$> APITeamQueue.getTeamNotifications zusr since size - where - parseSince :: Member (Error InvalidInput) r => Sem r (Maybe Public.NotificationId) - parseSince = maybe (pure Nothing) (fmap Just . parseUUID) sinceRaw - - parseUUID :: Member (Error InvalidInput) r => ByteString -> Sem r Public.NotificationId - parseUUID raw = - maybe - (throw InvalidTeamNotificationId) - (pure . Id) - ((UUID.fromASCIIBytes >=> isV1UUID) raw) - - isV1UUID :: UUID.UUID -> Maybe UUID.UUID - isV1UUID u = if UUID.version u == 1 then Just u else Nothing - finishCreateTeam :: - Members '[GundeckAccess, Input UTCTime, TeamStore] r => + ( Member GundeckAccess r, + Member (Input UTCTime) r, + Member TeamStore r + ) => Team -> TeamMember -> [TeamMember] -> @@ -1386,24 +1330,28 @@ finishCreateTeam team owner others zcon = do E.push1 $ newPushLocal1 ListComplete zusr (TeamEvent e) (list1 (userRecipient zusr) r) & pushConn .~ zcon getBindingTeamIdH :: - Members '[ErrorS 'TeamNotFound, ErrorS 'NonBindingTeam, TeamStore] r => + ( Member (ErrorS 'TeamNotFound) r, + Member (ErrorS 'NonBindingTeam) r, + Member TeamStore r + ) => UserId -> Sem r Response getBindingTeamIdH = fmap json . E.lookupBindingTeam getBindingTeamMembersH :: - Members '[ErrorS 'TeamNotFound, ErrorS 'NonBindingTeam, TeamStore] r => + ( Member (ErrorS 'TeamNotFound) r, + Member (ErrorS 'NonBindingTeam) r, + Member TeamStore r + ) => UserId -> Sem r Response getBindingTeamMembersH = fmap json . getBindingTeamMembers getBindingTeamMembers :: - Members - '[ ErrorS 'TeamNotFound, - ErrorS 'NonBindingTeam, - TeamStore - ] - r => + ( Member (ErrorS 'TeamNotFound) r, + Member (ErrorS 'NonBindingTeam) r, + Member TeamStore r + ) => UserId -> Sem r TeamMemberList getBindingTeamMembers zusr = do @@ -1425,14 +1373,11 @@ getBindingTeamMembers zusr = do -- RegisterError`. canUserJoinTeam :: forall db r. - ( Members - '[ BrigAccess, - LegalHoldStore, - TeamStore, - TeamFeatureStore db, - ErrorS 'TooManyTeamMembersOnTeamWithLegalhold - ] - r, + ( Member BrigAccess r, + Member LegalHoldStore r, + Member TeamStore r, + Member (TeamFeatureStore db) r, + Member (ErrorS 'TooManyTeamMembersOnTeamWithLegalhold) r, FeaturePersistentConstraint db LegalholdConfig ) => TeamId -> @@ -1453,15 +1398,9 @@ getSearchVisibilityInternal = . SearchVisibilityData.getSearchVisibility setSearchVisibilityInternal :: - forall db r. - ( Members - '[ ErrorS 'TeamSearchVisibilityNotEnabled, - Input Opts, - SearchVisibilityStore, - TeamFeatureStore db - ] - r, - FeaturePersistentConstraint db SearchVisibilityAvailableConfig + forall r. + ( Member (ErrorS 'TeamSearchVisibilityNotEnabled) r, + Member SearchVisibilityStore r ) => (TeamId -> Sem r Bool) -> TeamId -> @@ -1473,14 +1412,12 @@ setSearchVisibilityInternal availableForTeam tid (TeamSearchVisibilityView searc SearchVisibilityData.setSearchVisibility tid searchVisibility userIsTeamOwner :: - Members - '[ ErrorS 'TeamMemberNotFound, - ErrorS 'AccessDenied, - ErrorS 'NotATeamMember, - Input (Local ()), - TeamStore - ] - r => + ( Member (ErrorS 'TeamMemberNotFound) r, + Member (ErrorS 'AccessDenied) r, + Member (ErrorS 'NotATeamMember) r, + Member (Input (Local ())) r, + Member TeamStore r + ) => TeamId -> UserId -> Sem r () @@ -1491,7 +1428,9 @@ userIsTeamOwner tid uid = do -- Queues a team for async deletion queueTeamDeletion :: - Members '[ErrorS 'DeleteQueueFull, Queue DeleteItem] r => + ( Member (ErrorS 'DeleteQueueFull) r, + Member (Queue DeleteItem) r + ) => TeamId -> UserId -> Maybe ConnId -> diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index be179c9c99..b5172a853f 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -40,7 +40,7 @@ import Control.Lens import Data.Bifunctor (second) import Data.ByteString.Conversion (toByteString') import Data.Id -import Data.Kind (Constraint) +import Data.Kind import Data.Proxy (Proxy (Proxy)) import Data.Qualified (Local, tUnqualified) import Data.Schema @@ -76,7 +76,6 @@ import Wire.API.Conversation.Role (Action (RemoveConversationMember)) import Wire.API.Error (ErrorS, throwS) import Wire.API.Error.Galley import qualified Wire.API.Event.FeatureConfig as Event -import Wire.API.Federation.API import qualified Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti as Multi import Wire.API.Team.Feature import Wire.API.Team.Member @@ -86,36 +85,48 @@ import Wire.Sem.Paging.Cassandra data DoAuth = DoAuth UserId | DontDoAuth -- | Don't export methods of this typeclass -class GetFeatureConfig (db :: *) cfg where +class GetFeatureConfig (db :: Type) cfg where type GetConfigForTeamConstraints db cfg (r :: EffectRow) :: Constraint - type GetConfigForTeamConstraints db cfg (r :: EffectRow) = (FeaturePersistentConstraint db cfg, Members '[Input Opts, TeamFeatureStore db] r) + type + GetConfigForTeamConstraints db cfg (r :: EffectRow) = + ( FeaturePersistentConstraint db cfg, + ( Member (Input Opts) r, + Member (TeamFeatureStore db) r + ) + ) type GetConfigForUserConstraints db cfg (r :: EffectRow) :: Constraint type GetConfigForUserConstraints db cfg (r :: EffectRow) = ( FeaturePersistentConstraint db cfg, - Members - '[ Input Opts, - ErrorS OperationDenied, - ErrorS 'NotATeamMember, - ErrorS 'TeamNotFound, - TeamStore, - TeamFeatureStore db - ] - r + ( Member (Input Opts) r, + Member (ErrorS OperationDenied) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'TeamNotFound) r, + Member TeamStore r, + Member (TeamFeatureStore db) r + ) ) getConfigForServer :: - Members '[Input Opts] r => + Member (Input Opts) r => Sem r (WithStatus cfg) + -- only override if there is additional business logic for getting the feature config + -- and/or if the feature flag is configured for the backend in 'FeatureFlags' for galley in 'Galley.Types.Teams' + -- otherwise this will return the default config from wire-api + default getConfigForServer :: (IsFeatureConfig cfg) => Sem r (WithStatus cfg) + getConfigForServer = pure defFeatureStatus getConfigForTeam :: GetConfigForTeamConstraints db cfg r => TeamId -> Sem r (WithStatus cfg) default getConfigForTeam :: - GetConfigForTeamConstraints db cfg r => - (FeaturePersistentConstraint db cfg, Members '[Input Opts, TeamFeatureStore db] r) => + ( FeaturePersistentConstraint db cfg, + ( Member (Input Opts) r, + Member (TeamFeatureStore db) r + ) + ) => TeamId -> Sem r (WithStatus cfg) getConfigForTeam = genericGetConfigForTeam @db @@ -125,25 +136,21 @@ class GetFeatureConfig (db :: *) cfg where UserId -> Sem r (WithStatus cfg) default getConfigForUser :: - GetConfigForUserConstraints db cfg r => - GetConfigForTeamConstraints db cfg r => ( FeaturePersistentConstraint db cfg, - Members - '[ Input Opts, - ErrorS OperationDenied, - ErrorS 'NotATeamMember, - ErrorS 'TeamNotFound, - TeamStore, - TeamFeatureStore db - ] - r + ( Member (Input Opts) r, + Member (ErrorS OperationDenied) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'TeamNotFound) r, + Member TeamStore r, + Member (TeamFeatureStore db) r + ) ) => UserId -> Sem r (WithStatus cfg) getConfigForUser = genericGetConfigForUser @db -- | Don't export methods of this typeclass -class GetFeatureConfig (db :: *) cfg => SetFeatureConfig (db :: *) cfg where +class GetFeatureConfig (db :: Type) cfg => SetFeatureConfig (db :: Type) cfg where type SetConfigForTeamConstraints db cfg (r :: EffectRow) :: Constraint type SetConfigForTeamConstraints db cfg (r :: EffectRow) = () @@ -154,6 +161,21 @@ class GetFeatureConfig (db :: *) cfg => SetFeatureConfig (db :: *) cfg where ( SetConfigForTeamConstraints db cfg r, GetConfigForTeamConstraints db cfg r, FeaturePersistentConstraint db cfg, + ( Member (TeamFeatureStore db) r, + Member (P.Logger (Log.Msg -> Log.Msg)) r, + Member GundeckAccess r, + Member TeamStore r + ) + ) => + TeamId -> + WithStatusNoLock cfg -> + Sem r (WithStatus cfg) + default setConfigForTeam :: + ( GetConfigForTeamConstraints db cfg r, + FeaturePersistentConstraint db cfg, + IsFeatureConfig cfg, + KnownSymbol (FeatureSymbol cfg), + ToSchema cfg, Members '[ TeamFeatureStore db, P.Logger (Log.Msg -> Log.Msg), @@ -165,6 +187,7 @@ class GetFeatureConfig (db :: *) cfg => SetFeatureConfig (db :: *) cfg where TeamId -> WithStatusNoLock cfg -> Sem r (WithStatus cfg) + setConfigForTeam tid wsnl = persistAndPushEvent @db tid wsnl type FeaturePersistentAllFeatures db = ( FeaturePersistentConstraint db LegalholdConfig, @@ -181,20 +204,20 @@ type FeaturePersistentAllFeatures db = FeaturePersistentConstraint db SndFactorPasswordChallengeConfig, FeaturePersistentConstraint db MLSConfig, FeaturePersistentConstraint db SearchVisibilityInboundConfig, - FeaturePersistentConstraint db ExposeInvitationURLsToTeamAdminConfig + FeaturePersistentConstraint db ExposeInvitationURLsToTeamAdminConfig, + FeaturePersistentConstraint db OutlookCalIntegrationConfig, + FeaturePersistentConstraint db MlsE2EIdConfig ) getFeatureStatus :: forall db cfg r. ( GetFeatureConfig db cfg, GetConfigForTeamConstraints db cfg r, - Members - '[ ErrorS OperationDenied, - ErrorS 'NotATeamMember, - ErrorS 'TeamNotFound, - TeamStore - ] - r + ( Member (ErrorS OperationDenied) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'TeamNotFound) r, + Member TeamStore r + ) ) => DoAuth -> TeamId -> @@ -210,13 +233,10 @@ getFeatureStatus doauth tid = do getFeatureStatusMulti :: forall db cfg r. ( GetFeatureConfig db cfg, - GetConfigForTeamConstraints db cfg r, FeaturePersistentConstraint db cfg, - Members - '[ Input Opts, - TeamFeatureStore db - ] - r + ( Member (Input Opts) r, + Member (TeamFeatureStore db) r + ) ) => Multi.TeamFeatureNoConfigMultiRequest -> Sem r (Multi.TeamFeatureNoConfigMultiResponse cfg) @@ -234,17 +254,15 @@ patchFeatureStatusInternal :: GetConfigForTeamConstraints db cfg r, SetConfigForTeamConstraints db cfg r, FeaturePersistentConstraint db cfg, - Members - '[ ErrorS 'NotATeamMember, - ErrorS OperationDenied, - ErrorS 'TeamNotFound, - Error TeamFeatureError, - TeamStore, - TeamFeatureStore db, - P.Logger (Log.Msg -> Log.Msg), - GundeckAccess - ] - r + ( Member (ErrorS 'NotATeamMember) r, + Member (ErrorS OperationDenied) r, + Member (ErrorS 'TeamNotFound) r, + Member (Error TeamFeatureError) r, + Member TeamStore r, + Member (TeamFeatureStore db) r, + Member (P.Logger (Log.Msg -> Log.Msg)) r, + Member GundeckAccess r + ) ) => TeamId -> WithStatusPatch cfg -> @@ -269,17 +287,15 @@ setFeatureStatus :: GetConfigForTeamConstraints db cfg r, SetConfigForTeamConstraints db cfg r, FeaturePersistentConstraint db cfg, - Members - '[ ErrorS 'NotATeamMember, - ErrorS OperationDenied, - ErrorS 'TeamNotFound, - Error TeamFeatureError, - TeamStore, - TeamFeatureStore db, - P.Logger (Log.Msg -> Log.Msg), - GundeckAccess - ] - r + ( Member (ErrorS 'NotATeamMember) r, + Member (ErrorS OperationDenied) r, + Member (ErrorS 'TeamNotFound) r, + Member (Error TeamFeatureError) r, + Member TeamStore r, + Member (TeamFeatureStore db) r, + Member (P.Logger (Log.Msg -> Log.Msg)) r, + Member GundeckAccess r + ) ) => DoAuth -> TeamId -> @@ -301,17 +317,15 @@ setFeatureStatusInternal :: GetConfigForTeamConstraints db cfg r, SetConfigForTeamConstraints db cfg r, FeaturePersistentConstraint db cfg, - Members - '[ ErrorS 'NotATeamMember, - ErrorS OperationDenied, - ErrorS 'TeamNotFound, - Error TeamFeatureError, - TeamStore, - TeamFeatureStore db, - P.Logger (Log.Msg -> Log.Msg), - GundeckAccess - ] - r + ( Member (ErrorS 'NotATeamMember) r, + Member (ErrorS OperationDenied) r, + Member (ErrorS 'TeamNotFound) r, + Member (Error TeamFeatureError) r, + Member TeamStore r, + Member (TeamFeatureStore db) r, + Member (P.Logger (Log.Msg -> Log.Msg)) r, + Member GundeckAccess r + ) ) => TeamId -> WithStatusNoLock cfg -> @@ -338,14 +352,10 @@ updateLockStatus tid lockStatus = do -- Here we explicitly return the team setting if the user is a team member. -- In `getConfigForUser` this is mostly also the case. But there are exceptions, e.g. `ConferenceCallingConfig` getFeatureStatusForUser :: - forall (db :: *) cfg r. - ( Members - '[ ErrorS 'NotATeamMember, - ErrorS OperationDenied, - ErrorS 'TeamNotFound, - TeamStore - ] - r, + forall (db :: Type) cfg r. + ( Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'TeamNotFound) r, + Member TeamStore r, GetConfigForTeamConstraints db cfg r, GetConfigForUserConstraints db cfg r, GetFeatureConfig db cfg @@ -365,17 +375,15 @@ getFeatureStatusForUser zusr = do getAllFeatureConfigsForUser :: forall db r. - Members - '[ BrigAccess, - ErrorS 'NotATeamMember, - ErrorS OperationDenied, - ErrorS 'TeamNotFound, - Input Opts, - LegalHoldStore, - TeamFeatureStore db, - TeamStore - ] - r => + ( Member BrigAccess r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS OperationDenied) r, + Member (ErrorS 'TeamNotFound) r, + Member (Input Opts) r, + Member LegalHoldStore r, + Member (TeamFeatureStore db) r, + Member TeamStore r + ) => FeaturePersistentAllFeatures db => UserId -> Sem r AllFeatureConfigs @@ -392,16 +400,12 @@ getAllFeatureConfigsForUser zusr = do getAllFeatureConfigsForTeam :: forall db r. - Members - '[ BrigAccess, - ErrorS 'NotATeamMember, - ErrorS OperationDenied, - Input Opts, - LegalHoldStore, - TeamFeatureStore db, - TeamStore - ] - r => + ( Member (ErrorS 'NotATeamMember) r, + Member (Input Opts) r, + Member LegalHoldStore r, + Member (TeamFeatureStore db) r, + Member TeamStore r + ) => FeaturePersistentAllFeatures db => Local UserId -> TeamId -> @@ -412,51 +416,40 @@ getAllFeatureConfigsForTeam luid tid = do getAllFeatureConfigsTeam @db tid getAllFeatureConfigsForServer :: - forall db r. - Members - '[ BrigAccess, - ErrorS 'NotATeamMember, - ErrorS 'TeamNotFound, - ErrorS OperationDenied, - Input Opts, - LegalHoldStore, - TeamFeatureStore db, - TeamStore - ] - r => - FeaturePersistentAllFeatures db => + forall r. + Member (Input Opts) r => Sem r AllFeatureConfigs getAllFeatureConfigsForServer = AllFeatureConfigs - <$> getConfigForServer @db @LegalholdConfig - <*> getConfigForServer @db @SSOConfig - <*> getConfigForServer @db @SearchVisibilityAvailableConfig - <*> getConfigForServer @db @SearchVisibilityInboundConfig - <*> getConfigForServer @db @ValidateSAMLEmailsConfig - <*> getConfigForServer @db @DigitalSignaturesConfig - <*> getConfigForServer @db @AppLockConfig - <*> getConfigForServer @db @FileSharingConfig - <*> getConfigForServer @db @ClassifiedDomainsConfig - <*> getConfigForServer @db @ConferenceCallingConfig - <*> getConfigForServer @db @SelfDeletingMessagesConfig - <*> getConfigForServer @db @GuestLinksConfig - <*> getConfigForServer @db @SndFactorPasswordChallengeConfig - <*> getConfigForServer @db @MLSConfig - <*> getConfigForServer @db @ExposeInvitationURLsToTeamAdminConfig + <$> getConfigForServer @LegalholdConfig + <*> getConfigForServer @SSOConfig + <*> getConfigForServer @SearchVisibilityAvailableConfig + <*> getConfigForServer @SearchVisibilityInboundConfig + <*> getConfigForServer @ValidateSAMLEmailsConfig + <*> getConfigForServer @DigitalSignaturesConfig + <*> getConfigForServer @AppLockConfig + <*> getConfigForServer @FileSharingConfig + <*> getConfigForServer @ClassifiedDomainsConfig + <*> getConfigForServer @ConferenceCallingConfig + <*> getConfigForServer @SelfDeletingMessagesConfig + <*> getConfigForServer @GuestLinksConfig + <*> getConfigForServer @SndFactorPasswordChallengeConfig + <*> getConfigForServer @MLSConfig + <*> getConfigForServer @ExposeInvitationURLsToTeamAdminConfig + <*> getConfigForServer @OutlookCalIntegrationConfig + <*> getConfigForServer @MlsE2EIdConfig getAllFeatureConfigsUser :: forall db r. - Members - '[ BrigAccess, - ErrorS 'NotATeamMember, - ErrorS 'TeamNotFound, - ErrorS OperationDenied, - Input Opts, - LegalHoldStore, - TeamFeatureStore db, - TeamStore - ] - r => + ( Member BrigAccess r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'TeamNotFound) r, + Member (ErrorS OperationDenied) r, + Member (Input Opts) r, + Member LegalHoldStore r, + Member (TeamFeatureStore db) r, + Member TeamStore r + ) => FeaturePersistentAllFeatures db => UserId -> Sem r AllFeatureConfigs @@ -477,19 +470,16 @@ getAllFeatureConfigsUser uid = <*> getConfigForUser @db @SndFactorPasswordChallengeConfig uid <*> getConfigForUser @db @MLSConfig uid <*> getConfigForUser @db @ExposeInvitationURLsToTeamAdminConfig uid + <*> getConfigForUser @db @OutlookCalIntegrationConfig uid + <*> getConfigForUser @db @MlsE2EIdConfig uid getAllFeatureConfigsTeam :: forall db r. - Members - '[ BrigAccess, - ErrorS 'NotATeamMember, - ErrorS OperationDenied, - Input Opts, - LegalHoldStore, - TeamFeatureStore db, - TeamStore - ] - r => + ( Member (Input Opts) r, + Member LegalHoldStore r, + Member (TeamFeatureStore db) r, + Member TeamStore r + ) => FeaturePersistentAllFeatures db => TeamId -> Sem r AllFeatureConfigs @@ -510,15 +500,16 @@ getAllFeatureConfigsTeam tid = <*> getConfigForTeam @db @SndFactorPasswordChallengeConfig tid <*> getConfigForTeam @db @MLSConfig tid <*> getConfigForTeam @db @ExposeInvitationURLsToTeamAdminConfig tid + <*> getConfigForTeam @db @OutlookCalIntegrationConfig tid + <*> getConfigForTeam @db @MlsE2EIdConfig tid -- | Note: this is an internal function which doesn't cover all features, e.g. LegalholdConfig genericGetConfigForTeam :: forall db cfg r. GetFeatureConfig db cfg => FeaturePersistentConstraint db cfg => - Members '[TeamFeatureStore db] r => - GetConfigForTeamConstraints db cfg r => - Members '[Input Opts] r => + Member (TeamFeatureStore db) r => + Member (Input Opts) r => TeamId -> Sem r (WithStatus cfg) genericGetConfigForTeam tid = do @@ -532,9 +523,8 @@ genericGetConfigForMultiTeam :: forall db cfg r. GetFeatureConfig db cfg => FeaturePersistentConstraint db cfg => - Members '[TeamFeatureStore db] r => - GetConfigForTeamConstraints db cfg r => - Members '[Input Opts] r => + Member (TeamFeatureStore db) r => + Member (Input Opts) r => [TeamId] -> Sem r [(TeamId, WithStatus cfg)] genericGetConfigForMultiTeam tids = do @@ -546,16 +536,11 @@ genericGetConfigForMultiTeam tids = do genericGetConfigForUser :: forall db cfg r. FeaturePersistentConstraint db cfg => - GetConfigForTeamConstraints db cfg r => - ( Members - '[ Input Opts, - TeamFeatureStore db, - ErrorS OperationDenied, - ErrorS 'NotATeamMember, - ErrorS 'TeamNotFound, - TeamStore - ] - r, + ( Member (Input Opts) r, + Member (TeamFeatureStore db) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'TeamNotFound) r, + Member TeamStore r, GetFeatureConfig db cfg ) => UserId -> @@ -572,21 +557,18 @@ genericGetConfigForUser uid = do genericGetConfigForTeam @db tid persistAndPushEvent :: - forall (db :: *) cfg r. + forall (db :: Type) cfg r. ( IsFeatureConfig cfg, KnownSymbol (FeatureSymbol cfg), ToSchema cfg, GetFeatureConfig db cfg, FeaturePersistentConstraint db cfg, GetConfigForTeamConstraints db cfg r, - Show cfg, - Members - '[ TeamFeatureStore db, - P.Logger (Log.Msg -> Log.Msg), - GundeckAccess, - TeamStore - ] - r + ( Member (TeamFeatureStore db) r, + Member (P.Logger (Log.Msg -> Log.Msg)) r, + Member GundeckAccess r, + Member TeamStore r + ) ) => TeamId -> WithStatusNoLock cfg -> @@ -598,7 +580,10 @@ persistAndPushEvent tid wsnl = do pure fs pushFeatureConfigEvent :: - Members '[GundeckAccess, TeamStore, P.TinyLog] r => + ( Member GundeckAccess r, + Member TeamStore r, + Member P.TinyLog r + ) => TeamId -> Event.Event -> Sem r () @@ -638,7 +623,7 @@ instance GetFeatureConfig db SSOConfig where getConfigForUser = genericGetConfigForUser @db instance SetFeatureConfig db SSOConfig where - type SetConfigForTeamConstraints db SSOConfig (r :: EffectRow) = (Members '[Error TeamFeatureError] r) + type SetConfigForTeamConstraints db SSOConfig (r :: EffectRow) = (Member (Error TeamFeatureError) r) setConfigForTeam tid wsnl = do case wssStatus wsnl of @@ -655,7 +640,7 @@ instance GetFeatureConfig db SearchVisibilityAvailableConfig where pure $ setStatus status defFeatureStatus instance SetFeatureConfig db SearchVisibilityAvailableConfig where - type SetConfigForTeamConstraints db SearchVisibilityAvailableConfig (r :: EffectRow) = (Members '[SearchVisibilityStore] r) + type SetConfigForTeamConstraints db SearchVisibilityAvailableConfig (r :: EffectRow) = (Member SearchVisibilityStore r) setConfigForTeam tid wsnl = do case wssStatus wsnl of @@ -667,40 +652,35 @@ instance GetFeatureConfig db ValidateSAMLEmailsConfig where getConfigForServer = inputs (view (optSettings . setFeatureFlags . flagsTeamFeatureValidateSAMLEmailsStatus . unDefaults . unImplicitLockStatus)) -instance SetFeatureConfig db ValidateSAMLEmailsConfig where - setConfigForTeam tid wsnl = persistAndPushEvent @db tid wsnl +instance SetFeatureConfig db ValidateSAMLEmailsConfig -instance GetFeatureConfig db DigitalSignaturesConfig where - -- FUTUREWORK: we may also want to get a default from the server config file here, like for - -- sso, and team search visibility. - getConfigForServer = pure defFeatureStatus +instance GetFeatureConfig db DigitalSignaturesConfig -instance SetFeatureConfig db DigitalSignaturesConfig where - setConfigForTeam tid wsnl = persistAndPushEvent @db tid wsnl +instance SetFeatureConfig db DigitalSignaturesConfig instance GetFeatureConfig db LegalholdConfig where type GetConfigForTeamConstraints db LegalholdConfig (r :: EffectRow) = ( FeaturePersistentConstraint db LegalholdConfig, - Members '[Input Opts, TeamFeatureStore db, LegalHoldStore, TeamStore] r + ( Member (Input Opts) r, + Member (TeamFeatureStore db) r, + Member LegalHoldStore r, + Member TeamStore r + ) ) type GetConfigForUserConstraints db LegalholdConfig (r :: EffectRow) = ( FeaturePersistentConstraint db LegalholdConfig, - Members - '[ Input Opts, - TeamFeatureStore db, - LegalHoldStore, - TeamStore, - ErrorS OperationDenied, - ErrorS 'NotATeamMember, - ErrorS 'TeamNotFound - ] - r + ( Member (Input Opts) r, + Member (TeamFeatureStore db) r, + Member LegalHoldStore r, + Member TeamStore r, + Member (ErrorS OperationDenied) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'TeamNotFound) r + ) ) - getConfigForServer = pure defFeatureStatus - getConfigForTeam tid = do status <- isLegalHoldEnabledForTeam @db tid <&> \case @@ -708,49 +688,41 @@ instance GetFeatureConfig db LegalholdConfig where False -> FeatureStatusDisabled pure $ setStatus status defFeatureStatus -instance - ( CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" - ) => - SetFeatureConfig db LegalholdConfig - where +instance SetFeatureConfig db LegalholdConfig where type SetConfigForTeamConstraints db LegalholdConfig (r :: EffectRow) = ( Bounded (PagingBounds InternalPaging TeamMember), - Members - '[ BotAccess, - BrigAccess, - CodeStore, - ConversationStore, - Error AuthenticationError, - Error InternalError, - ErrorS ('ActionDenied 'RemoveConversationMember), - ErrorS 'CannotEnableLegalHoldServiceLargeTeam, - ErrorS 'NotATeamMember, - Error TeamFeatureError, - ErrorS 'LegalHoldNotEnabled, - ErrorS 'LegalHoldDisableUnimplemented, - ErrorS 'LegalHoldServiceNotRegistered, - ErrorS 'UserLegalHoldIllegalOperation, - ErrorS 'LegalHoldCouldNotBlockConnections, - ExternalAccess, - FederatorAccess, - FireAndForget, - GundeckAccess, - Input (Local ()), - Input Env, - Input UTCTime, - LegalHoldStore, - ListItems LegacyPaging ConvId, - MemberStore, - ProposalStore, - TeamFeatureStore db, - TeamStore, - TeamMemberStore InternalPaging, - P.TinyLog - ] - r, + ( Member BotAccess r, + Member BrigAccess r, + Member CodeStore r, + Member ConversationStore r, + Member (Error AuthenticationError) r, + Member (Error InternalError) r, + Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, + Member (ErrorS 'CannotEnableLegalHoldServiceLargeTeam) r, + Member (ErrorS 'NotATeamMember) r, + Member (Error TeamFeatureError) r, + Member (ErrorS 'LegalHoldNotEnabled) r, + Member (ErrorS 'LegalHoldDisableUnimplemented) r, + Member (ErrorS 'LegalHoldServiceNotRegistered) r, + Member (ErrorS 'UserLegalHoldIllegalOperation) r, + Member (ErrorS 'LegalHoldCouldNotBlockConnections) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member FireAndForget r, + Member GundeckAccess r, + Member (Input (Local ())) r, + Member (Input Env) r, + Member (Input UTCTime) r, + Member LegalHoldStore r, + Member (ListItems LegacyPaging ConvId) r, + Member MemberStore r, + Member ProposalStore r, + Member (TeamFeatureStore db) r, + Member TeamStore r, + Member (TeamMemberStore InternalPaging) r, + Member P.TinyLog r + ), FeaturePersistentConstraint db LegalholdConfig ) @@ -777,15 +749,14 @@ instance GetFeatureConfig db FileSharingConfig where getConfigForServer = input <&> view (optSettings . setFeatureFlags . flagFileSharing . unDefaults) -instance SetFeatureConfig db FileSharingConfig where - setConfigForTeam tid wsnl = persistAndPushEvent @db tid wsnl +instance SetFeatureConfig db FileSharingConfig instance GetFeatureConfig db AppLockConfig where getConfigForServer = input <&> view (optSettings . setFeatureFlags . flagAppLockDefaults . unDefaults . unImplicitLockStatus) instance SetFeatureConfig db AppLockConfig where - type SetConfigForTeamConstraints db AppLockConfig r = Members '[Error TeamFeatureError] r + type SetConfigForTeamConstraints db AppLockConfig r = Member (Error TeamFeatureError) r setConfigForTeam tid wsnl = do when ((applockInactivityTimeoutSecs . wssConfig $ wsnl) < 30) $ @@ -800,16 +771,14 @@ instance GetFeatureConfig db ConferenceCallingConfig where type GetConfigForUserConstraints db ConferenceCallingConfig r = ( FeaturePersistentConstraint db ConferenceCallingConfig, - Members - '[ Input Opts, - ErrorS OperationDenied, - ErrorS 'NotATeamMember, - ErrorS 'TeamNotFound, - TeamStore, - TeamFeatureStore db, - BrigAccess - ] - r + ( Member (Input Opts) r, + Member (ErrorS OperationDenied) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'TeamNotFound) r, + Member TeamStore r, + Member (TeamFeatureStore db) r, + Member BrigAccess r + ) ) getConfigForServer = @@ -819,32 +788,28 @@ instance GetFeatureConfig db ConferenceCallingConfig where wsnl <- getAccountConferenceCallingConfigClient uid pure $ withLockStatus (wsLockStatus (defFeatureStatus @ConferenceCallingConfig)) wsnl -instance SetFeatureConfig db ConferenceCallingConfig where - setConfigForTeam tid wsnl = persistAndPushEvent @db tid wsnl +instance SetFeatureConfig db ConferenceCallingConfig instance GetFeatureConfig db SelfDeletingMessagesConfig where getConfigForServer = input <&> view (optSettings . setFeatureFlags . flagSelfDeletingMessages . unDefaults) -instance SetFeatureConfig db SelfDeletingMessagesConfig where - setConfigForTeam tid wsnl = persistAndPushEvent @db tid wsnl +instance SetFeatureConfig db SelfDeletingMessagesConfig -instance SetFeatureConfig db GuestLinksConfig where - setConfigForTeam tid wsnl = persistAndPushEvent @db tid wsnl +instance SetFeatureConfig db GuestLinksConfig instance GetFeatureConfig db GuestLinksConfig where getConfigForServer = input <&> view (optSettings . setFeatureFlags . flagConversationGuestLinks . unDefaults) -instance SetFeatureConfig db SndFactorPasswordChallengeConfig where - setConfigForTeam tid wsnl = persistAndPushEvent @db tid wsnl +instance SetFeatureConfig db SndFactorPasswordChallengeConfig instance GetFeatureConfig db SndFactorPasswordChallengeConfig where getConfigForServer = input <&> view (optSettings . setFeatureFlags . flagTeamFeatureSndFactorPasswordChallengeStatus . unDefaults) instance SetFeatureConfig db SearchVisibilityInboundConfig where - type SetConfigForTeamConstraints db SearchVisibilityInboundConfig (r :: EffectRow) = (Members '[BrigAccess] r) + type SetConfigForTeamConstraints db SearchVisibilityInboundConfig (r :: EffectRow) = (Member BrigAccess r) setConfigForTeam tid wsnl = do updateSearchVisibilityInbound $ toTeamStatus tid wsnl persistAndPushEvent @db tid wsnl @@ -857,20 +822,9 @@ instance GetFeatureConfig db MLSConfig where getConfigForServer = input <&> view (optSettings . setFeatureFlags . flagMLS . unDefaults . unImplicitLockStatus) -instance SetFeatureConfig db MLSConfig where - setConfigForTeam tid wsnl = do - persistAndPushEvent @db tid wsnl +instance SetFeatureConfig db MLSConfig instance GetFeatureConfig db ExposeInvitationURLsToTeamAdminConfig where - getConfigForServer = - -- we could look at the galley settings, but we don't have a team here, so there is not much else we can say. - pure $ - withStatus - FeatureStatusDisabled - LockStatusLocked - ExposeInvitationURLsToTeamAdminConfig - FeatureTTLUnlimited - getConfigForTeam tid = do allowList <- input <&> view (optSettings . setExposeInvitationURLsTeamAllowlist . to (fromMaybe [])) mbOldStatus <- TeamFeatures.getFeatureConfig @db (Proxy @ExposeInvitationURLsToTeamAdminConfig) tid <&> fmap wssStatus @@ -891,13 +845,19 @@ instance GetFeatureConfig db ExposeInvitationURLsToTeamAdminConfig where ExposeInvitationURLsToTeamAdminConfig FeatureTTLUnlimited -instance SetFeatureConfig db ExposeInvitationURLsToTeamAdminConfig where - type SetConfigForTeamConstraints db ExposeInvitationURLsToTeamAdminConfig (r :: EffectRow) = (Member (ErrorS OperationDenied) r) - setConfigForTeam tid wsnl = do - lockStatus <- getConfigForTeam @db @ExposeInvitationURLsToTeamAdminConfig tid <&> wsLockStatus - case lockStatus of - LockStatusLocked -> throwS @OperationDenied - LockStatusUnlocked -> persistAndPushEvent @db tid wsnl +instance SetFeatureConfig db ExposeInvitationURLsToTeamAdminConfig + +instance SetFeatureConfig db OutlookCalIntegrationConfig + +instance GetFeatureConfig db OutlookCalIntegrationConfig where + getConfigForServer = + input <&> view (optSettings . setFeatureFlags . flagOutlookCalIntegration . unDefaults) + +instance SetFeatureConfig db MlsE2EIdConfig + +instance GetFeatureConfig db MlsE2EIdConfig where + getConfigForServer = + input <&> view (optSettings . setFeatureFlags . flagMlsE2EId . unDefaults) -- -- | If second factor auth is enabled, make sure that end-points that don't support it, but should, are blocked completely. (This is a workaround until we have 2FA for those end-points as well.) -- -- @@ -935,13 +895,11 @@ featureEnabledForTeam :: forall db cfg r. ( GetFeatureConfig db cfg, GetConfigForTeamConstraints db cfg r, - Members - '[ ErrorS OperationDenied, - ErrorS 'NotATeamMember, - ErrorS 'TeamNotFound, - TeamStore - ] - r + ( Member (ErrorS OperationDenied) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'TeamNotFound) r, + Member TeamStore r + ) ) => TeamId -> Sem r Bool diff --git a/services/galley/src/Galley/API/Teams/Notifications.hs b/services/galley/src/Galley/API/Teams/Notifications.hs index fe32f47ecd..8d34b4bedc 100644 --- a/services/galley/src/Galley/API/Teams/Notifications.hs +++ b/services/galley/src/Galley/API/Teams/Notifications.hs @@ -57,7 +57,10 @@ import Wire.API.Internal.Notification import Wire.API.User getTeamNotifications :: - Members '[BrigAccess, ErrorS 'TeamNotFound, TeamNotificationStore] r => + ( Member BrigAccess r, + Member (ErrorS 'TeamNotFound) r, + Member TeamNotificationStore r + ) => UserId -> Maybe NotificationId -> Range 1 10000 Int32 -> diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index 1065f4f92e..b431ed747e 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.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 RecordWildCards #-} module Galley.API.Update ( -- * Managing Conversations @@ -55,8 +56,8 @@ module Galley.API.Update postOtrMessageUnqualified, postProteusBroadcast, postOtrBroadcastUnqualified, - isTypingUnqualified, - isTypingQualified, + memberTypingUnqualified, + memberTyping, -- * External Services addServiceH, @@ -138,19 +139,16 @@ import Wire.API.Team.Member import Wire.API.User.Client acceptConvH :: - Members - '[ ConversationStore, - Error InternalError, - ErrorS 'ConvNotFound, - ErrorS 'InvalidOperation, - ErrorS 'NotConnected, - GundeckAccess, - Input (Local ()), - Input UTCTime, - MemberStore, - TinyLog - ] - r => + ( Member ConversationStore r, + Member (Error InternalError) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member GundeckAccess r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member MemberStore r, + Member TinyLog r + ) => UserId ::: Maybe ConnId ::: ConvId -> Sem r Response acceptConvH (usr ::: conn ::: cnv) = do @@ -158,18 +156,15 @@ acceptConvH (usr ::: conn ::: cnv) = do setStatus status200 . json <$> acceptConv lusr conn cnv acceptConv :: - Members - '[ ConversationStore, - Error InternalError, - ErrorS 'ConvNotFound, - ErrorS 'InvalidOperation, - ErrorS 'NotConnected, - GundeckAccess, - Input UTCTime, - MemberStore, - TinyLog - ] - r => + ( Member ConversationStore r, + Member (Error InternalError) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member GundeckAccess r, + Member (Input UTCTime) r, + Member MemberStore r, + Member TinyLog r + ) => Local UserId -> Maybe ConnId -> ConvId -> @@ -181,26 +176,22 @@ acceptConv lusr conn cnv = do conversationView lusr conv' blockConvH :: - Members - '[ ConversationStore, - ErrorS 'ConvNotFound, - ErrorS 'InvalidOperation, - MemberStore - ] - r => + ( Member ConversationStore r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member MemberStore r + ) => UserId ::: ConvId -> Sem r Response blockConvH (zusr ::: cnv) = empty <$ blockConv zusr cnv blockConv :: - Members - '[ ConversationStore, - ErrorS 'ConvNotFound, - ErrorS 'InvalidOperation, - MemberStore - ] - r => + ( Member ConversationStore r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member MemberStore r + ) => UserId -> ConvId -> Sem r () @@ -213,19 +204,16 @@ blockConv zusr cnv = do E.deleteMembers cnv (UserList [zusr] []) unblockConvH :: - Members - '[ ConversationStore, - Error InternalError, - ErrorS 'ConvNotFound, - ErrorS 'InvalidOperation, - ErrorS 'NotConnected, - GundeckAccess, - Input (Local ()), - Input UTCTime, - MemberStore, - TinyLog - ] - r => + ( Member ConversationStore r, + Member (Error InternalError) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member GundeckAccess r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member MemberStore r, + Member TinyLog r + ) => UserId ::: Maybe ConnId ::: ConvId -> Sem r Response unblockConvH (usr ::: conn ::: cnv) = do @@ -233,18 +221,15 @@ unblockConvH (usr ::: conn ::: cnv) = do setStatus status200 . json <$> unblockConv lusr conn cnv unblockConv :: - Members - '[ ConversationStore, - Error InternalError, - ErrorS 'ConvNotFound, - ErrorS 'InvalidOperation, - ErrorS 'NotConnected, - GundeckAccess, - Input UTCTime, - MemberStore, - TinyLog - ] - r => + ( Member ConversationStore r, + Member (Error InternalError) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member GundeckAccess r, + Member (Input UTCTime) r, + Member MemberStore r, + Member TinyLog r + ) => Local UserId -> Maybe ConnId -> ConvId -> @@ -290,10 +275,7 @@ type UpdateConversationAccessEffects = ] updateConversationAccess :: - ( Members UpdateConversationAccessEffects r, - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Galley "on-conversation-updated" + ( Members UpdateConversationAccessEffects r ) => Local UserId -> ConnId -> @@ -306,10 +288,7 @@ updateConversationAccess lusr con qcnv update = do updateLocalConversation @'ConversationAccessDataTag lcnv (tUntagged lusr) (Just con) update updateConversationAccessUnqualified :: - ( Members UpdateConversationAccessEffects r, - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Galley "on-conversation-updated" + ( Members UpdateConversationAccessEffects r ) => Local UserId -> ConnId -> @@ -325,27 +304,19 @@ updateConversationAccessUnqualified lusr con cnv update = update updateConversationReceiptMode :: - ( Members - '[ BrigAccess, - ConversationStore, - Error FederationError, - ErrorS ('ActionDenied 'ModifyConversationReceiptMode), - ErrorS 'ConvNotFound, - ErrorS 'InvalidOperation, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input (Local ()), - Input Env, - Input UTCTime, - MemberStore, - TinyLog - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Galley "update-conversation" + ( Member BrigAccess r, + Member ConversationStore r, + Member (Error FederationError) r, + Member (ErrorS ('ActionDenied 'ModifyConversationReceiptMode)) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member MemberStore r, + Member TinyLog r ) => Local UserId -> ConnId -> @@ -369,21 +340,15 @@ updateConversationReceiptMode lusr zcon qcnv update = updateRemoteConversation :: forall tag r. - ( Members - '[ BrigAccess, - Error FederationError, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input (Local ()), - MemberStore, - TinyLog - ] - r, - Members (HasConversationActionGalleyErrors tag) r, + ( Member BrigAccess r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input (Local ())) r, + Member MemberStore r, + Member TinyLog r, RethrowErrors (HasConversationActionGalleyErrors tag) (Error NoChanges : r), - SingI tag, - CallsFed 'Galley "update-conversation" + SingI tag ) => Remote ConvId -> Local UserId -> @@ -407,27 +372,19 @@ updateRemoteConversation rcnv lusr conn action = getUpdateResult $ do notifyRemoteConversationAction lusr (qualifyAs rcnv convUpdate) (Just conn) updateConversationReceiptModeUnqualified :: - ( Members - '[ BrigAccess, - ConversationStore, - Error FederationError, - ErrorS ('ActionDenied 'ModifyConversationReceiptMode), - ErrorS 'ConvNotFound, - ErrorS 'InvalidOperation, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input (Local ()), - Input Env, - Input UTCTime, - MemberStore, - TinyLog - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation", - CallsFed 'Galley "update-conversation" + ( Member BrigAccess r, + Member ConversationStore r, + Member (Error FederationError) r, + Member (ErrorS ('ActionDenied 'ModifyConversationReceiptMode)) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member MemberStore r, + Member TinyLog r ) => Local UserId -> ConnId -> @@ -437,22 +394,15 @@ updateConversationReceiptModeUnqualified :: updateConversationReceiptModeUnqualified lusr zcon cnv = updateConversationReceiptMode lusr zcon (tUntagged (qualifyAs lusr cnv)) updateConversationMessageTimer :: - ( Members - '[ ConversationStore, - ErrorS ('ActionDenied 'ModifyConversationMessageTimer), - ErrorS 'ConvNotFound, - ErrorS 'InvalidOperation, - Error FederationError, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input UTCTime - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member ConversationStore r, + Member (ErrorS ('ActionDenied 'ModifyConversationMessageTimer)) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member (Error FederationError) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input UTCTime) r ) => Local UserId -> ConnId -> @@ -476,22 +426,15 @@ updateConversationMessageTimer lusr zcon qcnv update = qcnv updateConversationMessageTimerUnqualified :: - ( Members - '[ ConversationStore, - ErrorS ('ActionDenied 'ModifyConversationMessageTimer), - ErrorS 'ConvNotFound, - ErrorS 'InvalidOperation, - Error FederationError, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input UTCTime - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member ConversationStore r, + Member (ErrorS ('ActionDenied 'ModifyConversationMessageTimer)) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member (Error FederationError) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input UTCTime) r ) => Local UserId -> ConnId -> @@ -501,25 +444,18 @@ updateConversationMessageTimerUnqualified :: updateConversationMessageTimerUnqualified lusr zcon cnv = updateConversationMessageTimer lusr zcon (tUntagged (qualifyAs lusr cnv)) deleteLocalConversation :: - ( Members - '[ CodeStore, - ConversationStore, - Error FederationError, - ErrorS 'NotATeamMember, - ErrorS ('ActionDenied 'DeleteConversation), - ErrorS 'ConvNotFound, - ErrorS 'InvalidOperation, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input UTCTime, - TeamStore - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member CodeStore r, + Member ConversationStore r, + Member (Error FederationError) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS ('ActionDenied 'DeleteConversation)) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input UTCTime) r, + Member TeamStore r ) => Local UserId -> ConnId -> @@ -608,17 +544,15 @@ addCode lusr zcon lcnv = do $ throwS @'ConvAccessDenied rmCodeUnqualified :: - Members - '[ CodeStore, - ConversationStore, - ErrorS 'ConvNotFound, - ErrorS 'ConvAccessDenied, - ExternalAccess, - GundeckAccess, - Input (Local ()), - Input UTCTime - ] - r => + ( Member CodeStore r, + Member ConversationStore r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'ConvAccessDenied) r, + Member ExternalAccess r, + Member GundeckAccess r, + Member (Input (Local ())) r, + Member (Input UTCTime) r + ) => Local UserId -> ConnId -> ConvId -> @@ -628,16 +562,14 @@ rmCodeUnqualified lusr zcon cnv = do rmCode lusr zcon lcnv rmCode :: - Members - '[ CodeStore, - ConversationStore, - ErrorS 'ConvAccessDenied, - ErrorS 'ConvNotFound, - ExternalAccess, - GundeckAccess, - Input UTCTime - ] - r => + ( Member CodeStore r, + Member ConversationStore r, + Member (ErrorS 'ConvAccessDenied) r, + Member (ErrorS 'ConvNotFound) r, + Member ExternalAccess r, + Member GundeckAccess r, + Member (Input UTCTime) r + ) => Local UserId -> ConnId -> Local ConvId -> @@ -686,15 +618,12 @@ returnCode c = do checkReusableCode :: forall db r. - ( Members - '[ CodeStore, - ConversationStore, - TeamFeatureStore db, - ErrorS 'CodeNotFound, - ErrorS 'ConvNotFound, - Input Opts - ] - r, + ( Member CodeStore r, + Member ConversationStore r, + Member (TeamFeatureStore db) r, + Member (ErrorS 'CodeNotFound) r, + Member (ErrorS 'ConvNotFound) r, + Member (Input Opts) r, FeaturePersistentConstraint db GuestLinksConfig ) => ConversationCode -> @@ -707,30 +636,25 @@ checkReusableCode convCode = do joinConversationByReusableCode :: forall db r. - ( Members - '[ BrigAccess, - CodeStore, - ConversationStore, - ErrorS 'CodeNotFound, - ErrorS 'ConvAccessDenied, - ErrorS 'ConvNotFound, - ErrorS 'GuestLinksDisabled, - ErrorS 'InvalidOperation, - ErrorS 'NotATeamMember, - ErrorS 'TooManyMembers, - FederatorAccess, - ExternalAccess, - GundeckAccess, - Input Opts, - Input UTCTime, - MemberStore, - TeamStore, - TeamFeatureStore db - ] - r, - FeaturePersistentConstraint db GuestLinksConfig, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation" + ( Member BrigAccess r, + Member CodeStore r, + Member ConversationStore r, + Member (ErrorS 'CodeNotFound) r, + Member (ErrorS 'ConvAccessDenied) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'GuestLinksDisabled) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'TooManyMembers) r, + Member FederatorAccess r, + Member ExternalAccess r, + Member GundeckAccess r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member MemberStore r, + Member TeamStore r, + Member (TeamFeatureStore db) r, + FeaturePersistentConstraint db GuestLinksConfig ) => Local UserId -> ConnId -> @@ -740,30 +664,24 @@ joinConversationByReusableCode lusr zcon convCode = do c <- verifyReusableCode convCode conv <- E.getConversation (codeConversation c) >>= noteS @'ConvNotFound Query.ensureGuestLinksEnabled @db (Data.convTeam conv) - joinConversation @db lusr zcon conv CodeAccess + joinConversation lusr zcon conv CodeAccess joinConversationById :: - forall db r. - ( Members - '[ BrigAccess, - FederatorAccess, - ConversationStore, - ErrorS 'ConvAccessDenied, - ErrorS 'ConvNotFound, - ErrorS 'InvalidOperation, - ErrorS 'NotATeamMember, - ErrorS 'TooManyMembers, - ExternalAccess, - GundeckAccess, - Input Opts, - Input UTCTime, - MemberStore, - TeamStore, - TeamFeatureStore db - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation" + forall r. + ( Member BrigAccess r, + Member FederatorAccess r, + Member ConversationStore r, + Member (ErrorS 'ConvAccessDenied) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'TooManyMembers) r, + Member ExternalAccess r, + Member GundeckAccess r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member MemberStore r, + Member TeamStore r ) => Local UserId -> ConnId -> @@ -771,28 +689,22 @@ joinConversationById :: Sem r (UpdateResult Event) joinConversationById lusr zcon cnv = do conv <- E.getConversation cnv >>= noteS @'ConvNotFound - joinConversation @db lusr zcon conv LinkAccess + joinConversation lusr zcon conv LinkAccess joinConversation :: - ( Members - '[ BrigAccess, - ConversationStore, - FederatorAccess, - ErrorS 'ConvAccessDenied, - ErrorS 'InvalidOperation, - ErrorS 'NotATeamMember, - ErrorS 'TooManyMembers, - ExternalAccess, - GundeckAccess, - Input Opts, - Input UTCTime, - MemberStore, - TeamStore, - TeamFeatureStore db - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-new-remote-conversation" + forall r. + ( Member BrigAccess r, + Member FederatorAccess r, + Member (ErrorS 'ConvAccessDenied) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'TooManyMembers) r, + Member ExternalAccess r, + Member GundeckAccess r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member MemberStore r, + Member TeamStore r ) => Local UserId -> ConnId -> @@ -823,36 +735,30 @@ joinConversation lusr zcon conv access = do action addMembers :: - ( Members - '[ BrigAccess, - ConversationStore, - Error FederationError, - Error InternalError, - ErrorS ('ActionDenied 'AddConversationMember), - ErrorS ('ActionDenied 'LeaveConversation), - ErrorS 'ConvAccessDenied, - ErrorS 'ConvNotFound, - ErrorS 'InvalidOperation, - ErrorS 'NotConnected, - ErrorS 'NotATeamMember, - ErrorS 'TooManyMembers, - ErrorS 'MissingLegalholdConsent, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input Opts, - Input UTCTime, - LegalHoldStore, - MemberStore, - ProposalStore, - TeamStore, - TinyLog - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member BrigAccess r, + Member ConversationStore r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member (ErrorS ('ActionDenied 'AddConversationMember)) r, + Member (ErrorS ('ActionDenied 'LeaveConversation)) r, + Member (ErrorS 'ConvAccessDenied) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'NotConnected) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'TooManyMembers) r, + Member (ErrorS 'MissingLegalholdConsent) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member LegalHoldStore r, + Member MemberStore r, + Member ProposalStore r, + Member TeamStore r, + Member TinyLog r ) => Local UserId -> ConnId -> @@ -866,36 +772,30 @@ addMembers lusr zcon qcnv (InviteQualified users role) = do ConversationJoin users role addMembersUnqualifiedV2 :: - ( Members - '[ BrigAccess, - ConversationStore, - Error FederationError, - Error InternalError, - ErrorS ('ActionDenied 'AddConversationMember), - ErrorS ('ActionDenied 'LeaveConversation), - ErrorS 'ConvAccessDenied, - ErrorS 'ConvNotFound, - ErrorS 'InvalidOperation, - ErrorS 'NotConnected, - ErrorS 'NotATeamMember, - ErrorS 'TooManyMembers, - ErrorS 'MissingLegalholdConsent, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input Opts, - Input UTCTime, - LegalHoldStore, - MemberStore, - ProposalStore, - TeamStore, - TinyLog - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member BrigAccess r, + Member ConversationStore r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member (ErrorS ('ActionDenied 'AddConversationMember)) r, + Member (ErrorS ('ActionDenied 'LeaveConversation)) r, + Member (ErrorS 'ConvAccessDenied) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'NotConnected) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'TooManyMembers) r, + Member (ErrorS 'MissingLegalholdConsent) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member LegalHoldStore r, + Member MemberStore r, + Member ProposalStore r, + Member TeamStore r, + Member TinyLog r ) => Local UserId -> ConnId -> @@ -909,36 +809,30 @@ addMembersUnqualifiedV2 lusr zcon cnv (InviteQualified users role) = do ConversationJoin users role addMembersUnqualified :: - ( Members - '[ BrigAccess, - ConversationStore, - Error FederationError, - Error InternalError, - ErrorS ('ActionDenied 'AddConversationMember), - ErrorS ('ActionDenied 'LeaveConversation), - ErrorS 'ConvAccessDenied, - ErrorS 'ConvNotFound, - ErrorS 'InvalidOperation, - ErrorS 'NotConnected, - ErrorS 'NotATeamMember, - ErrorS 'TooManyMembers, - ErrorS 'MissingLegalholdConsent, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input Opts, - Input UTCTime, - LegalHoldStore, - MemberStore, - ProposalStore, - TeamStore, - TinyLog - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member BrigAccess r, + Member ConversationStore r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member (ErrorS ('ActionDenied 'AddConversationMember)) r, + Member (ErrorS ('ActionDenied 'LeaveConversation)) r, + Member (ErrorS 'ConvAccessDenied) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'NotConnected) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'TooManyMembers) r, + Member (ErrorS 'MissingLegalholdConsent) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member LegalHoldStore r, + Member MemberStore r, + Member ProposalStore r, + Member TeamStore r, + Member TinyLog r ) => Local UserId -> ConnId -> @@ -950,15 +844,13 @@ addMembersUnqualified lusr zcon cnv (Invite users role) = do addMembers lusr zcon (tUntagged (qualifyAs lusr cnv)) (InviteQualified qusers role) updateSelfMember :: - Members - '[ ConversationStore, - ErrorS 'ConvNotFound, - ExternalAccess, - GundeckAccess, - Input UTCTime, - MemberStore - ] - r => + ( Member ConversationStore r, + Member (ErrorS 'ConvNotFound) r, + Member ExternalAccess r, + Member GundeckAccess r, + Member (Input UTCTime) r, + Member MemberStore r + ) => Local UserId -> ConnId -> Qualified ConvId -> @@ -973,14 +865,14 @@ updateSelfMember lusr zcon qcnv update = do pushConversationEvent (Just zcon) e (fmap pure lusr) [] where checkLocalMembership :: - Members '[MemberStore] r => + Member MemberStore r => Local ConvId -> Sem r Bool checkLocalMembership lcnv = isMember (tUnqualified lusr) <$> E.getLocalMembers (tUnqualified lcnv) checkRemoteMembership :: - Members '[ConversationStore] r => + Member ConversationStore r => Remote ConvId -> Sem r Bool checkRemoteMembership rcnv = @@ -999,15 +891,13 @@ updateSelfMember lusr zcon qcnv update = do } updateUnqualifiedSelfMember :: - Members - '[ ConversationStore, - ErrorS 'ConvNotFound, - ExternalAccess, - GundeckAccess, - Input UTCTime, - MemberStore - ] - r => + ( Member ConversationStore r, + Member (ErrorS 'ConvNotFound) r, + Member ExternalAccess r, + Member GundeckAccess r, + Member (Input UTCTime) r, + Member MemberStore r + ) => Local UserId -> ConnId -> ConvId -> @@ -1018,24 +908,17 @@ updateUnqualifiedSelfMember lusr zcon cnv update = do updateSelfMember lusr zcon (tUntagged lcnv) update updateOtherMemberLocalConv :: - ( Members - '[ ConversationStore, - ErrorS ('ActionDenied 'ModifyOtherConversationMember), - ErrorS 'InvalidTarget, - ErrorS 'InvalidOperation, - ErrorS 'ConvNotFound, - ErrorS 'ConvMemberNotFound, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input UTCTime, - MemberStore - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member ConversationStore r, + Member (ErrorS ('ActionDenied 'ModifyOtherConversationMember)) r, + Member (ErrorS 'InvalidTarget) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'ConvMemberNotFound) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input UTCTime) r, + Member MemberStore r ) => Local ConvId -> Local UserId -> @@ -1050,24 +933,17 @@ updateOtherMemberLocalConv lcnv lusr con qvictim update = void . getUpdateResult ConversationMemberUpdate qvictim update updateOtherMemberUnqualified :: - ( Members - '[ ConversationStore, - ErrorS ('ActionDenied 'ModifyOtherConversationMember), - ErrorS 'InvalidTarget, - ErrorS 'InvalidOperation, - ErrorS 'ConvNotFound, - ErrorS 'ConvMemberNotFound, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input UTCTime, - MemberStore - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member ConversationStore r, + Member (ErrorS ('ActionDenied 'ModifyOtherConversationMember)) r, + Member (ErrorS 'InvalidTarget) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'ConvMemberNotFound) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input UTCTime) r, + Member MemberStore r ) => Local UserId -> ConnId -> @@ -1081,25 +957,18 @@ updateOtherMemberUnqualified lusr zcon cnv victim update = do updateOtherMemberLocalConv lcnv lusr zcon (tUntagged lvictim) update updateOtherMember :: - ( Members - '[ ConversationStore, - Error FederationError, - ErrorS ('ActionDenied 'ModifyOtherConversationMember), - ErrorS 'InvalidTarget, - ErrorS 'InvalidOperation, - ErrorS 'ConvNotFound, - ErrorS 'ConvMemberNotFound, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input UTCTime, - MemberStore - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member ConversationStore r, + Member (Error FederationError) r, + Member (ErrorS ('ActionDenied 'ModifyOtherConversationMember)) r, + Member (ErrorS 'InvalidTarget) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'ConvMemberNotFound) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input UTCTime) r, + Member MemberStore r ) => Local UserId -> ConnId -> @@ -1122,26 +991,19 @@ updateOtherMemberRemoteConv :: updateOtherMemberRemoteConv _ _ _ _ _ = throw FederationNotImplemented removeMemberUnqualified :: - ( Members - '[ ConversationStore, - Error InternalError, - ErrorS ('ActionDenied 'RemoveConversationMember), - ErrorS 'ConvNotFound, - ErrorS 'InvalidOperation, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input UTCTime, - MemberStore, - ProposalStore, - TinyLog - ] - r, - CallsFed 'Galley "leave-conversation", - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member ConversationStore r, + Member (Error InternalError) r, + Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input UTCTime) r, + Member MemberStore r, + Member ProposalStore r, + Member TinyLog r ) => Local UserId -> ConnId -> @@ -1154,26 +1016,19 @@ removeMemberUnqualified lusr con cnv victim = do removeMemberQualified lusr con (tUntagged lcnv) (tUntagged lvictim) removeMemberQualified :: - ( Members - '[ ConversationStore, - Error InternalError, - ErrorS ('ActionDenied 'RemoveConversationMember), - ErrorS 'ConvNotFound, - ErrorS 'InvalidOperation, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input UTCTime, - MemberStore, - ProposalStore, - TinyLog - ] - r, - CallsFed 'Galley "leave-conversation", - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member ConversationStore r, + Member (Error InternalError) r, + Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input UTCTime) r, + Member MemberStore r, + Member ProposalStore r, + Member TinyLog r ) => Local UserId -> ConnId -> @@ -1190,14 +1045,10 @@ removeMemberQualified lusr con qcnv victim = victim removeMemberFromRemoteConv :: - ( Members - '[ FederatorAccess, - ErrorS ('ActionDenied 'RemoveConversationMember), - ErrorS 'ConvNotFound, - Input UTCTime - ] - r, - CallsFed 'Galley "leave-conversation" + ( Member FederatorAccess r, + Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, + Member (ErrorS 'ConvNotFound) r, + Member (Input UTCTime) r ) => Remote ConvId -> Local UserId -> @@ -1212,7 +1063,9 @@ removeMemberFromRemoteConv cnv lusr victim | otherwise = throwS @('ActionDenied 'RemoveConversationMember) where handleError :: - Members '[ErrorS ('ActionDenied 'RemoveConversationMember), ErrorS 'ConvNotFound] r => + ( Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, + Member (ErrorS 'ConvNotFound) r + ) => RemoveFromConversationError -> Sem r (Maybe Event) handleError RemoveFromConversationErrorRemovalNotAllowed = @@ -1229,26 +1082,20 @@ removeMemberFromRemoteConv cnv lusr victim -- | Remove a member from a local conversation. removeMemberFromLocalConv :: - ( Members - '[ ConversationStore, - Error InternalError, - ErrorS ('ActionDenied 'LeaveConversation), - ErrorS ('ActionDenied 'RemoveConversationMember), - ErrorS 'ConvNotFound, - ErrorS 'InvalidOperation, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input UTCTime, - MemberStore, - ProposalStore, - TinyLog - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member ConversationStore r, + Member (Error InternalError) r, + Member (ErrorS ('ActionDenied 'LeaveConversation)) r, + Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input Env) r, + Member (Input UTCTime) r, + Member MemberStore r, + Member ProposalStore r, + Member TinyLog r ) => Local ConvId -> Local UserId -> @@ -1271,24 +1118,16 @@ removeMemberFromLocalConv lcnv lusr con victim -- OTR postProteusMessage :: - ( Members - '[ BotAccess, - BrigAccess, - ClientStore, - ConversationStore, - FederatorAccess, - GundeckAccess, - ExternalAccess, - Input Opts, - Input UTCTime, - MemberStore, - TeamStore, - TinyLog - ] - r, - CallsFed 'Brig "get-user-clients", - CallsFed 'Galley "on-message-sent", - CallsFed 'Galley "send-message" + ( Member BrigAccess r, + Member ClientStore r, + Member ConversationStore r, + Member FederatorAccess r, + Member GundeckAccess r, + Member ExternalAccess r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member TeamStore r, + Member TinyLog r ) => Local UserId -> ConnId -> @@ -1303,24 +1142,17 @@ postProteusMessage sender zcon conv msg = runLocalInput sender $ do conv postProteusBroadcast :: - Members - '[ BotAccess, - BrigAccess, - ClientStore, - ConversationStore, - ErrorS 'TeamNotFound, - ErrorS 'NonBindingTeam, - ErrorS 'BroadcastLimitExceeded, - FederatorAccess, - GundeckAccess, - ExternalAccess, - Input Opts, - Input UTCTime, - MemberStore, - TeamStore, - TinyLog - ] - r => + ( Member BrigAccess r, + Member ClientStore r, + Member (ErrorS 'TeamNotFound) r, + Member (ErrorS 'NonBindingTeam) r, + Member (ErrorS 'BroadcastLimitExceeded) r, + Member GundeckAccess r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member TeamStore r, + Member TinyLog r + ) => Local UserId -> ConnId -> QualifiedNewOtrMessage -> @@ -1359,24 +1191,17 @@ unqualifyEndpoint loc f ignoreMissing reportMissing message = do unqualify (tDomain loc) <$> f qualifiedMessage postBotMessageUnqualified :: - ( Members - '[ BrigAccess, - ClientStore, - ConversationStore, - ErrorS 'ConvNotFound, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input (Local ()), - Input Opts, - MemberStore, - TeamStore, - TinyLog, - Input UTCTime - ] - r, - CallsFed 'Galley "on-message-sent", - CallsFed 'Brig "get-user-clients" + ( Member BrigAccess r, + Member ClientStore r, + Member ConversationStore r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input (Local ())) r, + Member (Input Opts) r, + Member TeamStore r, + Member TinyLog r, + Member (Input UTCTime) r ) => BotId -> ConvId -> @@ -1395,19 +1220,17 @@ postBotMessageUnqualified sender cnv ignoreMissing reportMissing message = do message postOtrBroadcastUnqualified :: - Members - '[ BrigAccess, - ClientStore, - ErrorS 'TeamNotFound, - ErrorS 'NonBindingTeam, - ErrorS 'BroadcastLimitExceeded, - GundeckAccess, - Input Opts, - Input UTCTime, - TeamStore, - TinyLog - ] - r => + ( Member BrigAccess r, + Member ClientStore r, + Member (ErrorS 'TeamNotFound) r, + Member (ErrorS 'NonBindingTeam) r, + Member (ErrorS 'BroadcastLimitExceeded) r, + Member GundeckAccess r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member TeamStore r, + Member TinyLog r + ) => Local UserId -> ConnId -> Maybe IgnoreMissing -> @@ -1420,23 +1243,16 @@ postOtrBroadcastUnqualified sender zcon = (postBroadcast sender (Just zcon)) postOtrMessageUnqualified :: - ( Members - '[ BotAccess, - BrigAccess, - ClientStore, - ConversationStore, - FederatorAccess, - GundeckAccess, - ExternalAccess, - MemberStore, - Input Opts, - Input UTCTime, - TeamStore, - TinyLog - ] - r, - CallsFed 'Galley "on-message-sent", - CallsFed 'Brig "get-user-clients" + ( Member BrigAccess r, + Member ClientStore r, + Member ConversationStore r, + Member FederatorAccess r, + Member GundeckAccess r, + Member ExternalAccess r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member TeamStore r, + Member TinyLog r ) => Local UserId -> ConnId -> @@ -1452,23 +1268,16 @@ postOtrMessageUnqualified sender zcon cnv = (runLocalInput sender . postQualifiedOtrMessage User (tUntagged sender) (Just zcon) lcnv) updateConversationName :: - ( Members - '[ ConversationStore, - Error FederationError, - Error InvalidInput, - ErrorS ('ActionDenied 'ModifyConversationName), - ErrorS 'ConvNotFound, - ErrorS 'InvalidOperation, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input UTCTime - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member ConversationStore r, + Member (Error FederationError) r, + Member (Error InvalidInput) r, + Member (ErrorS ('ActionDenied 'ModifyConversationName)) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input UTCTime) r ) => Local UserId -> ConnId -> @@ -1484,22 +1293,15 @@ updateConversationName lusr zcon qcnv convRename = do convRename updateUnqualifiedConversationName :: - ( Members - '[ ConversationStore, - Error InvalidInput, - ErrorS ('ActionDenied 'ModifyConversationName), - ErrorS 'ConvNotFound, - ErrorS 'InvalidOperation, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input UTCTime - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member ConversationStore r, + Member (Error InvalidInput) r, + Member (ErrorS ('ActionDenied 'ModifyConversationName)) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input UTCTime) r ) => Local UserId -> ConnId -> @@ -1511,22 +1313,15 @@ updateUnqualifiedConversationName lusr zcon cnv rename = do updateLocalConversationName lusr zcon lcnv rename updateLocalConversationName :: - ( Members - '[ ConversationStore, - Error InvalidInput, - ErrorS ('ActionDenied 'ModifyConversationName), - ErrorS 'ConvNotFound, - ErrorS 'InvalidOperation, - ExternalAccess, - FederatorAccess, - GundeckAccess, - Input Env, - Input UTCTime - ] - r, - CallsFed 'Galley "on-conversation-updated", - CallsFed 'Galley "on-mls-message-sent", - CallsFed 'Galley "on-new-remote-conversation" + ( Member ConversationStore r, + Member (Error InvalidInput) r, + Member (ErrorS ('ActionDenied 'ModifyConversationName)) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member ExternalAccess r, + Member FederatorAccess r, + Member GundeckAccess r, + Member (Input UTCTime) r ) => Local UserId -> ConnId -> @@ -1537,67 +1332,66 @@ updateLocalConversationName lusr zcon lcnv rename = getUpdateResult . fmap lcuEvent $ updateLocalConversation @'ConversationRenameTag lcnv (tUntagged lusr) (Just zcon) rename -isTypingQualified :: - ( Members - '[ GundeckAccess, - ErrorS 'ConvNotFound, - Input (Local ()), - Input UTCTime, - MemberStore, - FederatorAccess, - WaiRoutes - ] - r, - CallsFed 'Galley "on-typing-indicator-updated" +memberTyping :: + ( Member GundeckAccess r, + Member (ErrorS 'ConvNotFound) r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member ConversationStore r, + Member MemberStore r, + Member FederatorAccess r ) => Local UserId -> ConnId -> Qualified ConvId -> TypingStatus -> Sem r () -isTypingQualified lusr zcon qcnv ts = do +memberTyping lusr zcon qcnv ts = do foldQualified lusr - (\lcnv -> isTypingUnqualified lusr zcon (tUnqualified lcnv) ts) - (\rcnv -> isTypingRemote rcnv) + ( \lcnv -> do + (conv, _) <- getConversationAndMemberWithError @'ConvNotFound (tUntagged lusr) lcnv + void $ notifyTypingIndicator conv (tUntagged lusr) (Just zcon) ts + ) + ( \rcnv -> do + isMemberRemoteConv <- E.checkLocalMemberRemoteConv (tUnqualified lusr) rcnv + unless isMemberRemoteConv $ throwS @'ConvNotFound + let rpc = + TypingDataUpdateRequest + { tdurTypingStatus = ts, + tdurUserId = tUnqualified lusr, + tdurConvId = tUnqualified rcnv + } + res <- E.runFederated rcnv (fedClient @'Galley @"update-typing-indicator" rpc) + case res of + TypingDataUpdateSuccess (TypingDataUpdated {..}) -> do + pushTypingIndicatorEvents tudOrigUserId tudTime tudUsersInConv (Just zcon) qcnv tudTypingStatus + TypingDataUpdateError _ -> pure () + ) qcnv - where - isTypingRemote rcnv = do - isMemberRemoteConv <- E.checkLocalMemberRemoteConv (tUnqualified lusr) rcnv - unless isMemberRemoteConv $ throwS @'ConvNotFound - let rpc = - TypingDataUpdateRequest - { tdurTypingStatus = ts, - tdurUserId = tUnqualified lusr, - tdurConvId = tUnqualified rcnv - } - void $ E.runFederated rcnv (fedClient @'Galley @"on-typing-indicator-updated" rpc) - -isTypingUnqualified :: - Members - '[ GundeckAccess, - ErrorS 'ConvNotFound, - Input (Local ()), - Input UTCTime, - MemberStore, - WaiRoutes - ] - r => + +memberTypingUnqualified :: + ( Member GundeckAccess r, + Member (ErrorS 'ConvNotFound) r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member MemberStore r, + Member ConversationStore r, + Member FederatorAccess r + ) => Local UserId -> ConnId -> ConvId -> TypingStatus -> Sem r () -isTypingUnqualified lusr zcon cnv ts = do +memberTypingUnqualified lusr zcon cnv ts = do lcnv <- qualifyLocal cnv - isTyping (tUntagged lusr) (Just zcon) lcnv ts + memberTyping lusr zcon (tUntagged lcnv) ts addServiceH :: - Members - '[ ServiceStore, - WaiRoutes - ] - r => + ( Member ServiceStore r, + Member WaiRoutes r + ) => JsonRequest Service -> Sem r Response addServiceH req = do @@ -1605,7 +1399,9 @@ addServiceH req = do pure empty rmServiceH :: - Members '[ServiceStore, WaiRoutes] r => + ( Member ServiceStore r, + Member WaiRoutes r + ) => JsonRequest ServiceRef -> Sem r Response rmServiceH req = do @@ -1613,23 +1409,20 @@ rmServiceH req = do pure empty addBotH :: - Members - '[ ClientStore, - ConversationStore, - ErrorS ('ActionDenied 'AddConversationMember), - ErrorS 'ConvNotFound, - ErrorS 'InvalidOperation, - ErrorS 'TooManyMembers, - ExternalAccess, - GundeckAccess, - Input (Local ()), - Input Opts, - Input UTCTime, - MemberStore, - TeamStore, - WaiRoutes - ] - r => + ( Member ClientStore r, + Member ConversationStore r, + Member (ErrorS ('ActionDenied 'AddConversationMember)) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'TooManyMembers) r, + Member ExternalAccess r, + Member GundeckAccess r, + Member (Input (Local ())) r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member MemberStore r, + Member WaiRoutes r + ) => UserId ::: ConnId ::: JsonRequest AddBot -> Sem r Response addBotH (zusr ::: zcon ::: req) = do @@ -1639,21 +1432,18 @@ addBotH (zusr ::: zcon ::: req) = do addBot :: forall r. - Members - '[ ClientStore, - ConversationStore, - ErrorS ('ActionDenied 'AddConversationMember), - ErrorS 'ConvNotFound, - ErrorS 'InvalidOperation, - ErrorS 'TooManyMembers, - ExternalAccess, - GundeckAccess, - Input Opts, - Input UTCTime, - MemberStore, - TeamStore - ] - r => + ( Member ClientStore r, + Member ConversationStore r, + Member (ErrorS ('ActionDenied 'AddConversationMember)) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member (ErrorS 'TooManyMembers) r, + Member ExternalAccess r, + Member GundeckAccess r, + Member (Input Opts) r, + Member (Input UTCTime) r, + Member MemberStore r + ) => Local UserId -> ConnId -> AddBot -> @@ -1699,19 +1489,17 @@ addBot lusr zcon b = do pure (bots, users) rmBotH :: - Members - '[ ClientStore, - ConversationStore, - ErrorS 'ConvNotFound, - ExternalAccess, - GundeckAccess, - Input (Local ()), - Input UTCTime, - MemberStore, - WaiRoutes, - ErrorS ('ActionDenied 'RemoveConversationMember) - ] - r => + ( Member ClientStore r, + Member ConversationStore r, + Member (ErrorS 'ConvNotFound) r, + Member ExternalAccess r, + Member GundeckAccess r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member MemberStore r, + Member WaiRoutes r, + Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r + ) => UserId ::: Maybe ConnId ::: JsonRequest RemoveBot -> Sem r Response rmBotH (zusr ::: zcon ::: req) = do @@ -1720,17 +1508,15 @@ rmBotH (zusr ::: zcon ::: req) = do handleUpdateResult <$> rmBot lusr zcon bot rmBot :: - Members - '[ ClientStore, - ConversationStore, - ErrorS 'ConvNotFound, - ExternalAccess, - GundeckAccess, - Input UTCTime, - MemberStore, - ErrorS ('ActionDenied 'RemoveConversationMember) - ] - r => + ( Member ClientStore r, + Member ConversationStore r, + Member (ErrorS 'ConvNotFound) r, + Member ExternalAccess r, + Member GundeckAccess r, + Member (Input UTCTime) r, + Member MemberStore r, + Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r + ) => Local UserId -> Maybe ConnId -> RemoveBot -> diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index c31f30bc19..33ce13bbeb 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -70,7 +70,6 @@ import Wire.API.Conversation hiding (Member) import qualified Wire.API.Conversation as Public import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role -import Wire.API.Conversation.Typing import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Event.Conversation @@ -88,7 +87,10 @@ import Wire.API.User.Auth.ReAuth type JSON = Media "application" "json" ensureAccessRole :: - Members '[BrigAccess, ErrorS 'NotATeamMember, ErrorS 'ConvAccessDenied] r => + ( Member BrigAccess r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'ConvAccessDenied) r + ) => Set Public.AccessRole -> [(UserId, Maybe TeamMember)] -> Sem r () @@ -110,7 +112,10 @@ ensureAccessRole roles users = do -- Team members are always considered connected, so we only check 'ensureConnected' -- for non-team-members of the _given_ user ensureConnectedOrSameTeam :: - Members '[BrigAccess, ErrorS 'NotConnected, TeamStore] r => + ( Member BrigAccess r, + Member (ErrorS 'NotConnected) r, + Member TeamStore r + ) => Local UserId -> [UserId] -> Sem r () @@ -129,7 +134,9 @@ ensureConnectedOrSameTeam (tUnqualified -> u) uids = do -- B blocks A, the status of A-to-B is still 'Accepted' but it doesn't mean -- that they are connected). ensureConnected :: - Members '[BrigAccess, ErrorS 'NotConnected] r => + ( Member BrigAccess r, + Member (ErrorS 'NotConnected) r + ) => Local UserId -> UserList UserId -> Sem r () @@ -138,7 +145,9 @@ ensureConnected self others = do ensureConnectedToRemotes self (ulRemotes others) ensureConnectedToLocals :: - Members '[ErrorS 'NotConnected, BrigAccess] r => + ( Member (ErrorS 'NotConnected) r, + Member BrigAccess r + ) => UserId -> [UserId] -> Sem r () @@ -150,7 +159,9 @@ ensureConnectedToLocals u uids = do throwS @'NotConnected ensureConnectedToRemotes :: - Members '[BrigAccess, ErrorS 'NotConnected] r => + ( Member BrigAccess r, + Member (ErrorS 'NotConnected) r + ) => Local UserId -> [Remote UserId] -> Sem r () @@ -161,11 +172,9 @@ ensureConnectedToRemotes u remotes = do throwS @'NotConnected ensureReAuthorised :: - Members - '[ BrigAccess, - Error AuthenticationError - ] - r => + ( Member BrigAccess r, + Member (Error AuthenticationError) r + ) => UserId -> Maybe PlainTextPassword -> Maybe Code.Value -> @@ -179,7 +188,7 @@ ensureReAuthorised u secret mbAction mbCode = -- custom role, throw 'ActionDenied'. ensureActionAllowed :: forall (action :: Action) mem r. - (IsConvMember mem, Members '[ErrorS ('ActionDenied action)] r) => + (IsConvMember mem, Member (ErrorS ('ActionDenied action)) r) => Sing action -> mem -> Sem r () @@ -200,7 +209,7 @@ ensureGroupConversation conv = do -- This function needs to be review when custom roles are introduced since only -- custom roles can cause `roleNameToActions` to return a Nothing ensureConvRoleNotElevated :: - (IsConvMember mem, Members '[ErrorS 'InvalidAction] r) => + (IsConvMember mem, Member (ErrorS 'InvalidAction) r) => mem -> RoleName -> Sem r () @@ -218,11 +227,9 @@ permissionCheckS :: forall perm (p :: perm) r. ( SingKind perm, IsPerm (Demote perm), - Members - '[ ErrorS (PermError p), - ErrorS 'NotATeamMember - ] - r + ( Member (ErrorS (PermError p)) r, + Member (ErrorS 'NotATeamMember) r + ) ) => Sing p -> Maybe TeamMember -> @@ -240,7 +247,11 @@ permissionCheckS p = -- member does not have the given permission, throw 'operationDenied'. -- Otherwise, return the team member. permissionCheck :: - (IsPerm perm, Members '[ErrorS OperationDenied, ErrorS 'NotATeamMember] r) => + ( IsPerm perm, + ( Member (ErrorS OperationDenied) r, + Member (ErrorS 'NotATeamMember) r + ) + ) => perm -> Maybe TeamMember -> Sem r TeamMember @@ -253,14 +264,25 @@ permissionCheck p = \case -- FUTUREWORK: factor `noteS` out of this function. Nothing -> throwS @'NotATeamMember -assertTeamExists :: Members '[ErrorS 'TeamNotFound, TeamStore] r => TeamId -> Sem r () +assertTeamExists :: + ( Member (ErrorS 'TeamNotFound) r, + Member TeamStore r + ) => + TeamId -> + Sem r () assertTeamExists tid = do teamExists <- isJust <$> getTeam tid if teamExists then pure () else throwS @'TeamNotFound -assertOnTeam :: Members '[ErrorS 'NotATeamMember, TeamStore] r => UserId -> TeamId -> Sem r () +assertOnTeam :: + ( Member (ErrorS 'NotATeamMember) r, + Member TeamStore r + ) => + UserId -> + TeamId -> + Sem r () assertOnTeam uid tid = getTeamMember tid uid >>= \case Nothing -> throwS @'NotATeamMember @@ -268,17 +290,14 @@ assertOnTeam uid tid = -- | Try to accept a 1-1 conversation, promoting connect conversations as appropriate. acceptOne2One :: - Members - '[ ConversationStore, - ErrorS 'ConvNotFound, - Error InternalError, - ErrorS 'InvalidOperation, - ErrorS 'NotConnected, - GundeckAccess, - Input UTCTime, - MemberStore - ] - r => + ( Member ConversationStore r, + Member (ErrorS 'ConvNotFound) r, + Member (Error InternalError) r, + Member (ErrorS 'InvalidOperation) r, + Member GundeckAccess r, + Member (Input UTCTime) r, + Member MemberStore r + ) => Local UserId -> Data.Conversation -> Maybe ConnId -> @@ -501,7 +520,10 @@ getMember :: getMember p u = noteS @e . find ((u ==) . p) getConversationAndCheckMembership :: - Members '[ConversationStore, ErrorS 'ConvNotFound, ErrorS 'ConvAccessDenied] r => + ( Member ConversationStore r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'ConvAccessDenied) r + ) => UserId -> Local ConvId -> Sem r Data.Conversation @@ -524,7 +546,11 @@ getConversationWithError lcnv = getConversationAndMemberWithError :: forall e uid mem r. - (Members '[ConversationStore, ErrorS 'ConvNotFound, ErrorS e] r, IsConvMemberId uid mem) => + ( Member ConversationStore r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS e) r, + IsConvMemberId uid mem + ) => uid -> Local ConvId -> Sem r (Data.Conversation, mem) @@ -552,7 +578,10 @@ canDeleteMember deleter deletee -- | Send an event to local users and bots pushConversationEvent :: - (Members '[GundeckAccess, ExternalAccess] r, Foldable f) => + ( Member GundeckAccess r, + Member ExternalAccess r, + Foldable f + ) => Maybe ConnId -> Event -> Local (f UserId) -> @@ -564,7 +593,9 @@ pushConversationEvent conn e lusers bots = do deliverAsync (toList bots `zip` repeat e) verifyReusableCode :: - Members '[CodeStore, ErrorS 'CodeNotFound] r => + ( Member CodeStore r, + Member (ErrorS 'CodeNotFound) r + ) => ConversationCode -> Sem r DataTypes.Code verifyReusableCode convCode = do @@ -576,14 +607,11 @@ verifyReusableCode convCode = do pure c ensureConversationAccess :: - Members - '[ BrigAccess, - ConversationStore, - ErrorS 'ConvAccessDenied, - ErrorS 'NotATeamMember, - TeamStore - ] - r => + ( Member BrigAccess r, + Member (ErrorS 'ConvAccessDenied) r, + Member (ErrorS 'NotATeamMember) r, + Member TeamStore r + ) => UserId -> Data.Conversation -> Access -> @@ -721,7 +749,7 @@ fromConversationCreated loc rc@ConversationCreated {..} = -- | Notify remote users of being added to a new conversation registerRemoteConversationMemberships :: - (Member FederatorAccess r, CallsFed 'Galley "on-conversation-created") => + (Member FederatorAccess r) => -- | The time stamp when the conversation was created UTCTime -> -- | The domain of the user that created the conversation @@ -777,7 +805,9 @@ getLHStatus teamOfUser other = do pure $ maybe defUserLegalHoldStatus (view legalHoldStatus) mMember anyLegalholdActivated :: - Members '[Input Opts, TeamStore] r => + ( Member (Input Opts) r, + Member TeamStore r + ) => [UserId] -> Sem r Bool anyLegalholdActivated uids = do @@ -793,7 +823,10 @@ anyLegalholdActivated uids = do anyM (\uid -> userLHEnabled <$> getLHStatus (Map.lookup uid teamsOfUsers) uid) uidsPage allLegalholdConsentGiven :: - Members '[Input Opts, LegalHoldStore, TeamStore] r => + ( Member (Input Opts) r, + Member LegalHoldStore r, + Member TeamStore r + ) => [UserId] -> Sem r Bool allLegalholdConsentGiven uids = do @@ -833,7 +866,11 @@ getTeamMembersForFanout tid = do getTeamMembersWithLimit tid lim ensureMemberLimit :: - (Foldable f, Members '[ErrorS 'TooManyMembers, Input Opts] r) => + ( Foldable f, + ( Member (ErrorS 'TooManyMembers) r, + Member (Input Opts) r + ) + ) => [LocalMember] -> f a -> Sem r () @@ -844,7 +881,9 @@ ensureMemberLimit old new = do throwS @'TooManyMembers conversationExisted :: - Members '[Error InternalError, P.TinyLog] r => + ( Member (Error InternalError) r, + Member P.TinyLog r + ) => Local UserId -> Data.Conversation -> Sem r ConversationResponse @@ -872,30 +911,3 @@ instance if err' == demote @e then throwS @e else rethrowErrors @effs @r err' - --------------------------------------------------------------------------------- --- Send typing indicator events -isTyping :: - Members - '[ ErrorS 'ConvNotFound, - GundeckAccess, - Input UTCTime, - MemberStore - ] - r => - Qualified UserId -> - Maybe ConnId -> - Local ConvId -> - TypingStatus -> - Sem r () -isTyping qusr mcon lcnv ts = do - mm <- getLocalMembers (tUnqualified lcnv) - unless (qUnqualified qusr `isMember` mm) $ throwS @'ConvNotFound - now <- input - let e = Event (tUntagged lcnv) Nothing qusr now (EdTyping ts) - for_ (newPushLocal ListComplete (qUnqualified qusr) (ConvEvent e) (recipient <$> mm)) $ \p -> - push1 $ - p - & pushConn .~ mcon - & pushRoute .~ RouteDirect - & pushTransient .~ True diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index 4e18718f22..588a6b47b4 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -198,7 +198,7 @@ initHttpManager o = do } interpretTinyLog :: - Members '[Embed IO] r => + Member (Embed IO) r => Env -> Sem (P.TinyLog ': r) a -> Sem r a diff --git a/services/galley/src/Galley/Cassandra.hs b/services/galley/src/Galley/Cassandra.hs index ea4d501ef6..8d75052d2d 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 = 77 +schemaVersion = 79 diff --git a/services/galley/src/Galley/Cassandra/Client.hs b/services/galley/src/Galley/Cassandra/Client.hs index 25fb2a44d2..31b7720baf 100644 --- a/services/galley/src/Galley/Cassandra/Client.hs +++ b/services/galley/src/Galley/Cassandra/Client.hs @@ -58,7 +58,10 @@ eraseClients :: UserId -> Client () eraseClients user = retry x5 (write Cql.rmClients (params LocalQuorum (Identity user))) interpretClientStoreToCassandra :: - Members '[Embed IO, Input ClientState, Input Env] r => + ( Member (Embed IO) r, + Member (Input ClientState) r, + Member (Input Env) r + ) => Sem (ClientStore ': r) a -> Sem r a interpretClientStoreToCassandra = interpret $ \case diff --git a/services/galley/src/Galley/Cassandra/Code.hs b/services/galley/src/Galley/Cassandra/Code.hs index abb898e2fe..754df8e747 100644 --- a/services/galley/src/Galley/Cassandra/Code.hs +++ b/services/galley/src/Galley/Cassandra/Code.hs @@ -35,7 +35,10 @@ import Polysemy import Polysemy.Input interpretCodeStoreToCassandra :: - Members '[Embed IO, Input ClientState, Input Env] r => + ( Member (Embed IO) r, + Member (Input ClientState) r, + Member (Input Env) r + ) => Sem (CodeStore ': r) a -> Sem r a interpretCodeStoreToCassandra = interpret $ \case diff --git a/services/galley/src/Galley/Cassandra/Conversation.hs b/services/galley/src/Galley/Cassandra/Conversation.hs index 1b26c2207f..d4d82fcd45 100644 --- a/services/galley/src/Galley/Cassandra/Conversation.hs +++ b/services/galley/src/Galley/Cassandra/Conversation.hs @@ -273,7 +273,10 @@ localConversation cid = <*> UnliftIO.Concurrently (retry x1 $ query1 Cql.selectConv (params LocalQuorum (Identity cid))) localConversations :: - Members '[Embed IO, Input ClientState, TinyLog] r => + ( Member (Embed IO) r, + Member (Input ClientState) r, + Member TinyLog r + ) => [ConvId] -> Sem r [Conversation] localConversations = @@ -374,7 +377,10 @@ lookupGroupId gId = uncurry Qualified <$$> retry x1 (query1 Cql.lookupGroupId (params LocalQuorum (Identity gId))) interpretConversationStoreToCassandra :: - Members '[Embed IO, Input ClientState, TinyLog] r => + ( Member (Embed IO) r, + Member (Input ClientState) r, + Member TinyLog r + ) => Sem (ConversationStore ': r) a -> Sem r a interpretConversationStoreToCassandra = interpret $ \case diff --git a/services/galley/src/Galley/Cassandra/Conversation/Members.hs b/services/galley/src/Galley/Cassandra/Conversation/Members.hs index 7f79df3042..8590b2496e 100644 --- a/services/galley/src/Galley/Cassandra/Conversation/Members.hs +++ b/services/galley/src/Galley/Cassandra/Conversation/Members.hs @@ -364,7 +364,9 @@ lookupMLSClients groupId = (query Cql.lookupMLSClients (params LocalQuorum (Identity groupId))) interpretMemberStoreToCassandra :: - Members '[Embed IO, Input ClientState] r => + ( Member (Embed IO) r, + Member (Input ClientState) r + ) => Sem (MemberStore ': r) a -> Sem r a interpretMemberStoreToCassandra = interpret $ \case diff --git a/services/galley/src/Galley/Cassandra/ConversationList.hs b/services/galley/src/Galley/Cassandra/ConversationList.hs index 93ae9352d7..d0d5354884 100644 --- a/services/galley/src/Galley/Cassandra/ConversationList.hs +++ b/services/galley/src/Galley/Cassandra/ConversationList.hs @@ -65,21 +65,27 @@ remoteConversationIdsPageFrom usr pagingState max = uncurry toRemoteUnsafe <$$> paginateWithState Cql.selectUserRemoteConvs (paramsPagingState LocalQuorum (Identity usr) max pagingState) interpretConversationListToCassandra :: - Members '[Embed IO, Input ClientState] r => + ( Member (Embed IO) r, + Member (Input ClientState) r + ) => Sem (ListItems CassandraPaging ConvId ': r) a -> Sem r a interpretConversationListToCassandra = interpret $ \case ListItems uid ps max -> embedClient $ localConversationIdsPageFrom uid ps max interpretRemoteConversationListToCassandra :: - Members '[Embed IO, Input ClientState] r => + ( Member (Embed IO) r, + Member (Input ClientState) r + ) => Sem (ListItems CassandraPaging (Remote ConvId) ': r) a -> Sem r a interpretRemoteConversationListToCassandra = interpret $ \case ListItems uid ps max -> embedClient $ remoteConversationIdsPageFrom uid ps (fromRange max) interpretLegacyConversationListToCassandra :: - Members '[Embed IO, Input ClientState] r => + ( Member (Embed IO) r, + Member (Input ClientState) r + ) => Sem (ListItems LegacyPaging ConvId ': r) a -> Sem r a interpretLegacyConversationListToCassandra = interpret $ \case diff --git a/services/galley/src/Galley/Cassandra/CustomBackend.hs b/services/galley/src/Galley/Cassandra/CustomBackend.hs index c257416707..3eb73ae686 100644 --- a/services/galley/src/Galley/Cassandra/CustomBackend.hs +++ b/services/galley/src/Galley/Cassandra/CustomBackend.hs @@ -31,7 +31,9 @@ import Polysemy.Input import Wire.API.CustomBackend interpretCustomBackendStoreToCassandra :: - Members '[Embed IO, Input ClientState] r => + ( Member (Embed IO) r, + Member (Input ClientState) r + ) => Sem (CustomBackendStore ': r) a -> Sem r a interpretCustomBackendStoreToCassandra = interpret $ \case diff --git a/services/galley/src/Galley/Cassandra/Instances.hs b/services/galley/src/Galley/Cassandra/Instances.hs index 90b648e8ff..24d4e8a87a 100644 --- a/services/galley/src/Galley/Cassandra/Instances.hs +++ b/services/galley/src/Galley/Cassandra/Instances.hs @@ -32,7 +32,6 @@ import Data.Either.Combinators hiding (fromRight) import qualified Data.Text as T import qualified Data.Text.Encoding as T import Galley.Types.Bot () -import Galley.Types.Teams.Intra import Imports import Wire.API.Asset (AssetKey, assetKeyToText) import Wire.API.Conversation @@ -41,6 +40,7 @@ import Wire.API.MLS.CipherSuite import Wire.API.MLS.Proposal import Wire.API.MLS.PublicGroupState import Wire.API.MLS.Serialisation +import Wire.API.Routes.Internal.Galley.TeamsIntra import Wire.API.Team import qualified Wire.API.Team.Feature as Public import Wire.API.Team.SearchVisibility diff --git a/services/galley/src/Galley/Cassandra/LegalHold.hs b/services/galley/src/Galley/Cassandra/LegalHold.hs index 578be86d16..29528a02b4 100644 --- a/services/galley/src/Galley/Cassandra/LegalHold.hs +++ b/services/galley/src/Galley/Cassandra/LegalHold.hs @@ -55,7 +55,10 @@ import Wire.API.Provider.Service import Wire.API.User.Client.Prekey interpretLegalHoldStoreToCassandra :: - Members '[Embed IO, Input ClientState, Input Env] r => + ( Member (Embed IO) r, + Member (Input ClientState) r, + Member (Input Env) r + ) => FeatureLegalHold -> Sem (LegalHoldStore ': r) a -> Sem r a diff --git a/services/galley/src/Galley/Cassandra/Proposal.hs b/services/galley/src/Galley/Cassandra/Proposal.hs index 7b5089631c..e7d0c0b64f 100644 --- a/services/galley/src/Galley/Cassandra/Proposal.hs +++ b/services/galley/src/Galley/Cassandra/Proposal.hs @@ -39,7 +39,9 @@ defaultTTL :: Timeout defaultTTL = 28 # Day interpretProposalStoreToCassandra :: - Members '[Embed IO, Input ClientState] r => + ( Member (Embed IO) r, + Member (Input ClientState) r + ) => Sem (ProposalStore ': r) a -> Sem r a interpretProposalStoreToCassandra = diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index 5cdac44f74..6796456070 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -27,7 +27,6 @@ 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 import Text.RawString.QQ import Wire.API.Conversation @@ -39,6 +38,7 @@ import Wire.API.MLS.KeyPackage import Wire.API.MLS.PublicGroupState import Wire.API.Provider import Wire.API.Provider.Service +import Wire.API.Routes.Internal.Galley.TeamsIntra import Wire.API.Team import Wire.API.Team.Permission import Wire.API.Team.SearchVisibility diff --git a/services/galley/src/Galley/Cassandra/SearchVisibility.hs b/services/galley/src/Galley/Cassandra/SearchVisibility.hs index d8884b9758..5612739030 100644 --- a/services/galley/src/Galley/Cassandra/SearchVisibility.hs +++ b/services/galley/src/Galley/Cassandra/SearchVisibility.hs @@ -29,7 +29,9 @@ import Polysemy.Input import Wire.API.Team.SearchVisibility interpretSearchVisibilityStoreToCassandra :: - Members '[Embed IO, Input ClientState] r => + ( Member (Embed IO) r, + Member (Input ClientState) r + ) => Sem (SearchVisibilityStore ': r) a -> Sem r a interpretSearchVisibilityStoreToCassandra = interpret $ \case diff --git a/services/galley/src/Galley/Cassandra/Services.hs b/services/galley/src/Galley/Cassandra/Services.hs index 9052ca607c..f24544ff57 100644 --- a/services/galley/src/Galley/Cassandra/Services.hs +++ b/services/galley/src/Galley/Cassandra/Services.hs @@ -48,7 +48,9 @@ addBotMember s bot cnv = do -- Service -------------------------------------------------------------------- interpretServiceStoreToCassandra :: - Members '[Embed IO, Input ClientState] r => + ( Member (Embed IO) r, + Member (Input ClientState) r + ) => Sem (ServiceStore ': r) a -> Sem r a interpretServiceStoreToCassandra = interpret $ \case diff --git a/services/galley/src/Galley/Cassandra/Store.hs b/services/galley/src/Galley/Cassandra/Store.hs index 6c68eaf48c..1679452355 100644 --- a/services/galley/src/Galley/Cassandra/Store.hs +++ b/services/galley/src/Galley/Cassandra/Store.hs @@ -25,7 +25,12 @@ import Imports import Polysemy import Polysemy.Input -embedClient :: Members '[Embed IO, Input ClientState] r => Client a -> Sem r a +embedClient :: + ( Member (Embed IO) r, + Member (Input ClientState) r + ) => + Client a -> + Sem r a embedClient client = do cs <- input embed @IO $ runClient cs client diff --git a/services/galley/src/Galley/Cassandra/Team.hs b/services/galley/src/Galley/Cassandra/Team.hs index ff03dc0062..093faadb2a 100644 --- a/services/galley/src/Galley/Cassandra/Team.hs +++ b/services/galley/src/Galley/Cassandra/Team.hs @@ -51,11 +51,11 @@ import Galley.Env import Galley.Monad import Galley.Options import Galley.Types.Teams -import Galley.Types.Teams.Intra import Imports hiding (Set, max) import Polysemy import Polysemy.Input import qualified UnliftIO +import Wire.API.Routes.Internal.Galley.TeamsIntra import Wire.API.Team import Wire.API.Team.Conversation import Wire.API.Team.Member @@ -63,7 +63,10 @@ import Wire.API.Team.Permission (Perm (SetBilling), Permissions, self) import Wire.Sem.Paging.Cassandra interpretTeamStoreToCassandra :: - Members '[Embed IO, Input Env, Input ClientState] r => + ( Member (Embed IO) r, + Member (Input Env) r, + Member (Input ClientState) r + ) => FeatureLegalHold -> Sem (TeamStore ': r) a -> Sem r a @@ -102,14 +105,18 @@ interpretTeamStoreToCassandra lh = interpret $ \case embed @IO $ Aws.execute env (Aws.enqueue e) interpretTeamListToCassandra :: - Members '[Embed IO, Input ClientState] r => + ( Member (Embed IO) r, + Member (Input ClientState) r + ) => Sem (ListItems LegacyPaging TeamId ': r) a -> Sem r a interpretTeamListToCassandra = interpret $ \case ListItems uid ps lim -> embedClient $ teamIdsFrom uid ps lim interpretInternalTeamListToCassandra :: - Members '[Embed IO, Input ClientState] r => + ( Member (Embed IO) r, + Member (Input ClientState) r + ) => Sem (ListItems InternalPaging TeamId ': r) a -> Sem r a interpretInternalTeamListToCassandra = interpret $ \case @@ -120,7 +127,9 @@ interpretInternalTeamListToCassandra = interpret $ \case Just ps -> ipNext ps interpretTeamMemberStoreToCassandra :: - Members '[Embed IO, Input ClientState] r => + ( Member (Embed IO) r, + Member (Input ClientState) r + ) => FeatureLegalHold -> Sem (TeamMemberStore InternalPaging ': r) a -> Sem r a @@ -132,7 +141,9 @@ interpretTeamMemberStoreToCassandra lh = interpret $ \case Just ps -> ipNext ps interpretTeamMemberStoreToCassandraWithPaging :: - Members '[Embed IO, Input ClientState] r => + ( Member (Embed IO) r, + Member (Input ClientState) r + ) => FeatureLegalHold -> Sem (TeamMemberStore CassandraPaging ': r) a -> Sem r a diff --git a/services/galley/src/Galley/Cassandra/TeamFeatures.hs b/services/galley/src/Galley/Cassandra/TeamFeatures.hs index 169280c51d..272a6d27d1 100644 --- a/services/galley/src/Galley/Cassandra/TeamFeatures.hs +++ b/services/galley/src/Galley/Cassandra/TeamFeatures.hs @@ -28,6 +28,7 @@ import qualified Cassandra as C import Control.Monad.Trans.Maybe import Data.Id import Data.Proxy +import Data.Time (UTCTime) import Galley.Cassandra.Instances () import Galley.Cassandra.Store import qualified Galley.Effects.TeamFeatureStore as TFS @@ -44,7 +45,9 @@ data Cassandra type instance TFS.FeaturePersistentConstraint Cassandra = FeatureStatusCassandra interpretTeamFeatureStoreToCassandra :: - (Members '[Embed IO, Input ClientState] r) => + ( Member (Embed IO) r, + Member (Input ClientState) r + ) => Sem (TFS.TeamFeatureStore Cassandra ': r) a -> Sem r a interpretTeamFeatureStoreToCassandra = interpret $ \case @@ -314,6 +317,42 @@ instance FeatureStatusCassandra MLSConfig where "insert into team_features (team_id, mls_status, mls_default_protocol, \ \mls_protocol_toggle_users, mls_allowed_ciphersuites, mls_default_ciphersuite) values (?, ?, ?, ?, ?, ?)" +instance FeatureStatusCassandra MlsE2EIdConfig where + getFeatureConfig _ tid = do + let q = query1 select (params LocalQuorum (Identity tid)) + retry x1 q <&> \case + Nothing -> Nothing + Just (Nothing, _) -> Nothing + Just (mStatus, mTimeout) -> + WithStatusNoLock + <$> mStatus + <*> maybe (pure $ wsConfig defFeatureStatus) (pure . MlsE2EIdConfig) (Just mTimeout) + <*> Just FeatureTTLUnlimited + where + select :: PrepQuery R (Identity TeamId) (Maybe FeatureStatus, Maybe UTCTime) + select = + fromString $ + "select mls_e2eid_status, mls_e2eid_ver_exp from team_features where team_id = ?" + + setFeatureConfig _ tid status = do + let statusValue = wssStatus status + timeout = verificationExpiration . wssConfig $ status + retry x5 $ write insert (params LocalQuorum (tid, statusValue, timeout)) + where + insert :: PrepQuery W (TeamId, FeatureStatus, Maybe UTCTime) () + insert = + "insert into team_features (team_id, mls_e2eid_status, mls_e2eid_ver_exp) values (?, ?, ?)" + + getFeatureLockStatus _ = getLockStatusC "mls_e2eid_lock_status" + setFeatureLockStatus _ = setLockStatusC "mls_e2eid_lock_status" + instance FeatureStatusCassandra ExposeInvitationURLsToTeamAdminConfig where getFeatureConfig _ = getTrivialConfigC "expose_invitation_urls_to_team_admin" setFeatureConfig _ tid statusNoLock = setFeatureStatusC "expose_invitation_urls_to_team_admin" tid (wssStatus statusNoLock) + +instance FeatureStatusCassandra OutlookCalIntegrationConfig where + getFeatureConfig _ = getTrivialConfigC "outlook_cal_integration_status" + setFeatureConfig _ tid statusNoLock = setFeatureStatusC "outlook_cal_integration_status" tid (wssStatus statusNoLock) + + getFeatureLockStatus _ = getLockStatusC "outlook_cal_integration_lock_status" + setFeatureLockStatus _ = setLockStatusC "outlook_cal_integration_lock_status" diff --git a/services/galley/src/Galley/Cassandra/TeamNotifications.hs b/services/galley/src/Galley/Cassandra/TeamNotifications.hs index 22416dbfcc..8c4137c7ba 100644 --- a/services/galley/src/Galley/Cassandra/TeamNotifications.hs +++ b/services/galley/src/Galley/Cassandra/TeamNotifications.hs @@ -48,7 +48,9 @@ import Polysemy.Input import Wire.API.Internal.Notification interpretTeamNotificationStoreToCassandra :: - Members '[Embed IO, Input ClientState] r => + ( Member (Embed IO) r, + Member (Input ClientState) r + ) => Sem (TeamNotificationStore ': r) a -> Sem r a interpretTeamNotificationStoreToCassandra = interpret $ \case diff --git a/services/galley/src/Galley/Effects/TeamFeatureStore.hs b/services/galley/src/Galley/Effects/TeamFeatureStore.hs index 78ca78b8b1..86ea0ed352 100644 --- a/services/galley/src/Galley/Effects/TeamFeatureStore.hs +++ b/services/galley/src/Galley/Effects/TeamFeatureStore.hs @@ -29,13 +29,13 @@ module Galley.Effects.TeamFeatureStore where import Data.Id -import Data.Kind (Constraint) +import Data.Kind import Data.Proxy import Imports import Polysemy import Wire.API.Team.Feature -type family FeaturePersistentConstraint db :: * -> Constraint +type family FeaturePersistentConstraint db :: Type -> Constraint data TeamFeatureStore db m a where -- the proxy argument makes sure that makeSem below generates type-inference-friendly code diff --git a/services/galley/src/Galley/Effects/TeamStore.hs b/services/galley/src/Galley/Effects/TeamStore.hs index bf2fdbb566..cbca2cc43a 100644 --- a/services/galley/src/Galley/Effects/TeamStore.hs +++ b/services/galley/src/Galley/Effects/TeamStore.hs @@ -80,12 +80,12 @@ import Data.Id import Data.Range import Galley.Effects.ListItems import Galley.Types.Teams -import Galley.Types.Teams.Intra import Imports import Polysemy import qualified Proto.TeamEvents as E import Wire.API.Error import Wire.API.Error.Galley +import Wire.API.Routes.Internal.Galley.TeamsIntra import Wire.API.Team import Wire.API.Team.Conversation import Wire.API.Team.Member (HardTruncationLimit, TeamMember, TeamMemberList) @@ -139,12 +139,10 @@ listTeams :: listTeams = listItems lookupBindingTeam :: - Members - '[ ErrorS 'TeamNotFound, - ErrorS 'NonBindingTeam, - TeamStore - ] - r => + ( Member (ErrorS 'TeamNotFound) r, + Member (ErrorS 'NonBindingTeam) r, + Member TeamStore r + ) => UserId -> Sem r TeamId lookupBindingTeam zusr = do diff --git a/services/galley/src/Galley/Effects/WaiRoutes/IO.hs b/services/galley/src/Galley/Effects/WaiRoutes/IO.hs index 358513d8da..740e43f996 100644 --- a/services/galley/src/Galley/Effects/WaiRoutes/IO.hs +++ b/services/galley/src/Galley/Effects/WaiRoutes/IO.hs @@ -28,7 +28,9 @@ import Polysemy import Polysemy.Error interpretWaiRoutes :: - Members '[Embed IO, Error InvalidInput] r => + ( Member (Embed IO) r, + Member (Error InvalidInput) r + ) => Sem (WaiRoutes ': r) a -> Sem r a interpretWaiRoutes = interpret $ \case diff --git a/services/galley/src/Galley/External.hs b/services/galley/src/Galley/External.hs index 1f41ca3b52..e265c15a9e 100644 --- a/services/galley/src/Galley/External.hs +++ b/services/galley/src/Galley/External.hs @@ -47,7 +47,9 @@ import Wire.API.Event.Conversation (Event) import Wire.API.Provider.Service (serviceRefId, serviceRefProvider) interpretExternalAccess :: - Members '[Embed IO, Input Env] r => + ( Member (Embed IO) r, + Member (Input Env) r + ) => Sem (ExternalAccess ': r) a -> Sem r a interpretExternalAccess = interpret $ \case diff --git a/services/galley/src/Galley/External/LegalHoldService.hs b/services/galley/src/Galley/External/LegalHoldService.hs index 550629638b..867097790c 100644 --- a/services/galley/src/Galley/External/LegalHoldService.hs +++ b/services/galley/src/Galley/External/LegalHoldService.hs @@ -52,12 +52,10 @@ import Wire.API.Team.LegalHold.External -- | Get /status from legal hold service; throw 'Wai.Error' if things go wrong. checkLegalHoldServiceStatus :: - Members - '[ ErrorS 'LegalHoldServiceBadResponse, - LegalHoldStore, - P.TinyLog - ] - r => + ( Member (ErrorS 'LegalHoldServiceBadResponse) r, + Member LegalHoldStore r, + Member P.TinyLog r + ) => Fingerprint Rsa -> HttpsUrl -> Sem r () @@ -77,13 +75,11 @@ checkLegalHoldServiceStatus fpr url = do -- | @POST /initiate@. requestNewDevice :: - Members - '[ ErrorS 'LegalHoldServiceBadResponse, - ErrorS 'LegalHoldServiceNotRegistered, - LegalHoldStore, - P.TinyLog - ] - r => + ( Member (ErrorS 'LegalHoldServiceBadResponse) r, + Member (ErrorS 'LegalHoldServiceNotRegistered) r, + Member LegalHoldStore r, + Member P.TinyLog r + ) => TeamId -> UserId -> Sem r NewLegalHoldClient @@ -105,7 +101,9 @@ requestNewDevice tid uid = do -- | @POST /confirm@ -- Confirm that a device has been linked to a user and provide an authorization token confirmLegalHold :: - Members '[ErrorS 'LegalHoldServiceNotRegistered, LegalHoldStore] r => + ( Member (ErrorS 'LegalHoldServiceNotRegistered) r, + Member LegalHoldStore r + ) => ClientId -> TeamId -> UserId -> @@ -125,7 +123,9 @@ confirmLegalHold clientId tid uid legalHoldAuthToken = do -- | @POST /remove@ -- Inform the LegalHold Service that a user's legalhold has been disabled. removeLegalHold :: - Members '[ErrorS 'LegalHoldServiceNotRegistered, LegalHoldStore] r => + ( Member (ErrorS 'LegalHoldServiceNotRegistered) r, + Member LegalHoldStore r + ) => TeamId -> UserId -> Sem r () @@ -146,7 +146,9 @@ removeLegalHold tid uid = do -- the TSL fingerprint via 'makeVerifiedRequest' and passes the token so the service can -- authenticate the request. makeLegalHoldServiceRequest :: - Members '[ErrorS 'LegalHoldServiceNotRegistered, LegalHoldStore] r => + ( Member (ErrorS 'LegalHoldServiceNotRegistered) r, + Member LegalHoldStore r + ) => TeamId -> (Http.Request -> Http.Request) -> Sem r (Http.Response LC8.ByteString) diff --git a/services/galley/src/Galley/Intra/Client.hs b/services/galley/src/Galley/Intra/Client.hs index 80caf13870..7a6f0558bf 100644 --- a/services/galley/src/Galley/Intra/Client.hs +++ b/services/galley/src/Galley/Intra/Client.hs @@ -110,7 +110,11 @@ notifyClientsAboutLegalHoldRequest requesterUid targetUid lastPrekey' = do -- | Calls 'Brig.User.API.Auth.legalHoldLoginH'. getLegalHoldAuthToken :: - Members '[Embed IO, Error InternalError, P.TinyLog, Input Env] r => + ( Member (Embed IO) r, + Member (Error InternalError) r, + Member P.TinyLog r, + Member (Input Env) r + ) => UserId -> Maybe PlainTextPassword -> Sem r OpaqueAuthToken diff --git a/services/galley/src/Galley/Intra/Effects.hs b/services/galley/src/Galley/Intra/Effects.hs index c42b7f1d63..87c019c755 100644 --- a/services/galley/src/Galley/Intra/Effects.hs +++ b/services/galley/src/Galley/Intra/Effects.hs @@ -43,7 +43,11 @@ import qualified Polysemy.TinyLog as P import qualified UnliftIO interpretBrigAccess :: - Members '[Embed IO, Error InternalError, P.TinyLog, Input Env] r => + ( Member (Embed IO) r, + Member (Error InternalError) r, + Member P.TinyLog r, + Member (Input Env) r + ) => Sem (BrigAccess ': r) a -> Sem r a interpretBrigAccess = interpret $ \case @@ -92,7 +96,9 @@ interpretBrigAccess = interpret $ \case embedApp $ updateSearchVisibilityInbound status interpretSparAccess :: - Members '[Embed IO, Input Env] r => + ( Member (Embed IO) r, + Member (Input Env) r + ) => Sem (SparAccess ': r) a -> Sem r a interpretSparAccess = interpret $ \case @@ -100,14 +106,18 @@ interpretSparAccess = interpret $ \case LookupScimUserInfos uids -> embedApp $ lookupScimUserInfos uids interpretBotAccess :: - Members '[Embed IO, Input Env] r => + ( Member (Embed IO) r, + Member (Input Env) r + ) => Sem (BotAccess ': r) a -> Sem r a interpretBotAccess = interpret $ \case DeleteBot cid bid -> embedApp $ deleteBot cid bid interpretGundeckAccess :: - Members '[Embed IO, Input Env] r => + ( Member (Embed IO) r, + Member (Input Env) r + ) => Sem (GundeckAccess ': r) a -> Sem r a interpretGundeckAccess = interpret $ \case diff --git a/services/galley/src/Galley/Intra/Federator.hs b/services/galley/src/Galley/Intra/Federator.hs index 8f8f871e9c..1afab1b533 100644 --- a/services/galley/src/Galley/Intra/Federator.hs +++ b/services/galley/src/Galley/Intra/Federator.hs @@ -30,11 +30,12 @@ import Polysemy import Polysemy.Input import UnliftIO import Wire.API.Federation.Client -import Wire.API.Federation.Component import Wire.API.Federation.Error interpretFederatorAccess :: - Members '[Embed IO, Input Env] r => + ( Member (Embed IO) r, + Member (Input Env) r + ) => Sem (FederatorAccess ': r) a -> Sem r a interpretFederatorAccess = interpret $ \case @@ -47,7 +48,6 @@ interpretFederatorAccess = interpret $ \case IsFederationConfigured -> embedApp $ isJust <$> view federator runFederatedEither :: - KnownComponent c => Remote x -> FederatorClient c a -> App (Either FederationError a) @@ -66,7 +66,6 @@ runFederatedEither (tDomain -> remoteDomain) rpc = do liftIO . fmap (first FederationCallFailure) $ runFederatorClient ce rpc runFederated :: - KnownComponent c => Remote x -> FederatorClient c a -> App a @@ -76,8 +75,7 @@ runFederated dom rpc = runFederatedConcurrently :: ( Foldable f, - Functor f, - KnownComponent c + Functor f ) => f (Remote a) -> (Remote [a] -> FederatorClient c b) -> @@ -87,7 +85,7 @@ runFederatedConcurrently xs rpc = qualifyAs r <$> runFederated r (rpc r) runFederatedConcurrentlyEither :: - (Foldable f, Functor f, KnownComponent c) => + (Foldable f, Functor f) => f (Remote a) -> (Remote [a] -> FederatorClient c b) -> App [Either (Remote [a], FederationError) (Remote b)] diff --git a/services/galley/src/Galley/Intra/Journal.hs b/services/galley/src/Galley/Intra/Journal.hs index 7ae78d9a81..b864f174cc 100644 --- a/services/galley/src/Galley/Intra/Journal.hs +++ b/services/galley/src/Galley/Intra/Journal.hs @@ -54,13 +54,11 @@ import Wire.API.Team.Permission -- is started without journaling arguments teamActivate :: - Members - '[ Input Opts.Opts, - Input UTCTime, - TeamStore, - P.TinyLog - ] - r => + ( Member (Input Opts.Opts) r, + Member (Input UTCTime) r, + Member TeamStore r, + Member P.TinyLog r + ) => TeamId -> Natural -> Maybe Currency.Alpha -> @@ -71,7 +69,9 @@ teamActivate tid teamSize cur time = do journalEvent TeamEvent'TEAM_ACTIVATE tid (Just $ evData teamSize billingUserIds cur) time teamUpdate :: - Members '[TeamStore, Input UTCTime] r => + ( Member TeamStore r, + Member (Input UTCTime) r + ) => TeamId -> Natural -> [UserId] -> @@ -80,19 +80,25 @@ teamUpdate tid teamSize billingUserIds = journalEvent TeamEvent'TEAM_UPDATE tid (Just $ evData teamSize billingUserIds Nothing) Nothing teamDelete :: - Members '[TeamStore, Input UTCTime] r => + ( Member TeamStore r, + Member (Input UTCTime) r + ) => TeamId -> Sem r () teamDelete tid = journalEvent TeamEvent'TEAM_DELETE tid Nothing Nothing teamSuspend :: - Members '[TeamStore, Input UTCTime] r => + ( Member TeamStore r, + Member (Input UTCTime) r + ) => TeamId -> Sem r () teamSuspend tid = journalEvent TeamEvent'TEAM_SUSPEND tid Nothing Nothing journalEvent :: - Members '[TeamStore, Input UTCTime] r => + ( Member TeamStore r, + Member (Input UTCTime) r + ) => TeamEvent'EventType -> TeamId -> Maybe TeamEvent'EventData -> @@ -124,7 +130,10 @@ evData memberCount billingUserIds cur = -- 'getBillingTeamMembers'. This is required only until data is backfilled in the -- 'billing_team_user' table. getBillingUserIds :: - Members '[Input Opts.Opts, TeamStore, P.TinyLog] r => + ( Member (Input Opts.Opts) r, + Member TeamStore r, + Member P.TinyLog r + ) => TeamId -> Maybe TeamMemberList -> Sem r [UserId] @@ -153,7 +162,13 @@ getBillingUserIds tid maybeMemberList = do filterFromMembers list = pure $ map (view userId) $ filter (`hasPermission` SetBilling) (list ^. teamMembers) - handleList :: Members '[TeamStore, P.TinyLog] r => Bool -> TeamMemberList -> Sem r [UserId] + handleList :: + ( Member TeamStore r, + Member P.TinyLog r + ) => + Bool -> + TeamMemberList -> + Sem r [UserId] handleList enableIndexedBillingTeamMembers list = case list ^. teamMemberListType of ListTruncated -> diff --git a/services/galley/src/Galley/Intra/User.hs b/services/galley/src/Galley/Intra/User.hs index b04ca2a62b..8bc879a334 100644 --- a/services/galley/src/Galley/Intra/User.hs +++ b/services/galley/src/Galley/Intra/User.hs @@ -1,3 +1,6 @@ +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -185,7 +188,7 @@ lookupActivatedUsers = chunkify $ \uids -> do -- URL length limit of ~6500 (determined experimentally). 100 is a -- conservative setting. A uid contributes about 36+3 characters (+3 for the -- comma separator) to the overall URL length. -chunkify :: forall m key a. (Monad m, ToByteString key, Monoid a) => ([key] -> m a) -> [key] -> m a +chunkify :: forall m key a. (Monad m, Monoid a) => ([key] -> m a) -> [key] -> m a chunkify doChunk keys = mconcat <$> (doChunk `mapM` chunks keys) where maxSize :: Int diff --git a/services/galley/src/Galley/Monad.hs b/services/galley/src/Galley/Monad.hs index f880dfd498..83cb34e5ed 100644 --- a/services/galley/src/Galley/Monad.hs +++ b/services/galley/src/Galley/Monad.hs @@ -68,7 +68,9 @@ instance LC.MonadLogger App where log (env ^. applog) lvl (reqIdMsg (env ^. reqId) . m) embedApp :: - Members '[Embed IO, Input Env] r => + ( Member (Embed IO) r, + Member (Input Env) r + ) => App a -> Sem r a embedApp action = do diff --git a/services/galley/src/Galley/Validation.hs b/services/galley/src/Galley/Validation.hs index 4ff62e03e2..f87db6df4b 100644 --- a/services/galley/src/Galley/Validation.hs +++ b/services/galley/src/Galley/Validation.hs @@ -28,18 +28,19 @@ where import Control.Lens import Data.Range +import GHC.TypeNats import Galley.API.Error import Galley.Options import Imports import Polysemy import Polysemy.Error -rangeChecked :: (Member (Error InvalidInput) r, Within a n m) => a -> Sem r (Range n m a) +rangeChecked :: (KnownNat n, KnownNat m, Member (Error InvalidInput) r, Within a n m) => a -> Sem r (Range n m a) rangeChecked = either throwErr pure . checkedEither {-# INLINE rangeChecked #-} rangeCheckedMaybe :: - (Member (Error InvalidInput) r, Within a n m) => + (Member (Error InvalidInput) r, KnownNat n, KnownNat m, Within a n m) => Maybe a -> Sem r (Maybe (Range n m a)) rangeCheckedMaybe Nothing = pure Nothing diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index ce291f1ba4..9f28461430 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -66,7 +66,6 @@ import Federator.Discovery (DiscoveryFailure (..)) import Federator.MockServer import Galley.API.Mapping import Galley.Options (optFederator) -import Galley.Types.Conversations.Intra import Galley.Types.Conversations.Members import Imports import qualified Network.HTTP.Types.Status as HTTP @@ -94,6 +93,7 @@ import qualified Wire.API.Federation.API.Galley as F import Wire.API.Internal.Notification import Wire.API.Message import qualified Wire.API.Message as Message +import Wire.API.Routes.Internal.Galley.ConversationsIntra import Wire.API.Routes.MultiTablePaging import Wire.API.Routes.Version import Wire.API.Routes.Versioned @@ -244,10 +244,7 @@ tests s = "Typing indicators" [ test s "send typing indicators" postTypingIndicators, test s "send typing indicators without domain" postTypingIndicatorsV2, - test s "send typing indicators with invalid pyaload" postTypingIndicatorsHandlesNonsense, - test s "POST /federation/on-typing-indicator-updated : Update typing indicator by remote user" updateTypingIndicatorFromRemoteUser, - test s "POST /federation/on-typing-indicator-updated : Update typing indicator to remote user" updateTypingIndicatorToRemoteUser, - test s "send typing indicator update from local to remote on remote conv" updateTypingIndicatorToRemoteUserRemoteConv + test s "send typing indicators with invalid pyaload" postTypingIndicatorsHandlesNonsense ] ] @@ -279,6 +276,7 @@ testGetConvQualifiedV2 = do responseJsonError =<< postConvQualified alice + Nothing defNewProteusConv { newConvUsers = [bob] } @@ -338,7 +336,7 @@ postConvWithRemoteUsersOk = do WS.bracketR3 c alice alex amy $ \(wsAlice, wsAlex, wsAmy) -> do (rsp, federatedRequests) <- withTempMockFederator' (mockReply ()) $ - postConvQualified alice defNewProteusConv {newConvName = checked nameMaxSize, newConvQualifiedUsers = [qAlex, qAmy, qChad, qCharlie, qDee]} + postConvQualified alice Nothing defNewProteusConv {newConvName = checked nameMaxSize, newConvQualifiedUsers = [qAlex, qAmy, qChad, qCharlie, qDee]} addUserToTeam alice tid - assertQueue "team member (bob) join" $ tUpdate 2 [alice] + assertTeamUpdate "team member (bob) join" tid 2 [alice] refreshIndex dave <- view Teams.userId <$> addUserToTeam alice tid - assertQueue "team member (dave) join" $ tUpdate 3 [alice] + assertTeamUpdate "team member (dave) join" tid 3 [alice] refreshIndex (eve, qeve) <- randomUserTuple connectUsers alice (singleton eve) @@ -1538,6 +1541,7 @@ testAccessUpdateGuestRemoved = do responseJsonError =<< postConvWithRemoteUsers (qUnqualified alice) + Nothing defNewProteusConv { newConvQualifiedUsers = [bob, charlie, dee], newConvTeam = Just (ConvTeamInfo tid) @@ -2019,7 +2023,7 @@ postConvQualifiedFailNotConnected = do alice <- randomUser bob <- randomQualifiedUser jane <- randomQualifiedUser - postConvQualified alice defNewProteusConv {newConvQualifiedUsers = [bob, jane]} !!! do + postConvQualified alice Nothing defNewProteusConv {newConvQualifiedUsers = [bob, jane]} !!! do const 403 === statusCode const (Just "not-connected") === fmap label . responseJsonUnsafe @@ -2048,7 +2052,7 @@ postConvQualifiedFailNumMembers = do alice <- randomUser bob : others <- replicateM n randomQualifiedUser connectLocalQualifiedUsers alice (list1 bob others) - postConvQualified alice defNewProteusConv {newConvQualifiedUsers = bob : others} !!! do + postConvQualified alice Nothing defNewProteusConv {newConvQualifiedUsers = bob : others} !!! do const 400 === statusCode const (Just "client-error") === fmap label . responseJsonUnsafe @@ -2076,7 +2080,7 @@ postConvQualifiedFailBlocked = do connectLocalQualifiedUsers alice (list1 bob [jane]) putConnectionQualified jane alice Blocked !!! const 200 === statusCode - postConvQualified alice defNewProteusConv {newConvQualifiedUsers = [bob, jane]} !!! do + postConvQualified alice Nothing defNewProteusConv {newConvQualifiedUsers = [bob, jane]} !!! do const 403 === statusCode const (Just "not-connected") === fmap label . responseJsonUnsafe @@ -2084,7 +2088,7 @@ postConvQualifiedNoConnection :: TestM () postConvQualifiedNoConnection = do alice <- randomUser bob <- flip Qualified (Domain "far-away.example.com") <$> randomId - postConvQualified alice defNewProteusConv {newConvQualifiedUsers = [bob]} + postConvQualified alice Nothing defNewProteusConv {newConvQualifiedUsers = [bob]} !!! const 403 === statusCode postTeamConvQualifiedNoConnection :: TestM () @@ -2094,6 +2098,7 @@ postTeamConvQualifiedNoConnection = do charlie <- randomQualifiedUser postConvQualified (qUnqualified alice) + Nothing defNewProteusConv { newConvQualifiedUsers = [bob], newConvTeam = Just (ConvTeamInfo tid) @@ -2101,6 +2106,7 @@ postTeamConvQualifiedNoConnection = do !!! const 403 === statusCode postConvQualified (qUnqualified alice) + Nothing defNewProteusConv { newConvQualifiedUsers = [charlie], newConvTeam = Just (ConvTeamInfo tid) @@ -2114,6 +2120,7 @@ postConvQualifiedNonExistentDomain = do connectWithRemoteUser alice bob postConvQualified alice + Nothing defNewProteusConv {newConvQualifiedUsers = [bob]} !!! do const 422 === statusCode @@ -2132,9 +2139,9 @@ postConvQualifiedFederationNotEnabled = do -- like postConvQualified -- 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 :: 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 post $ g . path "/conversations" . zUser zusr . zConn "conn" . zType "access" . json conv postSelfConvOk :: TestM () @@ -2162,7 +2169,7 @@ 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 post (g . path "/conversations/one2one" . zUser alice . zConn "conn" . zType "access" . json inv) !!! do const 403 === statusCode const (Just "invalid-op") === fmap label . responseJsonUnsafe @@ -2341,6 +2348,7 @@ getConvQualifiedOk = do decodeConvId <$> postConvQualified alice + Nothing defNewProteusConv { newConvQualifiedUsers = [bob, chuck], newConvName = checked "gossip" @@ -2851,6 +2859,7 @@ deleteMembersConvLocalQualifiedOk = do decodeConvId <$> postConvQualified alice + Nothing defNewProteusConv { newConvQualifiedUsers = [qBob, qEve], newConvName = checked "federated gossip" @@ -2885,6 +2894,7 @@ deleteLocalMemberConvLocalQualifiedOk = do decodeConvId <$> postConvWithRemoteUsers alice + Nothing defNewProteusConv {newConvQualifiedUsers = [qBob, qEve]} let qconvId = Qualified convId localDomain @@ -2941,6 +2951,7 @@ deleteRemoteMemberConvLocalQualifiedOk = do fmap decodeConvId $ postConvQualified alice + Nothing defNewProteusConv {newConvQualifiedUsers = [qBob, qChad, qDee, qEve]} postConv alice' [alexDel'] (Just "gossip") [] Nothing Nothing - qconvA2 <- decodeQualifiedConvId <$> postConvWithRemoteUsers alice' defNewProteusConv {newConvQualifiedUsers = [alexDel, amy, berta, dwight]} + qconvA2 <- decodeQualifiedConvId <$> postConvWithRemoteUsers alice' Nothing defNewProteusConv {newConvQualifiedUsers = [alexDel, amy, berta, dwight]} qconvA3 <- decodeQualifiedConvId <$> postConv alice' [amy'] (Just "gossip3") [] Nothing Nothing - qconvA4 <- decodeQualifiedConvId <$> postConvWithRemoteUsers alice' defNewProteusConv {newConvQualifiedUsers = [alexDel, bart, carl]} + qconvA4 <- decodeQualifiedConvId <$> postConvWithRemoteUsers alice' Nothing defNewProteusConv {newConvQualifiedUsers = [alexDel, bart, carl]} convB1 <- randomId -- a remote conversation at 'bDomain' that Alice, AlexDel and Bart will be in convB2 <- randomId -- a remote conversation at 'bDomain' that AlexDel and Bart will be in convC1 <- randomId -- a remote conversation at 'cDomain' that AlexDel and Carl will be in @@ -4016,196 +4029,3 @@ testOne2OneConversationRequest shouldBeLocal actor desired = do pure $ statusCode resp == 200 liftIO $ found @?= ((actor, desired) == (LocalActor, Included)) ) - -updateTypingIndicatorToRemoteUserRemoteConv :: TestM () -updateTypingIndicatorToRemoteUserRemoteConv = do - c <- view tsCannon - qalice <- randomQualifiedUser - let alice = qUnqualified qalice - - -- create a remote conversation with alice - let remoteDomain = Domain "bobland.example.com" - qbob <- Qualified <$> randomId <*> pure remoteDomain - qconv <- Qualified <$> randomId <*> pure remoteDomain - connectWithRemoteUser alice qbob - - fedGalleyClient <- view tsFedGalleyClient - now <- liftIO getCurrentTime - let cu = - F.ConversationUpdate - { cuTime = now, - cuOrigUserId = qbob, - cuConvId = qUnqualified qconv, - cuAlreadyPresentUsers = [], - cuAction = - SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qalice) roleNameWireMember) - } - runFedClient @"on-conversation-updated" fedGalleyClient remoteDomain cu - - -- Fetch remote conversation - let bobAsLocal = - LocalMember - (qUnqualified qbob) - defMemberStatus - Nothing - roleNameWireAdmin - let mockConversation = - mkProteusConv - (qUnqualified qconv) - (qUnqualified qbob) - roleNameWireMember - [localMemberToOther remoteDomain bobAsLocal] - remoteConversationResponse = GetConversationsResponse [mockConversation] - void - $ withTempMockFederator' - (mockReply remoteConversationResponse) - $ getConvQualified alice qconv - do - -- Started - void $ - withTempMockFederator' (mockReply ()) $ do - -- post typing indicator from bob to alice - let tcReq = - TypingDataUpdateRequest - { tdurTypingStatus = StartedTyping, - tdurUserId = qUnqualified qbob, - tdurConvId = qUnqualified qconv - } - - runFedClient @"on-typing-indicator-updated" fedGalleyClient (qDomain qalice) tcReq - - -- backend A generates a notification for alice - void $ - WS.awaitMatch (5 # Second) wsAlice $ \n -> do - liftIO $ wsAssertTyping qconv qalice StartedTyping n - - -- stopped - void $ - withTempMockFederator' (mockReply ()) $ do - -- post typing indicator from bob to alice - let tcReq = - TypingDataUpdateRequest - { tdurTypingStatus = StoppedTyping, - tdurUserId = qUnqualified qbob, - tdurConvId = qUnqualified qconv - } - - runFedClient @"on-typing-indicator-updated" fedGalleyClient (qDomain qalice) tcReq - - -- backend A generates a notification for alice - void $ - WS.awaitMatch (5 # Second) wsAlice $ \n -> do - liftIO $ wsAssertTyping qconv qalice StoppedTyping n - -updateTypingIndicatorFromRemoteUser :: TestM () -updateTypingIndicatorFromRemoteUser = do - localDomain <- viewFederationDomain - [alice, bob] <- randomUsers 2 - let qAlice = Qualified alice localDomain - remoteDomain = Domain "far-away.example.com" - qBob = Qualified bob remoteDomain - - connectWithRemoteUser alice qBob - convId <- - decodeConvId - <$> postConvWithRemoteUsers - alice - defNewProteusConv {newConvQualifiedUsers = [qBob]} - let qconvId = Qualified convId localDomain - - c <- view tsCannon - WS.bracketR c alice $ \wsAlice -> do - -- Started - void $ - withTempMockFederator' (mockReply ()) $ do - -- post typing indicator from bob to alice - let tcReq = - TypingDataUpdateRequest - { tdurTypingStatus = StartedTyping, - tdurUserId = bob, - tdurConvId = convId - } - - fedGalleyClient <- view tsFedGalleyClient - runFedClient @"on-typing-indicator-updated" fedGalleyClient (qDomain qAlice) tcReq - - -- backend A generates a notification for alice - void $ - WS.awaitMatch (5 # Second) wsAlice $ \n -> do - liftIO $ wsAssertTyping qconvId qAlice StartedTyping n - - -- stopped - void $ - withTempMockFederator' (mockReply ()) $ do - -- post typing indicator from bob to alice - let tcReq = - TypingDataUpdateRequest - { tdurTypingStatus = StoppedTyping, - tdurUserId = bob, - tdurConvId = convId - } - - fedGalleyClient <- view tsFedGalleyClient - runFedClient @"on-typing-indicator-updated" fedGalleyClient (qDomain qAlice) tcReq - - -- backend A generates a notification for alice - void $ - WS.awaitMatch (5 # Second) wsAlice $ \n -> do - liftIO $ wsAssertTyping qconvId qAlice StoppedTyping n - -updateTypingIndicatorToRemoteUser :: TestM () -updateTypingIndicatorToRemoteUser = do - localDomain <- viewFederationDomain - [alice, bob] <- randomUsers 2 - let remoteDomain = Domain "far-away.example.com" - qBob = Qualified bob remoteDomain - - connectWithRemoteUser alice qBob - convId <- - decodeConvId - <$> postConvWithRemoteUsers - alice - defNewProteusConv {newConvQualifiedUsers = [qBob]} - let qconvId = Qualified convId localDomain - - c <- view tsCannon - WS.bracketR c bob $ \wsBob -> do - -- started - void $ - withTempMockFederator' (mockReply ()) $ do - -- post typing indicator from alice to bob - let tcReq = - TypingDataUpdateRequest - { tdurTypingStatus = StartedTyping, - tdurUserId = alice, - tdurConvId = convId - } - - fedGalleyClient <- view tsFedGalleyClient - runFedClient @"on-typing-indicator-updated" fedGalleyClient (qDomain qBob) tcReq - - -- backend A generates a notification for bob - void $ - WS.awaitMatch (5 # Second) wsBob $ \n -> do - liftIO $ wsAssertTyping qconvId qBob StartedTyping n - - -- stopped - void $ - withTempMockFederator' (mockReply ()) $ do - -- post typing indicator from alice to bob - let tcReq = - TypingDataUpdateRequest - { tdurTypingStatus = StoppedTyping, - tdurUserId = alice, - tdurConvId = convId - } - - fedGalleyClient <- view tsFedGalleyClient - runFedClient @"on-typing-indicator-updated" fedGalleyClient (qDomain qBob) tcReq - - -- backend A generates a notification for bob - void $ - WS.awaitMatch (5 # Second) wsBob $ \n -> do - liftIO $ wsAssertTyping qconvId qBob StoppedTyping n diff --git a/services/galley/test/integration/API/Federation.hs b/services/galley/test/integration/API/Federation.hs index 99e90443ba..4b2d42233f 100644 --- a/services/galley/test/integration/API/Federation.hs +++ b/services/galley/test/integration/API/Federation.hs @@ -41,7 +41,6 @@ import Data.Time.Clock import Data.Timeout (TimeoutUnit (..), (#)) import Data.UUID.V4 (nextRandom) import Federator.MockServer -import Galley.Types.Conversations.Intra import Imports import Test.QuickCheck (arbitrary, generate) import Test.Tasty @@ -59,6 +58,7 @@ import qualified Wire.API.Federation.API.Galley as FedGalley import Wire.API.Federation.Component import Wire.API.Internal.Notification import Wire.API.Message +import Wire.API.Routes.Internal.Galley.ConversationsIntra import Wire.API.User.Client (PubClient (..)) import Wire.API.User.Profile @@ -106,6 +106,7 @@ getConversationsAllFound = do responseJsonError =<< postConvWithRemoteUsers bob + Nothing defNewProteusConv {newConvQualifiedUsers = [aliceQ, carlQ]} -- create a one-to-one conversation between bob and alice @@ -657,6 +658,7 @@ leaveConversationSuccess = do decodeConvId <$> postConvQualified alice + Nothing defNewProteusConv { newConvQualifiedUsers = [qBob, qChad, qDee, qEve] } @@ -844,6 +846,7 @@ sendMessage = do fmap decodeConvId $ postConvQualified aliceId + Nothing defNewProteusConv { newConvQualifiedUsers = [bob, chad] } @@ -959,6 +962,7 @@ onUserDeleted = do decodeQualifiedConvId <$> ( postConvWithRemoteUsers (tUnqualified alice) + Nothing defNewProteusConv {newConvQualifiedUsers = [tUntagged bob, alex, bart, carl]} do @@ -1052,7 +1059,7 @@ updateConversationByRemoteAdmin = do WS.bracketR c alice $ \wsAlice -> do (rsp, _federatedRequests) <- withTempMockFederator' (mockReply ()) $ do - postConvQualified alice defNewProteusConv {newConvName = checked convName, newConvQualifiedUsers = [qbob, qcharlie]} + postConvQualified alice Nothing defNewProteusConv {newConvName = checked convName, newConvQualifiedUsers = [qbob, qcharlie]} Server api instance diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index ced2f442be..5666531bb5 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -215,7 +215,8 @@ postMLSConvFail = do connectUsers alice (list1 bob []) postConvQualified alice - (defNewMLSConv aliceClient) + (Just aliceClient) + defNewMLSConv { newConvQualifiedUsers = [Qualified bob (qDomain qalice)] } !!! do @@ -230,7 +231,11 @@ postMLSConvOk = do let aliceClient = newClientId 0 let nameMaxSize = T.replicate 256 "a" WS.bracketR c alice $ \wsA -> do - rsp <- postConvQualified alice (defNewMLSConv aliceClient) {newConvName = checked nameMaxSize} + rsp <- + postConvQualified + alice + (Just aliceClient) + defNewMLSConv {newConvName = checked nameMaxSize} pure rsp !!! do const 201 === statusCode const Nothing === fmap Wai.label . responseJsonError @@ -527,7 +532,7 @@ testSendAnotherUsersCommit = do testAddUsersToProteus :: TestM () testAddUsersToProteus = do [alice, bob] <- createAndConnectUsers (replicate 2 Nothing) - void $ postConvQualified (qUnqualified alice) defNewProteusConv + void $ postConvQualified (qUnqualified alice) Nothing defNewProteusConv groupId <- liftIO $ fmap (GroupId . BS.pack) (replicateM 32 (generate arbitrary)) runMLSTest $ do @@ -2107,7 +2112,8 @@ postMLSConvDisabled = do withMLSDisabled $ postConvQualified (qUnqualified alice) - (defNewMLSConv (newClientId 0)) + (Just (newClientId 0)) + defNewMLSConv !!! assertMLSNotEnabled postMLSMessageDisabled :: TestM () diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index 132843a524..313b0cbc61 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -1,4 +1,6 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- @@ -103,8 +105,6 @@ mapRemoteKeyPackageRef brig bundle = postMessage :: ( HasCallStack, MonadIO m, - MonadCatch m, - MonadThrow m, MonadHttp m, HasGalley m ) => @@ -126,8 +126,6 @@ postMessage sender msg = do postCommitBundle :: ( HasCallStack, MonadIO m, - MonadCatch m, - MonadThrow m, MonadHttp m, HasGalley m ) => @@ -146,12 +144,21 @@ postCommitBundle sender bundle = do . bytes bundle ) -postWelcome :: (MonadIO m, MonadHttp m, HasGalley m, HasCallStack) => UserId -> ByteString -> m ResponseLBS +-- FUTUREWORK: remove this and start using commit bundles everywhere in tests +postWelcome :: + ( MonadIO m, + MonadHttp m, + MonadReader TestSetup m, + HasCallStack + ) => + UserId -> + ByteString -> + m ResponseLBS postWelcome uid welcome = do - galley <- viewGalley + galley <- view tsUnversionedGalley post ( galley - . paths ["mls", "welcome"] + . paths ["v2", "mls", "welcome"] . zUser uid . zConn "conn" . content "message/mls" @@ -422,7 +429,8 @@ setupMLSGroup creator = setupMLSGroupWithConv action creator =<< liftTest ( postConvQualified (ciUser creator) - (defNewMLSConv (ciClient creator)) + (Just (ciClient creator)) + defNewMLSConv ) diff --git a/services/galley/test/integration/API/MessageTimer.hs b/services/galley/test/integration/API/MessageTimer.hs index 4bc37d317f..d1354b1e0a 100644 --- a/services/galley/test/integration/API/MessageTimer.hs +++ b/services/galley/test/integration/API/MessageTimer.hs @@ -151,6 +151,7 @@ messageTimerChangeWithRemotes = do resp <- postConvWithRemoteUsers bob + Nothing defNewProteusConv {newConvQualifiedUsers = [qalice]} let qconv = decodeQualifiedConvId resp diff --git a/services/galley/test/integration/API/Roles.hs b/services/galley/test/integration/API/Roles.hs index 72cb40b494..5b9847cefc 100644 --- a/services/galley/test/integration/API/Roles.hs +++ b/services/galley/test/integration/API/Roles.hs @@ -170,6 +170,7 @@ roleUpdateRemoteMember = do resp <- postConvWithRemoteUsers bob + Nothing defNewProteusConv {newConvQualifiedUsers = [qalice, qcharlie]} let qconv = decodeQualifiedConvId resp @@ -239,6 +240,7 @@ roleUpdateWithRemotes = do resp <- postConvWithRemoteUsers bob + Nothing defNewProteusConv {newConvQualifiedUsers = [qalice, qcharlie]} let qconv = decodeQualifiedConvId resp @@ -297,6 +299,7 @@ accessUpdateWithRemotes = do resp <- postConvWithRemoteUsers bob + Nothing defNewProteusConv {newConvQualifiedUsers = [qalice, qcharlie]} let qconv = decodeQualifiedConvId resp @@ -434,7 +437,7 @@ testConversationAccessRole = do } conv <- responseJsonError - =<< postConvQualified (qUnqualified alice) nc + =<< postConvQualified (qUnqualified alice) Nothing nc . +-- Disabling for HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- | TODO: most of this module is deprecated; use "Util.Test.SQS" from the types-common-aws package -- instead. module API.SQS where -import qualified Amazonka as AWS -import qualified Amazonka.SQS as SQS -import qualified Amazonka.SQS.Lens as SQS -import Control.Exception (asyncExceptionFromException) import Control.Lens hiding ((.=)) -import Control.Monad.Catch hiding (bracket) -import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Lazy (fromStrict) import qualified Data.Currency as Currency import Data.Id -import Data.ProtoLens.Encoding import qualified Data.Set as Set import Data.Text (pack) -import qualified Data.Text.Encoding as Text import qualified Data.UUID as UUID -import Data.UUID.V4 (nextRandom) -import Galley.Aws import qualified Galley.Aws as Aws import Galley.Options (JournalOpts) import Imports @@ -44,72 +36,72 @@ import Network.HTTP.Client.OpenSSL import OpenSSL.Session as Ssl import Proto.TeamEvents as E import Proto.TeamEvents_Fields as E -import Safe (headDef) import Ssl.Util -import System.Logger.Class import qualified System.Logger.Class as L import Test.Tasty.HUnit import TestSetup +import qualified Util.Test.SQS as SQS -ensureQueueEmpty :: TestM () -ensureQueueEmpty = view tsAwsEnv >>= ensureQueueEmptyIO - -ensureQueueEmptyIO :: MonadIO m => Maybe Aws.Env -> m () -ensureQueueEmptyIO (Just env) = liftIO $ Aws.execute env purgeQueue -ensureQueueEmptyIO Nothing = pure () - -assertQueue :: String -> (String -> Maybe E.TeamEvent -> IO ()) -> TestM () -assertQueue label check = - view tsAwsEnv >>= \case - Just env -> liftIO $ Aws.execute env $ fetchMessage label check +withTeamEventWatcher :: HasCallStack => (SQS.SQSWatcher TeamEvent -> TestM ()) -> TestM () +withTeamEventWatcher action = do + view tsTeamEventWatcher >>= \case Nothing -> pure () + Just w -> action w --- Try to assert an event in the queue for a `timeout` amount of seconds -tryAssertQueue :: Int -> String -> (String -> Maybe E.TeamEvent -> IO ()) -> TestM () -tryAssertQueue timeout label check = - view tsAwsEnv >>= \case - Just env -> liftIO $ Aws.execute env $ awaitMessage label timeout check +assertIfWatcher :: HasCallStack => String -> (TeamEvent -> Bool) -> (String -> Maybe TeamEvent -> TestM ()) -> TestM () +assertIfWatcher l matcher assertion = + view tsTeamEventWatcher >>= \case Nothing -> pure () + Just w -> SQS.assertMessage w l matcher assertion -assertQueueEmpty :: HasCallStack => TestM () -assertQueueEmpty = - view tsAwsEnv >>= \case - Just env -> liftIO $ Aws.execute env ensureNoMessages - Nothing -> pure () - -tActivateWithCurrency :: HasCallStack => Maybe Currency.Alpha -> String -> Maybe E.TeamEvent -> IO () -tActivateWithCurrency c l (Just e) = do +tActivateWithCurrency :: (HasCallStack, MonadIO m) => Maybe Currency.Alpha -> String -> Maybe E.TeamEvent -> m () +tActivateWithCurrency c l (Just e) = liftIO $ do assertEqual (l <> ": eventType") E.TeamEvent'TEAM_ACTIVATE (e ^. eventType) assertEqual "count" 1 (e ^. eventData . memberCount) -- NOTE: protobuf used to decodes absent, optional fields as (Just "") but not when using `maybe'` let cur = pack . show <$> c assertEqual "currency" cur (e ^. eventData . maybe'currency) -tActivateWithCurrency _ l Nothing = assertFailure $ l <> ": Expected 1 TeamActivate, got nothing" +tActivateWithCurrency _ l Nothing = liftIO $ assertFailure $ l <> ": Expected 1 TeamActivate, got nothing" + +assertTeamActivateWithCurrency :: HasCallStack => String -> TeamId -> Maybe Currency.Alpha -> TestM () +assertTeamActivateWithCurrency l tid c = + assertIfWatcher l (teamActivateMatcher tid) (tActivateWithCurrency c) -tActivate :: HasCallStack => String -> Maybe E.TeamEvent -> IO () -tActivate l (Just e) = do +tActivate :: (HasCallStack, MonadIO m) => String -> Maybe E.TeamEvent -> m () +tActivate l (Just e) = liftIO $ do assertEqual (l <> ": eventType") E.TeamEvent'TEAM_ACTIVATE (e ^. eventType) assertEqual "count" 1 (e ^. eventData . memberCount) -tActivate l Nothing = assertFailure $ l <> ": Expected 1 TeamActivate, got nothing" +tActivate l Nothing = liftIO $ assertFailure $ l <> ": Expected 1 TeamActivate, got nothing" -tDelete :: HasCallStack => String -> Maybe E.TeamEvent -> IO () -tDelete l (Just e) = assertEqual (l <> ": eventType") E.TeamEvent'TEAM_DELETE (e ^. eventType) -tDelete l Nothing = assertFailure $ l <> ": Expected 1 TeamDelete, got nothing" +assertTeamActivate :: HasCallStack => String -> TeamId -> TestM () +assertTeamActivate l tid = + assertIfWatcher l (teamActivateMatcher tid) tActivate -tSuspend :: HasCallStack => String -> Maybe E.TeamEvent -> IO () -tSuspend l (Just e) = assertEqual (l <> "eventType") E.TeamEvent'TEAM_SUSPEND (e ^. eventType) -tSuspend l Nothing = assertFailure $ l <> ": Expected 1 TeamSuspend, got nothing" +teamActivateMatcher :: TeamId -> TeamEvent -> Bool +teamActivateMatcher tid e = e ^. eventType == E.TeamEvent'TEAM_ACTIVATE && decodeIdFromBS (e ^. teamId) == tid -tUpdate :: HasCallStack => Int32 -> [UserId] -> String -> Maybe E.TeamEvent -> IO () -tUpdate c = tUpdateUncertainCount [c] +tDelete :: (HasCallStack, MonadIO m) => String -> Maybe E.TeamEvent -> m () +tDelete l (Just e) = liftIO $ assertEqual (l <> ": eventType") E.TeamEvent'TEAM_DELETE (e ^. eventType) +tDelete l Nothing = liftIO $ assertFailure $ l <> ": Expected 1 TeamDelete, got nothing" -tUpdateUncertainCount :: HasCallStack => [Int32] -> [UserId] -> String -> Maybe E.TeamEvent -> IO () -tUpdateUncertainCount countPossibilities uids l (Just e) = do +assertTeamDelete :: HasCallStack => Int -> String -> TeamId -> TestM () +assertTeamDelete maxWaitSeconds l tid = + withTeamEventWatcher $ \w -> do + mEvent <- SQS.waitForMessage w maxWaitSeconds (\e -> e ^. eventType == E.TeamEvent'TEAM_DELETE && decodeIdFromBS (e ^. teamId) == tid) + tDelete l mEvent + +tSuspend :: (HasCallStack, MonadIO m) => String -> Maybe E.TeamEvent -> m () +tSuspend l (Just e) = liftIO $ assertEqual (l <> "eventType") E.TeamEvent'TEAM_SUSPEND (e ^. eventType) +tSuspend l Nothing = liftIO $ assertFailure $ l <> ": Expected 1 TeamSuspend, got nothing" + +assertTeamSuspend :: HasCallStack => String -> TeamId -> TestM () +assertTeamSuspend l tid = + assertIfWatcher l (\e -> e ^. eventType == E.TeamEvent'TEAM_SUSPEND && decodeIdFromBS (e ^. teamId) == tid) tSuspend + +tUpdate :: (HasCallStack, MonadIO m) => Int32 -> [UserId] -> String -> Maybe E.TeamEvent -> m () +tUpdate expectedCount uids l (Just e) = liftIO $ do assertEqual (l <> ": eventType") E.TeamEvent'TEAM_UPDATE (e ^. eventType) - let actualCount = e ^. eventData . memberCount - assertBool - (l <> ": count, expected one of: " <> show countPossibilities <> ", got: " <> show actualCount) - (actualCount `elem` countPossibilities) + assertEqual (l <> ": member count") expectedCount (e ^. eventData . memberCount) let maybeBillingUserIds = map (UUID.fromByteString . fromStrict) (e ^. eventData . billingUser) assertBool "Invalid UUID found" (all isJust maybeBillingUserIds) let billingUserIds = catMaybes maybeBillingUserIds @@ -117,122 +109,14 @@ tUpdateUncertainCount countPossibilities uids l (Just e) = do (l <> ": billing users") (Set.fromList $ toUUID <$> uids) (Set.fromList $ billingUserIds) -tUpdateUncertainCount _ _ l Nothing = assertFailure $ l <> ": Expected 1 TeamUpdate, got nothing" - -ensureNoMessages :: HasCallStack => Amazon () -ensureNoMessages = do - QueueUrl url <- view eventQueue - amazonkaEnv <- view awsEnv - msgs <- fromMaybe [] . view SQS.receiveMessageResponse_messages <$> AWS.send amazonkaEnv (receive 1 url) - liftIO $ assertEqual "ensureNoMessages: length" 0 (length msgs) - -fetchMessage :: String -> (String -> Maybe E.TeamEvent -> IO ()) -> Amazon () -fetchMessage label callback = do - QueueUrl url <- view eventQueue - amazonkaEnv <- view awsEnv - msgs <- fromMaybe [] . view SQS.receiveMessageResponse_messages <$> AWS.send amazonkaEnv (receive 1 url) - events <- mapM (parseDeleteMessage url) msgs - liftIO $ callback label (headDef Nothing events) +tUpdate _ _ l Nothing = liftIO $ assertFailure $ l <> ": Expected 1 TeamUpdate, got nothing" -awaitMessage :: String -> Int -> (String -> Maybe E.TeamEvent -> IO ()) -> Amazon () -awaitMessage label timeout callback = do - QueueUrl url <- view eventQueue - tryMatch label timeout url callback +updateMatcher :: TeamId -> TeamEvent -> Bool +updateMatcher tid e = e ^. eventType == E.TeamEvent'TEAM_UPDATE && decodeIdFromBS (e ^. teamId) == tid -newtype MatchFailure = MatchFailure {mFailure :: (Maybe E.TeamEvent, SomeException)} - -type MatchSuccess = String - --- Try to match some assertions (callback) during the given timeout; if there's no --- match during the timeout, it asserts with the given label --- Matched matches are consumed while unmatched ones are republished to the queue -tryMatch :: - HasCallStack => - String -> - Int -> - Text -> - (String -> Maybe E.TeamEvent -> IO ()) -> - Amazon () -tryMatch label tries url callback = go tries - where - go 0 = liftIO (assertFailure $ label <> ": No matching team event found") - go n = do - msgs <- readAllUntilEmpty - (bad, ok) <- partitionEithers <$> mapM (check <=< parseDeleteMessage url) msgs - -- Requeue all failed checks - forM_ bad $ \x -> for_ (fst . mFailure $ x) queueEvent - -- If no success, continue! - when (null ok) $ do - liftIO $ threadDelay (10 ^ (6 :: Int)) - go (n - 1) - check :: Maybe E.TeamEvent -> Amazon (Either MatchFailure String) - check e = - do - liftIO $ callback label e - pure (Right $ show e) - `catchAll` \ex -> case asyncExceptionFromException ex of - Just x -> throwM (x :: SomeAsyncException) - Nothing -> pure . Left $ MatchFailure (e, ex) - --- Note that Amazon's purge queue is a bit incovenient for testing purposes because --- it may be delayed in ~60 seconds which causes messages that are published later --- to be (unintentionally) deleted -purgeQueue :: Amazon () -purgeQueue = void $ readAllUntilEmpty - -receive :: Int -> Text -> SQS.ReceiveMessage -receive n url = - SQS.newReceiveMessage url - & set SQS.receiveMessage_waitTimeSeconds (Just 1) - . set SQS.receiveMessage_maxNumberOfMessages (Just n) - . set SQS.receiveMessage_visibilityTimeout (Just 1) - -queueEvent :: E.TeamEvent -> Amazon () -queueEvent e = do - QueueUrl url <- view eventQueue - rnd <- liftIO nextRandom - amazonkaEnv <- view awsEnv - void $ AWS.send amazonkaEnv (req url rnd) - where - event = Text.decodeLatin1 $ B64.encode $ encodeMessage e - req url dedup = - SQS.newSendMessage url event - & SQS.sendMessage_messageGroupId ?~ "team.events" - & SQS.sendMessage_messageDeduplicationId ?~ UUID.toText dedup - -readAllUntilEmpty :: Amazon [SQS.Message] -readAllUntilEmpty = do - QueueUrl url <- view eventQueue - amazonkaEnv <- view awsEnv - msgs <- fromMaybe [] . view SQS.receiveMessageResponse_messages <$> AWS.send amazonkaEnv (receive 10 url) - readUntilEmpty msgs url msgs - where - readUntilEmpty acc _ [] = pure acc - readUntilEmpty acc url msgs = do - forM_ msgs $ deleteMessage url - amazonkaEnv <- view awsEnv - newMsgs <- fromMaybe [] . view SQS.receiveMessageResponse_messages <$> AWS.send amazonkaEnv (receive 10 url) - readUntilEmpty (acc ++ newMsgs) url newMsgs - -deleteMessage :: Text -> SQS.Message -> Amazon () -deleteMessage url m = do - amazonkaEnv <- view awsEnv - for_ - (m ^. SQS.message_receiptHandle) - (void . AWS.send amazonkaEnv . SQS.newDeleteMessage url) - -parseDeleteMessage :: Text -> SQS.Message -> Amazon (Maybe E.TeamEvent) -parseDeleteMessage url m = do - let decodedMessage = decodeMessage <=< (B64.decode . Text.encodeUtf8) - evt <- case decodedMessage <$> (m ^. SQS.message_body) of - Just (Right e) -> do - trace $ msg $ val "SQS event received" - pure (Just e) - _ -> do - err . msg $ val "Failed to parse SQS message or event" - pure Nothing - deleteMessage url m - pure evt +assertTeamUpdate :: HasCallStack => String -> TeamId -> Int32 -> [UserId] -> TestM () +assertTeamUpdate l tid c uids = + assertIfWatcher l (\e -> e ^. eventType == E.TeamEvent'TEAM_UPDATE && decodeIdFromBS (e ^. teamId) == tid) $ tUpdate c uids initHttpManager :: IO Manager initHttpManager = do @@ -255,3 +139,6 @@ mkAWSEnv opts = do l <- L.new $ L.setOutput L.StdOut . L.setFormat Nothing $ L.defSettings -- TODO: use mkLogger'? mgr <- initHttpManager Aws.mkEnv l mgr opts + +decodeIdFromBS :: ByteString -> Id a +decodeIdFromBS = Id . fromMaybe (error "failed to decode userId") . UUID.fromByteString . fromStrict diff --git a/services/galley/test/integration/API/Teams.hs b/services/galley/test/integration/API/Teams.hs index 63c416f644..097538d205 100644 --- a/services/galley/test/integration/API/Teams.hs +++ b/services/galley/test/integration/API/Teams.hs @@ -1,4 +1,6 @@ {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- @@ -62,7 +64,6 @@ import qualified Galley.Env as Galley import Galley.Options (optSettings, setEnableIndexedBillingTeamMembers, setFeatureFlags, setMaxConvSize, setMaxFanoutSize) import Galley.Types.Conversations.Roles import Galley.Types.Teams -import Galley.Types.Teams.Intra as TeamsIntra import Imports import Network.HTTP.Types.Status (status403) import qualified Network.Wai.Utilities.Error as Error @@ -82,6 +83,7 @@ import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Event.Team import Wire.API.Internal.Notification hiding (target) +import Wire.API.Routes.Internal.Galley.TeamsIntra as TeamsIntra import Wire.API.Team import Wire.API.Team.Export (TeamExportUser (..)) import qualified Wire.API.Team.Feature as Public @@ -133,8 +135,7 @@ tests s = test s "add team conversation with role" testAddTeamConvWithRole, test s "add team conversation as partner (fail)" testAddTeamConvAsExternalPartner, test s "add team MLS conversation" testCreateTeamMLSConv, - -- Queue is emptied here to ensure that lingering events do not affect other tests - test s "add team member to conversation without connection" (testAddTeamMemberToConv >> ensureQueueEmpty), + test s "add team member to conversation without connection" testAddTeamMemberToConv, test s "update conversation as member" (testUpdateTeamConv RoleMember roleNameWireAdmin), test s "update conversation as partner" (testUpdateTeamConv RoleExternalPartner roleNameWireMember), test s "delete binding team internal single member" testDeleteBindingTeamSingleMember, @@ -154,8 +155,7 @@ tests s = test s "update team data icon validation" testUpdateTeamIconValidation, test s "update team member" testUpdateTeamMember, test s "update team status" testUpdateTeamStatus, - -- Queue is emptied here to ensure that lingering events do not affect other tests - test s "team tests around truncation limits - no events, too large team" (testTeamAddRemoveMemberAboveThresholdNoEvents >> ensureQueueEmpty), + test s "team tests around truncation limits - no events, too large team" testTeamAddRemoveMemberAboveThresholdNoEvents, test s "send billing events to owners even in large teams" testBillingInLargeTeam, test s "send billing events to some owners in large teams (indexedBillingTeamMembers disabled)" testBillingInLargeTeamWithoutIndexedBillingTeamMembers, testGroup "broadcast" $ @@ -185,16 +185,17 @@ testCreateTeam = do owner <- Util.randomUser tid <- Util.createBindingTeamInternal "foo" owner team <- Util.getTeam owner tid - assertQueue "create team" tActivate + assertTeamActivate "create team" tid liftIO $ assertEqual "owner" owner (team ^. teamCreator) - assertQueueEmpty testGetTeams :: TestM () testGetTeams = do owner <- Util.randomUser Util.getTeams owner [] >>= checkTeamList Nothing - tid <- Util.createBindingTeamInternal "foo" owner <* assertQueue "create team" tActivate - wrongTid <- (Util.randomUser >>= Util.createBindingTeamInternal "foobar") <* assertQueue "create team" tActivate + tid <- Util.createBindingTeamInternal "foo" owner + assertTeamActivate "create team" tid + wrongTid <- Util.randomUser >>= Util.createBindingTeamInternal "foobar" + assertTeamActivate "create team" wrongTid 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) @@ -213,14 +214,14 @@ testGetTeams = do testCreateBindingTeamWithCurrency :: TestM () testCreateBindingTeamWithCurrency = do - _owner <- Util.randomUser - _ <- Util.createBindingTeamInternal "foo" _owner + owner1 <- Util.randomUser + tid1 <- Util.createBindingTeamInternal "foo" owner1 -- Backwards compatible - assertQueue "create team" (tActivateWithCurrency Nothing) + assertTeamActivateWithCurrency "create team" tid1 Nothing -- Ensure currency is properly journaled - _owner <- Util.randomUser - _ <- Util.createBindingTeamInternalWithCurrency "foo" _owner Currency.USD - assertQueue "create team" (tActivateWithCurrency $ Just Currency.USD) + owner2 <- Util.randomUser + tid2 <- Util.createBindingTeamInternalWithCurrency "foo" owner2 Currency.USD + assertTeamActivateWithCurrency "create team" tid2 (Just Currency.USD) testListTeamMembersDefaultLimit :: TestM () testListTeamMembersDefaultLimit = do @@ -260,7 +261,7 @@ testListTeamMembersCsv numMembers = do users <- Util.getUsersByHandle (catMaybes someHandles) mbrs <- view teamMembers <$> Util.bulkGetTeamMembers owner tid (U.userId <$> users) - let check :: (Show a, Eq a) => String -> (TeamExportUser -> Maybe a) -> UserId -> Maybe a -> IO () + let check :: Eq a => String -> (TeamExportUser -> Maybe a) -> UserId -> Maybe a -> IO () check msg getTeamExportUserAttr uid userAttr = do assertBool msg (isJust userAttr) assertEqual (msg <> ": " <> show uid) 1 (countOn getTeamExportUserAttr userAttr usersInCsv) @@ -398,7 +399,7 @@ testEnableSSOPerTeam :: TestM () testEnableSSOPerTeam = do owner <- Util.randomUser tid <- Util.createBindingTeamInternal "foo" owner - assertQueue "create team" tActivate + assertTeamActivate "create team" tid let check :: HasCallStack => String -> Public.FeatureStatus -> TestM () check msg enabledness = do status :: Public.WithStatusNoLock Public.SSOConfig <- responseJsonUnsafe <$> (getSSOEnabledInternal tid perms) = do WS.bracketR c (mem1 ^. userId) $ \wsMem1 -> do Util.addTeamMemberInternal tid (mem1 ^. userId) (mem1 ^. permissions) (mem1 ^. invitation) checkTeamMemberJoin tid (mem1 ^. userId) wsMem1 - assertQueue "team member join" $ tUpdate 2 [owner] + assertTeamUpdate "team member join" tid 2 [owner] void $ retryWhileN 10 repeatIf (Util.createOne2OneTeamConv owner (mem1 ^. userId) Nothing tid) -- Recreating a One2One is a no-op, returns a 200 Util.createOne2OneTeamConv owner (mem1 ^. userId) Nothing tid !!! const 200 === statusCode @@ -571,8 +572,6 @@ testTeamQueue = do liftIO $ assertEqual "team queue: size limit 3" (snd <$> queue3) [mem2, mem3, mem4] liftIO $ assertEqual "team queue: size limit 1, no start id" (snd <$> queue4) [mem1] - ensureQueueEmpty - testAddTeamMemberInternal :: TestM () testAddTeamMemberInternal = do c <- view tsCannon @@ -582,7 +581,7 @@ testAddTeamMemberInternal = do WS.bracketRN c [owner, mem1 ^. userId] $ \[wsOwner, wsMem1] -> do Util.addTeamMemberInternal tid (mem1 ^. userId) (mem1 ^. permissions) (mem1 ^. invitation) liftIO . void $ mapConcurrently (checkJoinEvent tid (mem1 ^. userId)) [wsOwner, wsMem1] - assertQueue "team member join" $ tUpdate 2 [owner] + assertTeamUpdate "team member join" tid 2 [owner] void $ Util.getTeamMemberInternal tid (mem1 ^. userId) testRemoveBindingTeamMember :: Bool -> TestM () @@ -597,15 +596,20 @@ testRemoveBindingTeamMember ownerHasPassword = do if ownerHasPassword then Util.addUserToTeam ownerWithPassword tid else Util.addUserToTeamWithSSO True tid + assertTeamUpdate "second member join" tid 2 [ownerWithPassword] + + refreshIndex Util.makeOwner ownerWithPassword ownerMem tid let owner = view userId ownerMem - ensureQueueEmpty + assertTeamUpdate "second member promoted to owner" tid 2 [ownerWithPassword, owner] + refreshIndex mext <- Util.randomUser mem1 <- Util.addUserToTeam owner tid - assertQueue "team member join" $ tUpdate 3 [ownerWithPassword, owner] + assertTeamUpdate "team member join" tid 3 [ownerWithPassword, owner] + refreshIndex - Util.connectUsers owner (singleton mext) + Util.connectUsers owner (List1.singleton mext) cid1 <- Util.createTeamConv owner tid [mem1 ^. userId, mext] (Just "blaa") Nothing Nothing when ownerHasPassword $ do -- Deleting from a binding team with empty body is invalid @@ -667,7 +671,7 @@ testRemoveBindingTeamMember ownerHasPassword = do === statusCode checkTeamMemberLeave tid (mem1 ^. userId) wsOwner checkConvMemberLeaveEvent (Qualified cid1 localDomain) (Qualified (mem1 ^. userId) localDomain) wsMext - assertQueue "team member leave" $ tUpdate 2 [ownerWithPassword, owner] + assertTeamUpdate "team member leave" tid 2 [ownerWithPassword, owner] WS.assertNoEvent timeout [wsMext] -- Mem1 is now gone from Wire Util.ensureDeletedState True owner (mem1 ^. userId) @@ -678,36 +682,34 @@ testRemoveBindingTeamOwner = do refreshIndex ownerB <- view userId <$> Util.addUserToTeamWithRole (Just RoleOwner) ownerA tid - assertQueue "Add owner" $ tUpdate 2 [ownerA, ownerB] + assertTeamUpdate "Add owner" tid 2 [ownerA, ownerB] refreshIndex ownerWithoutEmail <- do -- users must have a 'UserIdentity', or @get /i/users@ won't find it, so we use -- 'UserSSOId'. mem <- Util.addUserToTeamWithSSO False tid refreshIndex - assertQueue "Add user with SSO" $ tUpdate 3 [ownerA, ownerB] + assertTeamUpdate "Add user with SSO" tid 3 [ownerA, ownerB] Util.makeOwner ownerA mem tid pure $ view userId mem - assertQueue "Promote user to owner" $ tUpdate 3 [ownerA, ownerB, ownerWithoutEmail] + assertTeamUpdate "Promote user to owner" tid 3 [ownerA, ownerB, ownerWithoutEmail] admin <- view userId <$> Util.addUserToTeamWithRole (Just RoleAdmin) ownerA tid - assertQueue "Add admin" $ tUpdate 4 [ownerA, ownerB, ownerWithoutEmail] + assertTeamUpdate "Add admin" tid 4 [ownerA, ownerB, ownerWithoutEmail] refreshIndex -- non-owner can NOT delete owner check tid admin ownerWithoutEmail (Just Util.defPassword) (Just "access-denied") - assertQueueEmpty -- owners can NOT delete themselves check tid ownerA ownerA (Just Util.defPassword) (Just "access-denied") - assertQueueEmpty check tid ownerWithoutEmail ownerWithoutEmail Nothing (Just "access-denied") - assertQueueEmpty -- owners can delete other owners (no matter who has emails) check tid ownerWithoutEmail ownerA Nothing Nothing - assertQueue "Remove ownerA" $ tUpdate 3 [ownerB, ownerWithoutEmail] Util.waitForMemberDeletion ownerB tid ownerA + assertTeamUpdate "Remove ownerA" tid 3 [ownerB, ownerWithoutEmail] refreshIndex check tid ownerB ownerWithoutEmail (Just Util.defPassword) Nothing - assertQueue ("Remove ownerWithoutEmail: " <> show ownerWithoutEmail <> ", ownerA: " <> show ownerA) $ tUpdateUncertainCount [2, 3] [ownerB] + Util.waitForMemberDeletion ownerB tid ownerWithoutEmail + assertTeamUpdate "Remove ownerWithoutEmail" tid 2 [ownerB] where check :: HasCallStack => TeamId -> UserId -> UserId -> Maybe PlainTextPassword -> Maybe LText -> TestM () check tid deleter deletee pass maybeError = do @@ -742,7 +744,6 @@ testAddTeamConvLegacy = do mapM_ (checkConvCreateEvent cid) wss -- All members become admin by default mapM_ (assertConvMemberWithRole roleNameWireAdmin cid) allUserIds - ensureQueueEmpty testAddTeamConvWithRole :: TestM () testAddTeamConvWithRole = do @@ -773,8 +774,6 @@ testAddTeamConvWithRole = do checkTeamMemberJoin tid (mem1 ^. userId) wsMem2 -- ... but not to regular ones. Util.assertNotConvMember (mem1 ^. userId) cid2 - -- there is another team update event in the queue, so we clean it up - ensureQueueEmpty testCreateTeamMLSConv :: TestM () testCreateTeamMLSConv = do @@ -804,13 +803,13 @@ testAddTeamConvAsExternalPartner :: TestM () testAddTeamConvAsExternalPartner = do (owner, tid) <- Util.createBindingTeam memMember1 <- Util.addUserToTeamWithRole (Just RoleMember) owner tid - assertQueue "team member join 2" $ tUpdate 2 [owner] + assertTeamUpdate "team member join 2" tid 2 [owner] refreshIndex memMember2 <- Util.addUserToTeamWithRole (Just RoleMember) owner tid - assertQueue "team member join 3" $ tUpdate 3 [owner] + assertTeamUpdate "team member join 3" tid 3 [owner] refreshIndex memExternalPartner <- Util.addUserToTeamWithRole (Just RoleExternalPartner) owner tid - assertQueue "team member join 4" $ tUpdate 4 [owner] + assertTeamUpdate "team member join 4" tid 4 [owner] refreshIndex let acc = Just $ Set.fromList [InviteAccess, CodeAccess] Util.createTeamConvAccessRaw @@ -939,7 +938,7 @@ testDeleteBindingTeamSingleMember = do c <- view tsCannon (owner, tid) <- Util.createBindingTeam other <- Util.addUserToTeam owner tid - ensureQueueEmpty + assertTeamUpdate "team member leave 1" tid 2 [owner] refreshIndex -- Useful for tests extern <- Util.randomUser @@ -964,7 +963,7 @@ testDeleteBindingTeamSingleMember = do ) !!! const 202 === statusCode - assertQueue "team member leave 1" $ tUpdate 1 [owner] + assertTeamUpdate "team member leave 1" tid 1 [owner] -- Async things are hard... void $ retryWhileN @@ -986,24 +985,21 @@ testDeleteBindingTeamSingleMember = do WS.assertNoEvent (1 # Second) [wsExtern] -- Note that given the async nature of team deletion, we may -- have other events in the queue (such as TEAM_UPDATE) - tryAssertQueue 10 "team delete, should be there" tDelete + assertTeamDelete 10 "team delete, should be there" tid - Util.ensureDeletedState True extern owner -- Ensure users are marked as deleted; since we already -- received the event, should _really_ be deleted -- Let's clean the queue, just in case - ensureQueueEmpty + Util.ensureDeletedState True extern owner testDeleteBindingTeamNoMembers :: TestM () testDeleteBindingTeamNoMembers = do g <- viewGalley (owner, tid) <- Util.createBindingTeam deleteUser owner !!! const 200 === statusCode - ensureQueueEmpty refreshIndex delete (g . paths ["/i/teams", toByteString' tid]) !!! const 202 === statusCode - tryAssertQueue 10 "team delete, should be there" tDelete - ensureQueueEmpty + assertTeamDelete 10 "team delete, should be there" tid testDeleteBindingTeamMoreThanOneMember :: TestM () testDeleteBindingTeamMoreThanOneMember = do @@ -1011,7 +1007,6 @@ testDeleteBindingTeamMoreThanOneMember = do b <- viewBrig c <- view tsCannon (alice, tid, members) <- Util.createBindingTeamWithNMembers 10 - ensureQueueEmpty void . WS.bracketRN c (alice : members) $ \(wsAlice : wsMembers) -> do -- deleting a team with more than one member should be forbidden delete (g . paths ["/i/teams", toByteString' tid]) !!! do @@ -1022,7 +1017,7 @@ testDeleteBindingTeamMoreThanOneMember = do const 202 === statusCode checkUserDeleteEvent alice wsAlice zipWithM_ checkUserDeleteEvent members wsMembers - tryAssertQueue 10 "team delete, should be there" tDelete + assertTeamDelete 10 "team delete, should be there" tid let ensureDeleted :: UserId -> TestM () ensureDeleted uid = do @@ -1032,7 +1027,6 @@ testDeleteBindingTeamMoreThanOneMember = do ensureDeleted alice for_ members ensureDeleted - ensureQueueEmpty testDeleteTeamVerificationCodeSuccess :: TestM () testDeleteTeamVerificationCodeSuccess = do @@ -1052,8 +1046,7 @@ testDeleteTeamVerificationCodeSuccess = do ) !!! do const 202 === statusCode - tryAssertQueue 10 "team delete, should be there" tDelete - assertQueueEmpty + assertTeamDelete 10 "team delete, should be there" tid -- @SF.Channel @TSFI.RESTfulAPI @S2 -- @@ -1076,7 +1069,6 @@ testDeleteTeamVerificationCodeMissingCode = do !!! do const 403 === statusCode const "code-authentication-required" === (Error.label . responseJsonUnsafeWithMsg "error label") - assertQueueEmpty -- @END @@ -1093,7 +1085,7 @@ testDeleteTeamVerificationCodeExpiredCode = do generateVerificationCode $ Public.SendVerificationCode Public.DeleteTeam email code <- getVerificationCode (U.userId owner) Public.DeleteTeam -- wait > 5 sec for the code to expire (assumption: setVerificationTimeout in brig.integration.yaml is set to <= 5 sec) - threadDelay $ (5 * 1000 * 1000) + 600 * 1000 + threadDelay $ (10 * 1000 * 1000) + 600 * 1000 delete ( g . paths ["teams", toByteString' tid] @@ -1104,7 +1096,6 @@ testDeleteTeamVerificationCodeExpiredCode = do !!! do const 403 === statusCode const "code-authentication-failed" === (Error.label . responseJsonUnsafeWithMsg "error label") - assertQueueEmpty -- @END @@ -1130,11 +1121,10 @@ testDeleteTeamVerificationCodeWrongCode = do !!! do const 403 === statusCode const "code-authentication-failed" === (Error.label . responseJsonUnsafeWithMsg "error label") - assertQueueEmpty -- @END -setFeatureLockStatus :: forall cfg. (Public.IsFeatureConfig cfg, KnownSymbol (Public.FeatureSymbol cfg)) => TeamId -> Public.LockStatus -> TestM () +setFeatureLockStatus :: forall cfg. (KnownSymbol (Public.FeatureSymbol cfg)) => TeamId -> Public.LockStatus -> TestM () setFeatureLockStatus tid status = do g <- viewGalley put (g . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg, toByteString' status]) !!! const 200 === statusCode @@ -1169,18 +1159,20 @@ testDeleteBindingTeam ownerHasPassword = do if ownerHasPassword then Util.addUserToTeam ownerWithPassword tid else Util.addUserToTeamWithSSO True tid + assertTeamUpdate "team member join 2" tid 2 [ownerWithPassword] + refreshIndex Util.makeOwner ownerWithPassword ownerMem tid let owner = view userId ownerMem - ensureQueueEmpty + assertTeamUpdate "team member promoted" tid 2 [ownerWithPassword, owner] refreshIndex mem1 <- Util.addUserToTeam owner tid - assertQueue "team member join 3" $ tUpdate 3 [ownerWithPassword, owner] + assertTeamUpdate "team member join 3" tid 3 [ownerWithPassword, owner] refreshIndex mem2 <- Util.addUserToTeam owner tid - assertQueue "team member join 4" $ tUpdate 4 [ownerWithPassword, owner] + assertTeamUpdate "team member join 4" tid 4 [ownerWithPassword, owner] refreshIndex mem3 <- Util.addUserToTeam owner tid - assertQueue "team member join 5" $ tUpdate 5 [ownerWithPassword, owner] + assertTeamUpdate "team member join 5" tid 5 [ownerWithPassword, owner] refreshIndex extern <- Util.randomUser delete @@ -1208,7 +1200,7 @@ testDeleteBindingTeam ownerHasPassword = do ) !!! const 202 === statusCode - assertQueue "team member leave 1" $ tUpdate 4 [ownerWithPassword, owner] + assertTeamUpdate "team member leave 1" tid 4 [ownerWithPassword, owner] void . WS.bracketRN c [owner, mem1 ^. userId, mem2 ^. userId, extern] $ \[wsOwner, wsMember1, wsMember2, wsExtern] -> do delete ( g @@ -1234,13 +1226,11 @@ testDeleteBindingTeam ownerHasPassword = do WS.assertNoEvent (1 # Second) [wsExtern] -- Note that given the async nature of team deletion, we may -- have other events in the queue (such as TEAM_UPDATE) - tryAssertQueue 10 "team delete, should be there" tDelete + assertTeamDelete 10 "team delete, should be there" tid forM_ [owner, mem1 ^. userId, mem2 ^. userId] $ -- Ensure users are marked as deleted; since we already -- received the event, should _really_ be deleted Util.ensureDeletedState True extern - -- Let's clean it up, just in case - ensureQueueEmpty testDeleteTeamConv :: TestM () testDeleteTeamConv = do @@ -1252,8 +1242,6 @@ testDeleteTeamConv = do member <- newTeamMember' p <$> Util.randomUser qMember <- Qualified (member ^. userId) <$> viewFederationDomain Util.addTeamMemberInternal tid (member ^. userId) (member ^. permissions) Nothing - -- get rid of team update event - ensureQueueEmpty let members = [qOwner, qMember] extern <- Util.randomUser qExtern <- Qualified extern <$> viewFederationDomain @@ -1425,7 +1413,6 @@ testTeamAddRemoveMemberAboveThresholdNoEvents = do Util.postMembers owner (pure qextern) qcid1 !!! const 200 === statusCode -- Test team deletion (should contain only conv. removal and user.deletion for _non_ team members) deleteTeam tid owner [] [Qualified cid1 localDomain] extern - ensureQueueEmpty where modifyUserProfileAndExpectEvent :: HasCallStack => Bool -> UserId -> [UserId] -> TestM () modifyUserProfileAndExpectEvent expect target listeners = do @@ -1533,8 +1520,7 @@ testBillingInLargeTeam = do ( \billingMembers n -> do newBillingMemberId <- view userId <$> Util.addUserToTeamWithRole (Just RoleOwner) firstOwner team let allBillingMembers = newBillingMemberId : billingMembers - assertQueue ("add " <> show n <> "th billing member: " <> show newBillingMemberId) $ - tUpdate n allBillingMembers + assertTeamUpdate ("add " <> show n <> "th billing member: " <> show newBillingMemberId) team n allBillingMembers refreshIndex pure allBillingMembers ) @@ -1543,19 +1529,16 @@ testBillingInLargeTeam = do -- Additions after the fanout limit should still send events to all owners ownerFanoutPlusTwo <- view userId <$> Util.addUserToTeamWithRole (Just RoleOwner) firstOwner team - assertQueue ("add fanoutLimit + 2nd billing member: " <> show ownerFanoutPlusTwo) $ - tUpdate (fanoutLimit + 2) (ownerFanoutPlusTwo : allOwnersBeforeFanoutLimit) + assertTeamUpdate ("add fanoutLimit + 2nd billing member: " <> show ownerFanoutPlusTwo) team (fanoutLimit + 2) (ownerFanoutPlusTwo : allOwnersBeforeFanoutLimit) refreshIndex -- Deletions after the fanout limit should still send events to all owners ownerFanoutPlusThree <- view userId <$> Util.addUserToTeamWithRole (Just RoleOwner) firstOwner team - assertQueue ("add fanoutLimit + 3rd billing member: " <> show ownerFanoutPlusThree) $ - tUpdate (fanoutLimit + 3) (allOwnersBeforeFanoutLimit <> [ownerFanoutPlusTwo, ownerFanoutPlusThree]) + assertTeamUpdate ("add fanoutLimit + 3rd billing member: " <> show ownerFanoutPlusThree) team (fanoutLimit + 3) (allOwnersBeforeFanoutLimit <> [ownerFanoutPlusTwo, ownerFanoutPlusThree]) refreshIndex Util.deleteTeamMember galley team firstOwner ownerFanoutPlusThree - assertQueue ("delete fanoutLimit + 3rd billing member: " <> show ownerFanoutPlusThree) $ - tUpdate (fanoutLimit + 2) (allOwnersBeforeFanoutLimit <> [ownerFanoutPlusTwo]) + assertTeamUpdate ("delete fanoutLimit + 3rd billing member: " <> show ownerFanoutPlusThree) team (fanoutLimit + 2) (allOwnersBeforeFanoutLimit <> [ownerFanoutPlusTwo]) refreshIndex testBillingInLargeTeamWithoutIndexedBillingTeamMembers :: TestM () @@ -1569,83 +1552,98 @@ testBillingInLargeTeamWithoutIndexedBillingTeamMembers = do let fanoutLimit = fromRange $ Galley.currentFanoutLimit opts -- Billing should work properly upto fanout limit + billingUsers <- mapM (\n -> (n,) <$> randomUser) [2 .. (fanoutLimit + 1)] allOwnersBeforeFanoutLimit <- - foldM - ( \billingMembers n -> do - newBillingMemberId <- randomUser - let mem = json $ Member.mkNewTeamMember newBillingMemberId (rolePermissions RoleOwner) Nothing - -- We cannot properly add the new owner with an invite as we don't have a way to - -- override galley settings while making a call to brig - withoutIndexedBillingTeamMembers $ + withoutIndexedBillingTeamMembers $ + foldM + ( \billingMembers (n, newBillingMemberId) -> do + let mem = json $ Member.mkNewTeamMember newBillingMemberId (rolePermissions RoleOwner) Nothing + -- We cannot add the new owner with an invite as we don't have a way + -- to override galley settings while making a call to brig, so we use + -- the internal API here. post (galley . paths ["i", "teams", toByteString' team, "members"] . mem) !!! const 200 === statusCode - let allBillingMembers = newBillingMemberId : billingMembers - -- We don't make a call to brig to add member, hence the count of team is always 2 - assertQueue ("add " <> show n <> "th billing member: " <> show newBillingMemberId) $ - tUpdate 2 allBillingMembers - refreshIndex - pure allBillingMembers - ) - [firstOwner] - [2 .. (fanoutLimit + 1)] + let allBillingMembers = newBillingMemberId : billingMembers + -- We don't make a call to brig to add member, this prevents new + -- members from being indexed. Hence the count of team is always 2 + assertTeamUpdate ("add " <> show n <> "th billing member: " <> show newBillingMemberId) team 2 allBillingMembers + pure allBillingMembers + ) + [firstOwner] + billingUsers + refreshIndex -- If we add another owner, one of them won't get notified ownerFanoutPlusTwo <- randomUser let memFanoutPlusTwo = json $ Member.mkNewTeamMember ownerFanoutPlusTwo (rolePermissions RoleOwner) Nothing - -- We cannot properly add the new owner with an invite as we don't have a way to - -- override galley settings while making a call to brig + -- We cannot add the new owner with an invite as we don't have a way to + -- override galley settings while making a call to brig, so we use the + -- internal API here. withoutIndexedBillingTeamMembers $ do g <- viewGalley post (g . paths ["i", "teams", toByteString' team, "members"] . memFanoutPlusTwo) !!! const 200 === statusCode - assertQueue ("add " <> show (fanoutLimit + 2) <> "th billing member: " <> show ownerFanoutPlusTwo) $ + assertIfWatcher ("add " <> show (fanoutLimit + 2) <> "th billing member: " <> show ownerFanoutPlusTwo) (updateMatcher team) $ \s maybeEvent -> - case maybeEvent of + liftIO $ case maybeEvent of Nothing -> assertFailure "Expected 1 TeamUpdate, got nothing" Just event -> do assertEqual (s <> ": eventType") E.TeamEvent'TEAM_UPDATE (event ^. E.eventType) assertEqual (s <> ": count") 2 (event ^. E.eventData . E.memberCount) let reportedBillingUserIds = mapMaybe (UUID.fromByteString . fromStrict) (event ^. E.eventData . E.billingUser) assertEqual (s <> ": number of billing users") (fromIntegral fanoutLimit + 1) (length reportedBillingUserIds) + refreshIndex -- While members are added with indexedBillingTeamMembers disabled, new owners must still be -- indexed, just not used. When the feature is enabled, we should be able to send billing to -- all the owners ownerFanoutPlusThree <- view userId <$> Util.addUserToTeamWithRole (Just RoleOwner) firstOwner team - assertQueue ("add fanoutLimit + 3rd billing member: " <> show ownerFanoutPlusThree) $ - tUpdateUncertainCount [2, 3] (allOwnersBeforeFanoutLimit <> [ownerFanoutPlusTwo, ownerFanoutPlusThree]) + assertTeamUpdate + ("add fanoutLimit + 3rd billing member: " <> show ownerFanoutPlusThree) + team + 2 + (allOwnersBeforeFanoutLimit <> [ownerFanoutPlusTwo, ownerFanoutPlusThree]) refreshIndex -- Deletions with indexedBillingTeamMembers disabled should still remove owners from the -- indexed table withoutIndexedBillingTeamMembers $ Util.deleteTeamMember galley team firstOwner ownerFanoutPlusTwo - ensureQueueEmpty Util.waitForMemberDeletion firstOwner team ownerFanoutPlusTwo + assertTeamUpdate "delete 1 owner" team 1 (allOwnersBeforeFanoutLimit <> [ownerFanoutPlusThree]) ownerFanoutPlusFour <- view userId <$> Util.addUserToTeamWithRole (Just RoleOwner) firstOwner team - assertQueue ("add billing member to test deletion: " <> show ownerFanoutPlusFour) $ - tUpdateUncertainCount [3, 4] (allOwnersBeforeFanoutLimit <> [ownerFanoutPlusThree, ownerFanoutPlusFour]) + assertTeamUpdate + ("add billing member to test deletion: " <> show ownerFanoutPlusFour) + team + 3 + (allOwnersBeforeFanoutLimit <> [ownerFanoutPlusThree, ownerFanoutPlusFour]) refreshIndex -- Promotions and demotion should also be kept track of regardless of feature being enabled let demoteFanoutPlusThree = Member.mkNewTeamMember ownerFanoutPlusThree (rolePermissions RoleAdmin) Nothing withoutIndexedBillingTeamMembers $ updateTeamMember galley team firstOwner demoteFanoutPlusThree !!! const 200 === statusCode - ensureQueueEmpty + assertTeamUpdate "demote 1 user" team 3 (allOwnersBeforeFanoutLimit <> [ownerFanoutPlusFour]) ownerFanoutPlusFive <- view userId <$> Util.addUserToTeamWithRole (Just RoleOwner) firstOwner team - assertQueue ("add billing member to test demotion: " <> show ownerFanoutPlusFive) $ - tUpdateUncertainCount [4, 5] (allOwnersBeforeFanoutLimit <> [ownerFanoutPlusFour, ownerFanoutPlusFive]) + assertTeamUpdate + ("add billing member to test demotion: " <> show ownerFanoutPlusFive) + team + 4 + (allOwnersBeforeFanoutLimit <> [ownerFanoutPlusFour, ownerFanoutPlusFive]) refreshIndex let promoteFanoutPlusThree = Member.mkNewTeamMember ownerFanoutPlusThree (rolePermissions RoleOwner) Nothing withoutIndexedBillingTeamMembers $ updateTeamMember galley team firstOwner promoteFanoutPlusThree !!! const 200 === statusCode - ensureQueueEmpty + assertTeamUpdate "demote 1 user" team 4 (allOwnersBeforeFanoutLimit <> [ownerFanoutPlusThree, ownerFanoutPlusFour, ownerFanoutPlusFive]) ownerFanoutPlusSix <- view userId <$> Util.addUserToTeamWithRole (Just RoleOwner) firstOwner team - assertQueue ("add billing member to test promotion: " <> show ownerFanoutPlusSix) $ - tUpdateUncertainCount [5, 6] (allOwnersBeforeFanoutLimit <> [ownerFanoutPlusThree, ownerFanoutPlusFour, ownerFanoutPlusFive, ownerFanoutPlusSix]) + assertTeamUpdate + ("add billing member to test promotion: " <> show ownerFanoutPlusSix) + team + 5 + (allOwnersBeforeFanoutLimit <> [ownerFanoutPlusThree, ownerFanoutPlusFour, ownerFanoutPlusFive, ownerFanoutPlusSix]) where updateTeamMember g tid zusr change = put @@ -1667,7 +1665,7 @@ testUpdateTeamMember = do c <- view tsCannon (owner, tid) <- Util.createBindingTeam member <- Util.addUserToTeamWithRole (Just RoleAdmin) owner tid - assertQueue "add member" $ tUpdate 2 [owner] + assertTeamUpdate "add member" tid 2 [owner] refreshIndex -- non-owner can **NOT** demote owner let demoteOwner = Member.mkNewTeamMember owner (rolePermissions RoleAdmin) Nothing @@ -1684,7 +1682,7 @@ testUpdateTeamMember = do checkTeamMemberUpdateEvent tid (member ^. userId) wsOwner (pure noPermissions) checkTeamMemberUpdateEvent tid (member ^. userId) wsMember (pure noPermissions) WS.assertNoEvent timeout [wsOwner, wsMember] - assertQueue "Member demoted" $ tUpdate 2 [owner] + assertTeamUpdate "Member demoted" tid 2 [owner] -- owner can promote non-owner let promoteMember = Member.mkNewTeamMember (member ^. userId) fullPermissions (member ^. invitation) WS.bracketR2 c owner (member ^. userId) $ \(wsOwner, wsMember) -> do @@ -1695,7 +1693,7 @@ testUpdateTeamMember = do checkTeamMemberUpdateEvent tid (member ^. userId) wsOwner (pure fullPermissions) checkTeamMemberUpdateEvent tid (member ^. userId) wsMember (pure fullPermissions) WS.assertNoEvent timeout [wsOwner, wsMember] - assertQueue "Member promoted to owner" $ tUpdate 2 [owner, member ^. userId] + assertTeamUpdate "Member promoted to owner" tid 2 [owner, member ^. userId] -- owner can **NOT** demote herself, even when another owner exists updateTeamMember g tid owner demoteOwner !!! do const 403 === statusCode @@ -1709,7 +1707,7 @@ testUpdateTeamMember = do checkTeamMemberUpdateEvent tid owner wsOwner (pure (rolePermissions RoleAdmin)) checkTeamMemberUpdateEvent tid owner wsMember (pure (rolePermissions RoleAdmin)) WS.assertNoEvent timeout [wsOwner, wsMember] - assertQueue "Owner demoted" $ tUpdate 2 [member ^. userId] + assertTeamUpdate "Owner demoted" tid 2 [member ^. userId] where updateTeamMember g tid zusr change = put @@ -1733,15 +1731,12 @@ testUpdateTeamStatus = do (_, tid) <- Util.createBindingTeam -- Check for idempotency Util.changeTeamStatus tid TeamsIntra.Active - assertQueueEmpty Util.changeTeamStatus tid TeamsIntra.Suspended - assertQueue "suspend first time" tSuspend + assertTeamSuspend "suspend first time" tid Util.changeTeamStatus tid TeamsIntra.Suspended - assertQueueEmpty Util.changeTeamStatus tid TeamsIntra.Suspended - assertQueueEmpty Util.changeTeamStatus tid TeamsIntra.Active - assertQueue "activate again" tActivate + assertTeamActivate "activate again" tid void $ put ( g @@ -1761,7 +1756,7 @@ postCryptoBroadcastMessage bcast = do -- Team1: Alice, Bob. Team2: Charlie. Regular user: Dan. Connect Alice,Charlie,Dan (alice, tid) <- Util.createBindingTeam bob <- view userId <$> Util.addUserToTeam alice tid - assertQueue "add bob" $ tUpdate 2 [alice] + assertTeamUpdate "add bob" tid 2 [alice] refreshIndex (charlie, _) <- Util.createBindingTeam refreshIndex @@ -1811,11 +1806,11 @@ postCryptoBroadcastMessageFilteredTooLargeTeam bcast = do -- Team1: alice, bob and 3 unnamed (alice, tid) <- Util.createBindingTeam bob <- view userId <$> Util.addUserToTeam alice tid - assertQueue "add bob" $ tUpdate 2 [alice] + assertTeamUpdate "add bob" tid 2 [alice] refreshIndex forM_ [3 .. 5] $ \count -> do void $ Util.addUserToTeam alice tid - assertQueue "add user" $ tUpdate count [alice] + assertTeamUpdate "add user" tid count [alice] refreshIndex -- Team2: charlie (charlie, _) <- Util.createBindingTeam @@ -1875,7 +1870,7 @@ postCryptoBroadcastMessageReportMissingBody bcast = do let qalice = Qualified alice localDomain bob <- view userId <$> Util.addUserToTeam alice tid _bc <- Util.randomClient bob (someLastPrekeys !! 1) -- this is important! - assertQueue "add bob" $ tUpdate 2 [alice] + assertTeamUpdate "add bob" tid 2 [alice] refreshIndex ac <- Util.randomClient alice (head someLastPrekeys) let -- add extraneous query parameter (unless using query parameter API) @@ -1896,7 +1891,7 @@ postCryptoBroadcastMessage2 bcast = do -- Team1: Alice, Bob. Team2: Charlie. Connect Alice,Charlie (alice, tid) <- Util.createBindingTeam bob <- view userId <$> Util.addUserToTeam alice tid - assertQueue "add bob" $ tUpdate 2 [alice] + assertTeamUpdate "add bob" tid 2 [alice] refreshIndex (charlie, _) <- Util.createBindingTeam refreshIndex @@ -1967,8 +1962,8 @@ postCryptoBroadcastMessage100OrMaxConns bcast = do c <- view tsCannon (alice, ac) <- randomUserWithClient (head someLastPrekeys) let qalice = Qualified alice localDomain - _ <- createBindingTeamInternal "foo" alice - assertQueue "" tActivate + tid <- createBindingTeamInternal "foo" alice + assertTeamActivate "" tid ((bob, bc), others) <- createAndConnectUserWhileLimitNotReached alice (100 :: Int) [] (someLastPrekeys !! 1) connectUsers alice (list1 bob (fst <$> others)) let t = 3 # Second -- WS receive timeout @@ -1987,7 +1982,7 @@ postCryptoBroadcastMessage100OrMaxConns bcast = do where createAndConnectUserWhileLimitNotReached alice remaining acc pk = do (uid, cid) <- randomUserWithClient pk - (r1, r2) <- List1.head <$> connectUsersUnchecked alice (singleton uid) + (r1, r2) <- List1.head <$> connectUsersUnchecked alice (List1.singleton uid) case (statusCode r1, statusCode r2, remaining, acc) of (201, 200, 0, []) -> error "Need to connect with at least 1 user" (201, 200, 0, x : xs) -> pure (x, xs) @@ -2011,14 +2006,14 @@ putSSOEnabledInternal :: HasCallStack => TeamId -> Public.FeatureStatus -> TestM putSSOEnabledInternal tid statusValue = void $ Util.putTeamFeatureFlagInternal @Public.SSOConfig expect2xx tid (Public.WithStatusNoLock statusValue Public.SSOConfig Public.FeatureTTLUnlimited) -getSearchVisibility :: HasCallStack => (Request -> Request) -> UserId -> TeamId -> (MonadIO m, MonadHttp m) => m ResponseLBS +getSearchVisibility :: HasCallStack => (Request -> Request) -> UserId -> TeamId -> MonadHttp m => m ResponseLBS getSearchVisibility g uid tid = do get $ g . paths ["teams", toByteString' tid, "search-visibility"] . zUser uid -putSearchVisibility :: HasCallStack => (Request -> Request) -> UserId -> TeamId -> TeamSearchVisibility -> (MonadIO m, MonadHttp m) => m ResponseLBS +putSearchVisibility :: HasCallStack => (Request -> Request) -> UserId -> TeamId -> TeamSearchVisibility -> MonadHttp m => m ResponseLBS putSearchVisibility g uid tid vis = do put $ g diff --git a/services/galley/test/integration/API/Teams/Feature.hs b/services/galley/test/integration/API/Teams/Feature.hs index 3df6b0271d..d0c6ee5edf 100644 --- a/services/galley/test/integration/API/Teams/Feature.hs +++ b/services/galley/test/integration/API/Teams/Feature.hs @@ -18,7 +18,7 @@ module API.Teams.Feature (tests) where -import API.SQS (assertQueue, tActivate) +import API.SQS (assertTeamActivate) import API.Util import API.Util.TeamFeature hiding (getFeatureConfig, setLockStatusInternal) import qualified API.Util.TeamFeature as Util @@ -57,8 +57,7 @@ import qualified Wire.API.Event.FeatureConfig as FeatureConfig import Wire.API.Internal.Notification (Notification) import Wire.API.MLS.CipherSuite import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti as Multi -import Wire.API.Team.Feature (ExposeInvitationURLsToTeamAdminConfig (..), FeatureStatus (..), FeatureTTL, FeatureTTL' (..), LockStatus (LockStatusUnlocked), MLSConfig (MLSConfig)) -import qualified Wire.API.Team.Feature as Public +import Wire.API.Team.Feature hiding (setLockStatus) tests :: IO TestSetup -> TestTree tests s = @@ -69,73 +68,85 @@ tests s = test s "LegalHold - set with HTTP PUT" (testLegalHold putLegalHoldInternal), test s "LegalHold - set with HTTP PATCH" (testLegalHold patchLegalHoldInternal), test s "SearchVisibility" testSearchVisibility, - test s "DigitalSignatures" $ testSimpleFlag @Public.DigitalSignaturesConfig Public.FeatureStatusDisabled, - test s "ValidateSAMLEmails" $ testSimpleFlag @Public.ValidateSAMLEmailsConfig Public.FeatureStatusEnabled, - test s "FileSharing with lock status" $ testSimpleFlagWithLockStatus @Public.FileSharingConfig Public.FeatureStatusEnabled Public.LockStatusUnlocked, + test s "DigitalSignatures" $ testSimpleFlag @DigitalSignaturesConfig FeatureStatusDisabled, + test s "ValidateSAMLEmails" $ testSimpleFlag @ValidateSAMLEmailsConfig FeatureStatusEnabled, + test s "FileSharing with lock status" $ testSimpleFlagWithLockStatus @FileSharingConfig FeatureStatusEnabled LockStatusUnlocked, test s "Classified Domains (enabled)" testClassifiedDomainsEnabled, test s "Classified Domains (disabled)" testClassifiedDomainsDisabled, test s "All features" testAllFeatures, test s "Feature Configs / Team Features Consistency" testFeatureConfigConsistency, - test s "ConferenceCalling" $ testSimpleFlag @Public.ConferenceCallingConfig Public.FeatureStatusEnabled, + test s "ConferenceCalling" $ testSimpleFlag @ConferenceCallingConfig FeatureStatusEnabled, test s "SelfDeletingMessages" testSelfDeletingMessages, test s "ConversationGuestLinks - public API" testGuestLinksPublic, test s "ConversationGuestLinks - internal API" testGuestLinksInternal, - test s "ConversationGuestLinks - lock status" $ testSimpleFlagWithLockStatus @Public.GuestLinksConfig Public.FeatureStatusEnabled Public.LockStatusUnlocked, - test s "SndFactorPasswordChallenge - lock status" $ testSimpleFlagWithLockStatus @Public.SndFactorPasswordChallengeConfig Public.FeatureStatusDisabled Public.LockStatusLocked, + test s "ConversationGuestLinks - lock status" $ testSimpleFlagWithLockStatus @GuestLinksConfig FeatureStatusEnabled LockStatusUnlocked, + test s "SndFactorPasswordChallenge - lock status" $ testSimpleFlagWithLockStatus @SndFactorPasswordChallengeConfig FeatureStatusDisabled LockStatusLocked, test s "SearchVisibilityInbound - internal API" testSearchVisibilityInbound, test s "SearchVisibilityInbound - internal multi team API" testFeatureNoConfigMultiSearchVisibilityInbound, + test s "OutlookCalIntegration" $ testSimpleFlagWithLockStatus @OutlookCalIntegrationConfig FeatureStatusDisabled LockStatusLocked, testGroup "TTL / Conference calling" - [ test s "ConferenceCalling unlimited TTL" $ testSimpleFlagTTL @Public.ConferenceCallingConfig Public.FeatureStatusEnabled FeatureTTLUnlimited, - test s "ConferenceCalling 2s TTL" $ testSimpleFlagTTL @Public.ConferenceCallingConfig Public.FeatureStatusEnabled (FeatureTTLSeconds 2) + [ test s "ConferenceCalling unlimited TTL" $ testSimpleFlagTTL @ConferenceCallingConfig FeatureStatusEnabled FeatureTTLUnlimited, + test s "ConferenceCalling 2s TTL" $ testSimpleFlagTTL @ConferenceCallingConfig FeatureStatusEnabled (FeatureTTLSeconds 2) ], testGroup "TTL / Overrides" - [ test s "increase to unlimited" $ testSimpleFlagTTLOverride @Public.ConferenceCallingConfig Public.FeatureStatusEnabled (FeatureTTLSeconds 2) FeatureTTLUnlimited, - test s "increase" $ testSimpleFlagTTLOverride @Public.ConferenceCallingConfig Public.FeatureStatusEnabled (FeatureTTLSeconds 2) (FeatureTTLSeconds 4), - test s "reduce from unlimited" $ testSimpleFlagTTLOverride @Public.ConferenceCallingConfig Public.FeatureStatusEnabled FeatureTTLUnlimited (FeatureTTLSeconds 2), - test s "reduce" $ testSimpleFlagTTLOverride @Public.ConferenceCallingConfig Public.FeatureStatusEnabled (FeatureTTLSeconds 5) (FeatureTTLSeconds 2), - test s "Unlimited to unlimited" $ testSimpleFlagTTLOverride @Public.ConferenceCallingConfig Public.FeatureStatusEnabled FeatureTTLUnlimited FeatureTTLUnlimited + [ test s "increase to unlimited" $ testSimpleFlagTTLOverride @ConferenceCallingConfig FeatureStatusEnabled (FeatureTTLSeconds 2) FeatureTTLUnlimited, + test s "increase" $ testSimpleFlagTTLOverride @ConferenceCallingConfig FeatureStatusEnabled (FeatureTTLSeconds 2) (FeatureTTLSeconds 4), + test s "reduce from unlimited" $ testSimpleFlagTTLOverride @ConferenceCallingConfig FeatureStatusEnabled FeatureTTLUnlimited (FeatureTTLSeconds 2), + test s "reduce" $ testSimpleFlagTTLOverride @ConferenceCallingConfig FeatureStatusEnabled (FeatureTTLSeconds 5) (FeatureTTLSeconds 2), + test s "Unlimited to unlimited" $ testSimpleFlagTTLOverride @ConferenceCallingConfig FeatureStatusEnabled FeatureTTLUnlimited FeatureTTLUnlimited ], test s "MLS feature config" testMLS, - test s "SearchVisibilityInbound" $ testSimpleFlag @Public.SearchVisibilityInboundConfig Public.FeatureStatusDisabled, + test s "SearchVisibilityInbound" $ testSimpleFlag @SearchVisibilityInboundConfig FeatureStatusDisabled, + test s "MlsE2EId feature config" $ + testNonTrivialConfigNoTTL + ( withStatus + FeatureStatusDisabled + LockStatusUnlocked + (MlsE2EIdConfig Nothing) + FeatureTTLUnlimited + ), testGroup "Patch" [ -- Note: `SSOConfig` and `LegalHoldConfig` may not be able to be reset -- (depending on prior state or configuration). Thus, they cannot be -- tested here (setting random values), but are tested with separate -- tests. - test s (unpack $ Public.featureNameBS @Public.SearchVisibilityAvailableConfig) $ - testPatch IgnoreLockStatusChange Public.FeatureStatusEnabled Public.SearchVisibilityAvailableConfig, - test s (unpack $ Public.featureNameBS @Public.ValidateSAMLEmailsConfig) $ - testPatch IgnoreLockStatusChange Public.FeatureStatusEnabled Public.ValidateSAMLEmailsConfig, - test s (unpack $ Public.featureNameBS @Public.DigitalSignaturesConfig) $ - testPatch IgnoreLockStatusChange Public.FeatureStatusEnabled Public.DigitalSignaturesConfig, - test s (unpack $ Public.featureNameBS @Public.AppLockConfig) $ - testPatchWithCustomGen IgnoreLockStatusChange Public.FeatureStatusEnabled (Public.AppLockConfig (Public.EnforceAppLock False) 60) validAppLockConfigGen, - test s (unpack $ Public.featureNameBS @Public.ConferenceCallingConfig) $ - testPatch IgnoreLockStatusChange Public.FeatureStatusEnabled Public.ConferenceCallingConfig, - test s (unpack $ Public.featureNameBS @Public.SearchVisibilityAvailableConfig) $ - testPatch IgnoreLockStatusChange Public.FeatureStatusEnabled Public.SearchVisibilityAvailableConfig, - test s (unpack $ Public.featureNameBS @Public.MLSConfig) $ + test s (unpack $ featureNameBS @SearchVisibilityAvailableConfig) $ + testPatch IgnoreLockStatusChange FeatureStatusEnabled SearchVisibilityAvailableConfig, + test s (unpack $ featureNameBS @ValidateSAMLEmailsConfig) $ + testPatch IgnoreLockStatusChange FeatureStatusEnabled ValidateSAMLEmailsConfig, + test s (unpack $ featureNameBS @DigitalSignaturesConfig) $ + testPatch IgnoreLockStatusChange FeatureStatusEnabled DigitalSignaturesConfig, + test s (unpack $ featureNameBS @AppLockConfig) $ + testPatchWithCustomGen IgnoreLockStatusChange FeatureStatusEnabled (AppLockConfig (EnforceAppLock False) 60) validAppLockConfigGen, + test s (unpack $ featureNameBS @ConferenceCallingConfig) $ + testPatch IgnoreLockStatusChange FeatureStatusEnabled ConferenceCallingConfig, + test s (unpack $ featureNameBS @SearchVisibilityAvailableConfig) $ + testPatch IgnoreLockStatusChange FeatureStatusEnabled SearchVisibilityAvailableConfig, + test s (unpack $ featureNameBS @MLSConfig) $ testPatchWithCustomGen IgnoreLockStatusChange - Public.FeatureStatusEnabled - ( Public.MLSConfig + FeatureStatusEnabled + ( MLSConfig [] ProtocolProteusTag [MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519] MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 ) validMLSConfigGen, - test s (unpack $ Public.featureNameBS @Public.FileSharingConfig) $ - testPatch AssertLockStatusChange Public.FeatureStatusEnabled Public.FileSharingConfig, - test s (unpack $ Public.featureNameBS @Public.GuestLinksConfig) $ - testPatch AssertLockStatusChange Public.FeatureStatusEnabled Public.GuestLinksConfig, - test s (unpack $ Public.featureNameBS @Public.SndFactorPasswordChallengeConfig) $ - testPatch AssertLockStatusChange Public.FeatureStatusDisabled Public.SndFactorPasswordChallengeConfig, - test s (unpack $ Public.featureNameBS @Public.SelfDeletingMessagesConfig) $ - testPatch AssertLockStatusChange Public.FeatureStatusEnabled (Public.SelfDeletingMessagesConfig 0) + test s (unpack $ featureNameBS @FileSharingConfig) $ + testPatch AssertLockStatusChange FeatureStatusEnabled FileSharingConfig, + test s (unpack $ featureNameBS @GuestLinksConfig) $ + testPatch AssertLockStatusChange FeatureStatusEnabled GuestLinksConfig, + test s (unpack $ featureNameBS @SndFactorPasswordChallengeConfig) $ + testPatch AssertLockStatusChange FeatureStatusDisabled SndFactorPasswordChallengeConfig, + test s (unpack $ featureNameBS @SelfDeletingMessagesConfig) $ + testPatch AssertLockStatusChange FeatureStatusEnabled (SelfDeletingMessagesConfig 0), + test s (unpack $ featureNameBS @OutlookCalIntegrationConfig) $ + testPatch AssertLockStatusChange FeatureStatusDisabled OutlookCalIntegrationConfig, + test s (unpack $ featureNameBS @MlsE2EIdConfig) $ testPatchWithArbitrary AssertLockStatusChange FeatureStatusDisabled (MlsE2EIdConfig Nothing) ], testGroup "ExposeInvitationURLsToTeamAdmin" @@ -146,11 +157,11 @@ tests s = ] -- | Provides a `Gen` with test objects that are realistic and can easily be asserted -validMLSConfigGen :: Gen (Public.WithStatusPatch MLSConfig) +validMLSConfigGen :: Gen (WithStatusPatch MLSConfig) validMLSConfigGen = arbitrary - `suchThat` ( \cfg -> case Public.wspConfig cfg of - Just (Public.MLSConfig us _ cTags ctag) -> + `suchThat` ( \cfg -> case wspConfig cfg of + Just (MLSConfig us _ cTags ctag) -> sortedAndNoDuplicates us && sortedAndNoDuplicates cTags && elem ctag cTags @@ -159,11 +170,11 @@ validMLSConfigGen = where sortedAndNoDuplicates xs = (sort . nub) xs == xs -validAppLockConfigGen :: Gen (Public.WithStatusPatch Public.AppLockConfig) +validAppLockConfigGen :: Gen (WithStatusPatch AppLockConfig) validAppLockConfigGen = arbitrary - `suchThat` ( \cfg -> case Public.wspConfig cfg of - Just (Public.AppLockConfig _ secs) -> secs >= 30 + `suchThat` ( \cfg -> case wspConfig cfg of + Just (AppLockConfig _ secs) -> secs >= 30 Nothing -> True ) @@ -171,20 +182,39 @@ validAppLockConfigGen = data AssertLockStatusChange = AssertLockStatusChange | IgnoreLockStatusChange deriving (Eq) +testPatchWithArbitrary :: + forall cfg. + ( HasCallStack, + IsFeatureConfig cfg, + Typeable cfg, + ToSchema cfg, + Eq cfg, + Show cfg, + KnownSymbol (FeatureSymbol cfg), + Arbitrary (WithStatusPatch cfg) + ) => + AssertLockStatusChange -> + FeatureStatus -> + cfg -> + TestM () +testPatchWithArbitrary assertLockStatusChange featureStatus cfg = do + generatedConfig <- liftIO $ generate arbitrary + testPatch' assertLockStatusChange generatedConfig featureStatus cfg + testPatchWithCustomGen :: forall cfg. ( HasCallStack, - Public.IsFeatureConfig cfg, + IsFeatureConfig cfg, Typeable cfg, ToSchema cfg, Eq cfg, Show cfg, - KnownSymbol (Public.FeatureSymbol cfg) + KnownSymbol (FeatureSymbol cfg) ) => AssertLockStatusChange -> - Public.FeatureStatus -> + FeatureStatus -> cfg -> - Gen (Public.WithStatusPatch cfg) -> + Gen (WithStatusPatch cfg) -> TestM () testPatchWithCustomGen assertLockStatusChange featureStatus cfg gen = do generatedConfig <- liftIO $ generate gen @@ -193,17 +223,16 @@ testPatchWithCustomGen assertLockStatusChange featureStatus cfg gen = do testPatch :: forall cfg. ( HasCallStack, - Public.IsFeatureConfig cfg, + IsFeatureConfig cfg, Typeable cfg, ToSchema cfg, Eq cfg, Show cfg, - KnownSymbol (Public.FeatureSymbol cfg), - Arbitrary (Public.WithStatus cfg), - Arbitrary (Public.WithStatusPatch cfg) + KnownSymbol (FeatureSymbol cfg), + Arbitrary (WithStatusPatch cfg) ) => AssertLockStatusChange -> - Public.FeatureStatus -> + FeatureStatus -> cfg -> TestM () testPatch assertLockStatusChange status cfg = testPatchWithCustomGen assertLockStatusChange status cfg arbitrary @@ -211,16 +240,16 @@ testPatch assertLockStatusChange status cfg = testPatchWithCustomGen assertLockS testPatch' :: forall cfg. ( HasCallStack, - Public.IsFeatureConfig cfg, + IsFeatureConfig cfg, Typeable cfg, ToSchema cfg, Eq cfg, Show cfg, - KnownSymbol (Public.FeatureSymbol cfg) + KnownSymbol (FeatureSymbol cfg) ) => AssertLockStatusChange -> - Public.WithStatusPatch cfg -> - Public.FeatureStatus -> + WithStatusPatch cfg -> + FeatureStatus -> cfg -> TestM () testPatch' testLockStatusChange rndFeatureConfig defStatus defConfig = do @@ -229,132 +258,132 @@ testPatch' testLockStatusChange rndFeatureConfig defStatus defConfig = do patchFeatureStatusInternal tid rndFeatureConfig !!! statusCode === const 200 Just actual <- responseJsonMaybe <$> getFeatureStatusInternal @cfg tid liftIO $ - if Public.wsLockStatus actual == Public.LockStatusLocked + if wsLockStatus actual == LockStatusLocked then do - Public.wsStatus actual @?= defStatus - Public.wsConfig actual @?= defConfig + wsStatus actual @?= defStatus + wsConfig actual @?= defConfig else do - Public.wsStatus actual @?= fromMaybe (Public.wsStatus original) (Public.wspStatus rndFeatureConfig) + wsStatus actual @?= fromMaybe (wsStatus original) (wspStatus rndFeatureConfig) when (testLockStatusChange == AssertLockStatusChange) $ - Public.wsLockStatus actual @?= fromMaybe (Public.wsLockStatus original) (Public.wspLockStatus rndFeatureConfig) - Public.wsConfig actual @?= fromMaybe (Public.wsConfig original) (Public.wspConfig rndFeatureConfig) + wsLockStatus actual @?= fromMaybe (wsLockStatus original) (wspLockStatus rndFeatureConfig) + wsConfig actual @?= fromMaybe (wsConfig original) (wspConfig rndFeatureConfig) -testSSO :: (TeamId -> Public.FeatureStatus -> TestM ()) -> TestM () +testSSO :: (TeamId -> FeatureStatus -> TestM ()) -> TestM () testSSO setSSOFeature = do (_owner, tid, member : _) <- createBindingTeamWithNMembers 1 nonMember <- randomUser - let getSSO :: HasCallStack => Public.FeatureStatus -> TestM () - getSSO = assertFlagNoConfig @Public.SSOConfig $ getTeamFeatureFlag @Public.SSOConfig member tid - getSSOFeatureConfig :: HasCallStack => Public.FeatureStatus -> TestM () + let getSSO :: HasCallStack => FeatureStatus -> TestM () + getSSO = assertFlagNoConfig @SSOConfig $ getTeamFeatureFlag @SSOConfig member tid + getSSOFeatureConfig :: HasCallStack => FeatureStatus -> TestM () getSSOFeatureConfig expectedStatus = do - actual <- Util.getFeatureConfig @Public.SSOConfig member - liftIO $ Public.wsStatus actual @?= expectedStatus - getSSOInternal :: HasCallStack => Public.FeatureStatus -> TestM () - getSSOInternal = assertFlagNoConfig @Public.SSOConfig $ getTeamFeatureFlagInternal @Public.SSOConfig tid + actual <- Util.getFeatureConfig @SSOConfig member + liftIO $ wsStatus actual @?= expectedStatus + getSSOInternal :: HasCallStack => FeatureStatus -> TestM () + getSSOInternal = assertFlagNoConfig @SSOConfig $ getTeamFeatureFlagInternal @SSOConfig tid - assertFlagForbidden $ getTeamFeatureFlag @Public.SSOConfig nonMember tid + assertFlagForbidden $ getTeamFeatureFlag @SSOConfig nonMember tid featureSSO <- view (tsGConf . optSettings . setFeatureFlags . flagSSO) case featureSSO of FeatureSSODisabledByDefault -> do -- Test default - getSSO Public.FeatureStatusDisabled - getSSOInternal Public.FeatureStatusDisabled - getSSOFeatureConfig Public.FeatureStatusDisabled + getSSO FeatureStatusDisabled + getSSOInternal FeatureStatusDisabled + getSSOFeatureConfig FeatureStatusDisabled -- Test override - setSSOFeature tid Public.FeatureStatusEnabled - getSSO Public.FeatureStatusEnabled - getSSOInternal Public.FeatureStatusEnabled - getSSOFeatureConfig Public.FeatureStatusEnabled + setSSOFeature tid FeatureStatusEnabled + getSSO FeatureStatusEnabled + getSSOInternal FeatureStatusEnabled + getSSOFeatureConfig FeatureStatusEnabled FeatureSSOEnabledByDefault -> do -- since we don't allow to disable (see 'disableSsoNotImplemented'), we can't test -- much here. (disable failure is covered in "enable/disable SSO" above.) - getSSO Public.FeatureStatusEnabled - getSSOInternal Public.FeatureStatusEnabled - getSSOFeatureConfig Public.FeatureStatusEnabled + getSSO FeatureStatusEnabled + getSSOInternal FeatureStatusEnabled + getSSOFeatureConfig FeatureStatusEnabled -putSSOInternal :: HasCallStack => TeamId -> Public.FeatureStatus -> TestM () +putSSOInternal :: HasCallStack => TeamId -> FeatureStatus -> TestM () putSSOInternal tid = void - . putTeamFeatureFlagInternal @Public.SSOConfig expect2xx tid - . (\st -> Public.WithStatusNoLock st Public.SSOConfig Public.FeatureTTLUnlimited) + . putTeamFeatureFlagInternal @SSOConfig expect2xx tid + . (\st -> WithStatusNoLock st SSOConfig FeatureTTLUnlimited) -patchSSOInternal :: HasCallStack => TeamId -> Public.FeatureStatus -> TestM () -patchSSOInternal tid status = void $ patchFeatureStatusInternalWithMod @Public.SSOConfig expect2xx tid (Public.withStatus' (Just status) Nothing Nothing (Just Public.FeatureTTLUnlimited)) +patchSSOInternal :: HasCallStack => TeamId -> FeatureStatus -> TestM () +patchSSOInternal tid status = void $ patchFeatureStatusInternalWithMod @SSOConfig expect2xx tid (withStatus' (Just status) Nothing Nothing (Just FeatureTTLUnlimited)) -testLegalHold :: ((Request -> Request) -> TeamId -> Public.FeatureStatus -> TestM ()) -> TestM () +testLegalHold :: ((Request -> Request) -> TeamId -> FeatureStatus -> TestM ()) -> TestM () testLegalHold setLegalHoldInternal = do (_owner, tid, member : _) <- createBindingTeamWithNMembers 1 nonMember <- randomUser - let getLegalHold :: HasCallStack => Public.FeatureStatus -> TestM () - getLegalHold = assertFlagNoConfig @Public.LegalholdConfig $ getTeamFeatureFlag @Public.LegalholdConfig member tid - getLegalHoldInternal :: HasCallStack => Public.FeatureStatus -> TestM () - getLegalHoldInternal = assertFlagNoConfig @Public.LegalholdConfig $ getTeamFeatureFlagInternal @Public.LegalholdConfig tid + let getLegalHold :: HasCallStack => FeatureStatus -> TestM () + getLegalHold = assertFlagNoConfig @LegalholdConfig $ getTeamFeatureFlag @LegalholdConfig member tid + getLegalHoldInternal :: HasCallStack => FeatureStatus -> TestM () + getLegalHoldInternal = assertFlagNoConfig @LegalholdConfig $ getTeamFeatureFlagInternal @LegalholdConfig tid getLegalHoldFeatureConfig expectedStatus = do - actual <- Util.getFeatureConfig @Public.LegalholdConfig member - liftIO $ Public.wsStatus actual @?= expectedStatus + actual <- Util.getFeatureConfig @LegalholdConfig member + liftIO $ wsStatus actual @?= expectedStatus - getLegalHold Public.FeatureStatusDisabled - getLegalHoldInternal Public.FeatureStatusDisabled + getLegalHold FeatureStatusDisabled + getLegalHoldInternal FeatureStatusDisabled - assertFlagForbidden $ getTeamFeatureFlag @Public.LegalholdConfig nonMember tid + assertFlagForbidden $ getTeamFeatureFlag @LegalholdConfig nonMember tid -- FUTUREWORK: run two galleys, like below for custom search visibility. featureLegalHold <- view (tsGConf . optSettings . setFeatureFlags . flagLegalHold) case featureLegalHold of FeatureLegalHoldDisabledByDefault -> do -- Test default - getLegalHold Public.FeatureStatusDisabled - getLegalHoldInternal Public.FeatureStatusDisabled - getLegalHoldFeatureConfig Public.FeatureStatusDisabled + getLegalHold FeatureStatusDisabled + getLegalHoldInternal FeatureStatusDisabled + getLegalHoldFeatureConfig FeatureStatusDisabled -- Test override - setLegalHoldInternal expect2xx tid Public.FeatureStatusEnabled - getLegalHold Public.FeatureStatusEnabled - getLegalHoldInternal Public.FeatureStatusEnabled - getLegalHoldFeatureConfig Public.FeatureStatusEnabled + setLegalHoldInternal expect2xx tid FeatureStatusEnabled + getLegalHold FeatureStatusEnabled + getLegalHoldInternal FeatureStatusEnabled + getLegalHoldFeatureConfig FeatureStatusEnabled -- turned off for instance FeatureLegalHoldDisabledPermanently -> do - setLegalHoldInternal expect4xx tid Public.FeatureStatusEnabled + setLegalHoldInternal expect4xx tid FeatureStatusEnabled -- turned off but for whitelisted teams with implicit consent FeatureLegalHoldWhitelistTeamsAndImplicitConsent -> do - setLegalHoldInternal expect4xx tid Public.FeatureStatusEnabled + setLegalHoldInternal expect4xx tid FeatureStatusEnabled -putLegalHoldInternal :: HasCallStack => (Request -> Request) -> TeamId -> Public.FeatureStatus -> TestM () +putLegalHoldInternal :: HasCallStack => (Request -> Request) -> TeamId -> FeatureStatus -> TestM () putLegalHoldInternal expectation tid = void - . putTeamFeatureFlagInternal @Public.LegalholdConfig expectation tid - . (\st -> Public.WithStatusNoLock st Public.LegalholdConfig Public.FeatureTTLUnlimited) + . putTeamFeatureFlagInternal @LegalholdConfig expectation tid + . (\st -> WithStatusNoLock st LegalholdConfig FeatureTTLUnlimited) -patchLegalHoldInternal :: HasCallStack => (Request -> Request) -> TeamId -> Public.FeatureStatus -> TestM () -patchLegalHoldInternal expectation tid status = void $ patchFeatureStatusInternalWithMod @Public.LegalholdConfig expectation tid (Public.withStatus' (Just status) Nothing Nothing (Just Public.FeatureTTLUnlimited)) +patchLegalHoldInternal :: HasCallStack => (Request -> Request) -> TeamId -> FeatureStatus -> TestM () +patchLegalHoldInternal expectation tid status = void $ patchFeatureStatusInternalWithMod @LegalholdConfig expectation tid (withStatus' (Just status) Nothing Nothing (Just FeatureTTLUnlimited)) testSearchVisibility :: TestM () testSearchVisibility = do - let getTeamSearchVisibility :: TeamId -> UserId -> Public.FeatureStatus -> TestM () + let getTeamSearchVisibility :: TeamId -> UserId -> FeatureStatus -> TestM () getTeamSearchVisibility teamid uid expected = do g <- viewGalley getTeamSearchVisibilityAvailable g uid teamid !!! do statusCode === const 200 - responseJsonEither === const (Right (Public.WithStatusNoLock expected Public.SearchVisibilityAvailableConfig Public.FeatureTTLUnlimited)) + responseJsonEither === const (Right (WithStatusNoLock expected SearchVisibilityAvailableConfig FeatureTTLUnlimited)) - let getTeamSearchVisibilityInternal :: TeamId -> Public.FeatureStatus -> TestM () + let getTeamSearchVisibilityInternal :: TeamId -> FeatureStatus -> TestM () getTeamSearchVisibilityInternal teamid expected = do g <- viewGalley getTeamSearchVisibilityAvailableInternal g teamid !!! do statusCode === const 200 - responseJsonEither === const (Right (Public.WithStatusNoLock expected Public.SearchVisibilityAvailableConfig Public.FeatureTTLUnlimited)) + responseJsonEither === const (Right (WithStatusNoLock expected SearchVisibilityAvailableConfig FeatureTTLUnlimited)) - let getTeamSearchVisibilityFeatureConfig :: UserId -> Public.FeatureStatus -> TestM () + let getTeamSearchVisibilityFeatureConfig :: UserId -> FeatureStatus -> TestM () getTeamSearchVisibilityFeatureConfig uid expected = do - actual <- Util.getFeatureConfig @Public.SearchVisibilityAvailableConfig uid - liftIO $ Public.wsStatus actual @?= expected + actual <- Util.getFeatureConfig @SearchVisibilityAvailableConfig uid + liftIO $ wsStatus actual @?= expected - let setTeamSearchVisibilityInternal :: TeamId -> Public.FeatureStatus -> TestM () + let setTeamSearchVisibilityInternal :: TeamId -> FeatureStatus -> TestM () setTeamSearchVisibilityInternal teamid val = do g <- viewGalley putTeamSearchVisibilityAvailableInternal g teamid val @@ -362,74 +391,74 @@ testSearchVisibility = do (owner, tid, [member]) <- createBindingTeamWithNMembers 1 nonMember <- randomUser - assertFlagForbidden $ getTeamFeatureFlag @Public.SearchVisibilityAvailableConfig nonMember tid + assertFlagForbidden $ getTeamFeatureFlag @SearchVisibilityAvailableConfig nonMember tid withCustomSearchFeature FeatureTeamSearchVisibilityUnavailableByDefault $ do - getTeamSearchVisibility tid owner Public.FeatureStatusDisabled - getTeamSearchVisibilityInternal tid Public.FeatureStatusDisabled - getTeamSearchVisibilityFeatureConfig member Public.FeatureStatusDisabled + getTeamSearchVisibility tid owner FeatureStatusDisabled + getTeamSearchVisibilityInternal tid FeatureStatusDisabled + getTeamSearchVisibilityFeatureConfig member FeatureStatusDisabled - setTeamSearchVisibilityInternal tid Public.FeatureStatusEnabled - getTeamSearchVisibility tid owner Public.FeatureStatusEnabled - getTeamSearchVisibilityInternal tid Public.FeatureStatusEnabled - getTeamSearchVisibilityFeatureConfig member Public.FeatureStatusEnabled + setTeamSearchVisibilityInternal tid FeatureStatusEnabled + getTeamSearchVisibility tid owner FeatureStatusEnabled + getTeamSearchVisibilityInternal tid FeatureStatusEnabled + getTeamSearchVisibilityFeatureConfig member FeatureStatusEnabled - setTeamSearchVisibilityInternal tid Public.FeatureStatusDisabled - getTeamSearchVisibility tid owner Public.FeatureStatusDisabled - getTeamSearchVisibilityInternal tid Public.FeatureStatusDisabled - getTeamSearchVisibilityFeatureConfig member Public.FeatureStatusDisabled + setTeamSearchVisibilityInternal tid FeatureStatusDisabled + getTeamSearchVisibility tid owner FeatureStatusDisabled + getTeamSearchVisibilityInternal tid FeatureStatusDisabled + getTeamSearchVisibilityFeatureConfig member FeatureStatusDisabled (owner2, tid2, team2member : _) <- createBindingTeamWithNMembers 1 withCustomSearchFeature FeatureTeamSearchVisibilityAvailableByDefault $ do - getTeamSearchVisibility tid2 owner2 Public.FeatureStatusEnabled - getTeamSearchVisibilityInternal tid2 Public.FeatureStatusEnabled - getTeamSearchVisibilityFeatureConfig team2member Public.FeatureStatusEnabled + getTeamSearchVisibility tid2 owner2 FeatureStatusEnabled + getTeamSearchVisibilityInternal tid2 FeatureStatusEnabled + getTeamSearchVisibilityFeatureConfig team2member FeatureStatusEnabled - setTeamSearchVisibilityInternal tid2 Public.FeatureStatusDisabled - getTeamSearchVisibility tid2 owner2 Public.FeatureStatusDisabled - getTeamSearchVisibilityInternal tid2 Public.FeatureStatusDisabled - getTeamSearchVisibilityFeatureConfig team2member Public.FeatureStatusDisabled + setTeamSearchVisibilityInternal tid2 FeatureStatusDisabled + getTeamSearchVisibility tid2 owner2 FeatureStatusDisabled + getTeamSearchVisibilityInternal tid2 FeatureStatusDisabled + getTeamSearchVisibilityFeatureConfig team2member FeatureStatusDisabled - setTeamSearchVisibilityInternal tid2 Public.FeatureStatusEnabled - getTeamSearchVisibility tid2 owner2 Public.FeatureStatusEnabled - getTeamSearchVisibilityInternal tid2 Public.FeatureStatusEnabled - getTeamSearchVisibilityFeatureConfig team2member Public.FeatureStatusEnabled + setTeamSearchVisibilityInternal tid2 FeatureStatusEnabled + getTeamSearchVisibility tid2 owner2 FeatureStatusEnabled + getTeamSearchVisibilityInternal tid2 FeatureStatusEnabled + getTeamSearchVisibilityFeatureConfig team2member FeatureStatusEnabled getClassifiedDomains :: (HasCallStack, HasGalley m, MonadIO m, MonadHttp m, MonadCatch m) => UserId -> TeamId -> - Public.WithStatusNoLock Public.ClassifiedDomainsConfig -> + WithStatusNoLock ClassifiedDomainsConfig -> m () getClassifiedDomains member tid = - assertFlagWithConfig @Public.ClassifiedDomainsConfig $ - getTeamFeatureFlag @Public.ClassifiedDomainsConfig member tid + assertFlagWithConfig @ClassifiedDomainsConfig $ + getTeamFeatureFlag @ClassifiedDomainsConfig member tid getClassifiedDomainsInternal :: (HasCallStack, HasGalley m, MonadIO m, MonadHttp m, MonadCatch m) => TeamId -> - Public.WithStatusNoLock Public.ClassifiedDomainsConfig -> + WithStatusNoLock ClassifiedDomainsConfig -> m () getClassifiedDomainsInternal tid = - assertFlagWithConfig @Public.ClassifiedDomainsConfig $ - getTeamFeatureFlagInternal @Public.ClassifiedDomainsConfig tid + assertFlagWithConfig @ClassifiedDomainsConfig $ + getTeamFeatureFlagInternal @ClassifiedDomainsConfig tid testClassifiedDomainsEnabled :: TestM () testClassifiedDomainsEnabled = do (_owner, tid, member : _) <- createBindingTeamWithNMembers 1 let expected = - Public.WithStatusNoLock Public.FeatureStatusEnabled (Public.ClassifiedDomainsConfig [Domain "example.com"]) Public.FeatureTTLUnlimited + WithStatusNoLock FeatureStatusEnabled (ClassifiedDomainsConfig [Domain "example.com"]) FeatureTTLUnlimited let getClassifiedDomainsFeatureConfig :: (HasCallStack, HasGalley m, MonadIO m, MonadHttp m, MonadCatch m) => UserId -> - Public.WithStatusNoLock Public.ClassifiedDomainsConfig -> + WithStatusNoLock ClassifiedDomainsConfig -> m () getClassifiedDomainsFeatureConfig uid expected' = do - result <- Util.getFeatureConfig @Public.ClassifiedDomainsConfig uid - liftIO $ Public.wsStatus result @?= Public.wssStatus expected' - liftIO $ Public.wsConfig result @?= Public.wssConfig expected' + result <- Util.getFeatureConfig @ClassifiedDomainsConfig uid + liftIO $ wsStatus result @?= wssStatus expected' + liftIO $ wsConfig result @?= wssConfig expected' getClassifiedDomains member tid expected getClassifiedDomainsInternal tid expected @@ -439,23 +468,23 @@ testClassifiedDomainsDisabled :: TestM () testClassifiedDomainsDisabled = do (_owner, tid, member : _) <- createBindingTeamWithNMembers 1 let expected = - Public.WithStatusNoLock Public.FeatureStatusDisabled (Public.ClassifiedDomainsConfig []) Public.FeatureTTLUnlimited + WithStatusNoLock FeatureStatusDisabled (ClassifiedDomainsConfig []) FeatureTTLUnlimited let getClassifiedDomainsFeatureConfig :: (HasCallStack, HasGalley m, MonadIO m, MonadHttp m, MonadCatch m) => UserId -> - Public.WithStatusNoLock Public.ClassifiedDomainsConfig -> + WithStatusNoLock ClassifiedDomainsConfig -> m () getClassifiedDomainsFeatureConfig uid expected' = do - result <- Util.getFeatureConfig @Public.ClassifiedDomainsConfig uid - liftIO $ Public.wsStatus result @?= Public.wssStatus expected' - liftIO $ Public.wsConfig result @?= Public.wssConfig expected' + result <- Util.getFeatureConfig @ClassifiedDomainsConfig uid + liftIO $ wsStatus result @?= wssStatus expected' + liftIO $ wsConfig result @?= wssConfig expected' let classifiedDomainsDisabled opts = opts & over (optSettings . setFeatureFlags . flagClassifiedDomains) - (\(ImplicitLockStatus s) -> ImplicitLockStatus (s & Public.setStatus Public.FeatureStatusDisabled & Public.setConfig (Public.ClassifiedDomainsConfig []))) + (\(ImplicitLockStatus s) -> ImplicitLockStatus (s & setStatus FeatureStatusDisabled & setConfig (ClassifiedDomainsConfig []))) withSettingsOverrides classifiedDomainsDisabled $ do getClassifiedDomains member tid expected getClassifiedDomainsInternal tid expected @@ -465,14 +494,13 @@ testSimpleFlag :: forall cfg. ( HasCallStack, Typeable cfg, - Public.IsFeatureConfig cfg, - KnownSymbol (Public.FeatureSymbol cfg), - Public.FeatureTrivialConfig cfg, + IsFeatureConfig cfg, + KnownSymbol (FeatureSymbol cfg), + FeatureTrivialConfig cfg, ToSchema cfg, - FromJSON (Public.WithStatusNoLock cfg), - ToJSON (Public.WithStatusNoLock cfg) + FromJSON (WithStatusNoLock cfg) ) => - Public.FeatureStatus -> + FeatureStatus -> TestM () testSimpleFlag defaultValue = testSimpleFlagTTL @cfg defaultValue FeatureTTLUnlimited @@ -480,14 +508,13 @@ testSimpleFlagTTLOverride :: forall cfg. ( HasCallStack, Typeable cfg, - Public.IsFeatureConfig cfg, - KnownSymbol (Public.FeatureSymbol cfg), - Public.FeatureTrivialConfig cfg, + IsFeatureConfig cfg, + KnownSymbol (FeatureSymbol cfg), + FeatureTrivialConfig cfg, ToSchema cfg, - FromJSON (Public.WithStatusNoLock cfg), - ToJSON (Public.WithStatusNoLock cfg) + FromJSON (WithStatusNoLock cfg) ) => - Public.FeatureStatus -> + FeatureStatus -> FeatureTTL -> FeatureTTL -> TestM () @@ -495,23 +522,23 @@ testSimpleFlagTTLOverride defaultValue ttl ttlAfter = do (_owner, tid, member : _) <- createBindingTeamWithNMembers 1 nonMember <- randomUser - let getFlag :: HasCallStack => Public.FeatureStatus -> TestM () + let getFlag :: HasCallStack => FeatureStatus -> TestM () getFlag expected = eventually $ do flip (assertFlagNoConfig @cfg) expected $ getTeamFeatureFlag @cfg member tid - getFeatureConfig :: HasCallStack => Public.FeatureStatus -> FeatureTTL -> TestM () + getFeatureConfig :: HasCallStack => FeatureStatus -> FeatureTTL -> TestM () getFeatureConfig expectedStatus expectedTtl = eventually $ do actual <- Util.getFeatureConfig @cfg member - liftIO $ Public.wsStatus actual @?= expectedStatus - liftIO $ checkTtl (Public.wsTTL actual) expectedTtl + liftIO $ wsStatus actual @?= expectedStatus + liftIO $ checkTtl (wsTTL actual) expectedTtl - getFlagInternal :: HasCallStack => Public.FeatureStatus -> TestM () + getFlagInternal :: HasCallStack => FeatureStatus -> TestM () getFlagInternal expected = eventually $ do flip (assertFlagNoConfig @cfg) expected $ getTeamFeatureFlagInternal @cfg tid - setFlagInternal :: Public.FeatureStatus -> FeatureTTL -> TestM () + setFlagInternal :: FeatureStatus -> FeatureTTL -> TestM () setFlagInternal statusValue ttl' = - void $ putTeamFeatureFlagInternalTTL @cfg expect2xx tid (Public.WithStatusNoLock statusValue (Public.trivialConfig @cfg) ttl') + void $ putTeamFeatureFlagInternalTTL @cfg expect2xx tid (WithStatusNoLock statusValue (trivialConfig @cfg) ttl') select :: PrepQuery R (Identity TeamId) (Identity (Maybe FeatureTTL)) select = fromString "select ttl(conference_calling) from team_features where team_id = ?" @@ -554,8 +581,8 @@ testSimpleFlagTTLOverride defaultValue ttl ttlAfter = do assertFlagForbidden $ getTeamFeatureFlag @cfg nonMember tid let otherValue = case defaultValue of - Public.FeatureStatusDisabled -> Public.FeatureStatusEnabled - Public.FeatureStatusEnabled -> Public.FeatureStatusDisabled + FeatureStatusDisabled -> FeatureStatusEnabled + FeatureStatusEnabled -> FeatureStatusDisabled -- Initial value should be the default value getFlag defaultValue @@ -621,36 +648,35 @@ testSimpleFlagTTL :: forall cfg. ( HasCallStack, Typeable cfg, - Public.IsFeatureConfig cfg, - KnownSymbol (Public.FeatureSymbol cfg), - Public.FeatureTrivialConfig cfg, + IsFeatureConfig cfg, + KnownSymbol (FeatureSymbol cfg), + FeatureTrivialConfig cfg, ToSchema cfg, - FromJSON (Public.WithStatusNoLock cfg), - ToJSON (Public.WithStatusNoLock cfg) + FromJSON (WithStatusNoLock cfg) ) => - Public.FeatureStatus -> + FeatureStatus -> FeatureTTL -> TestM () testSimpleFlagTTL defaultValue ttl = do (_owner, tid, member : _) <- createBindingTeamWithNMembers 1 nonMember <- randomUser - let getFlag :: HasCallStack => Public.FeatureStatus -> TestM () + let getFlag :: HasCallStack => FeatureStatus -> TestM () getFlag expected = flip (assertFlagNoConfig @cfg) expected $ getTeamFeatureFlag @cfg member tid - getFeatureConfig :: HasCallStack => Public.FeatureStatus -> TestM () + getFeatureConfig :: HasCallStack => FeatureStatus -> TestM () getFeatureConfig expected = do actual <- Util.getFeatureConfig @cfg member - liftIO $ Public.wsStatus actual @?= expected + liftIO $ wsStatus actual @?= expected - getFlagInternal :: HasCallStack => Public.FeatureStatus -> TestM () + getFlagInternal :: HasCallStack => FeatureStatus -> TestM () getFlagInternal expected = flip (assertFlagNoConfig @cfg) expected $ getTeamFeatureFlagInternal @cfg tid - setFlagInternal :: Public.FeatureStatus -> FeatureTTL -> TestM () + setFlagInternal :: FeatureStatus -> FeatureTTL -> TestM () setFlagInternal statusValue ttl' = - void $ putTeamFeatureFlagInternalTTL @cfg expect2xx tid (Public.WithStatusNoLock statusValue (Public.trivialConfig @cfg) ttl') + void $ putTeamFeatureFlagInternalTTL @cfg expect2xx tid (WithStatusNoLock statusValue (trivialConfig @cfg) ttl') select :: PrepQuery R (Identity TeamId) (Identity (Maybe FeatureTTL)) select = fromString "select ttl(conference_calling) from team_features where team_id = ?" @@ -678,8 +704,8 @@ testSimpleFlagTTL defaultValue ttl = do assertFlagForbidden $ getTeamFeatureFlag @cfg nonMember tid let otherValue = case defaultValue of - Public.FeatureStatusDisabled -> Public.FeatureStatusEnabled - Public.FeatureStatusEnabled -> Public.FeatureStatusDisabled + FeatureStatusDisabled -> FeatureStatusEnabled + FeatureStatusEnabled -> FeatureStatusDisabled -- Initial value should be the default value getFlag defaultValue @@ -719,33 +745,32 @@ testSimpleFlagWithLockStatus :: Typeable cfg, Eq cfg, Show cfg, - Public.FeatureTrivialConfig cfg, - Public.IsFeatureConfig cfg, - KnownSymbol (Public.FeatureSymbol cfg), + FeatureTrivialConfig cfg, + IsFeatureConfig cfg, + KnownSymbol (FeatureSymbol cfg), ToSchema cfg, - FromJSON (Public.WithStatusNoLock cfg), - ToJSON (Public.WithStatusNoLock cfg) + ToJSON (WithStatusNoLock cfg) ) => - Public.FeatureStatus -> - Public.LockStatus -> + FeatureStatus -> + LockStatus -> TestM () testSimpleFlagWithLockStatus defaultStatus defaultLockStatus = do galley <- viewGalley (owner, tid, member : _) <- createBindingTeamWithNMembers 1 nonMember <- randomUser - let getFlag :: HasCallStack => Public.FeatureStatus -> Public.LockStatus -> TestM () + let getFlag :: HasCallStack => FeatureStatus -> LockStatus -> TestM () getFlag expectedStatus expectedLockStatus = do let flag = getTeamFeatureFlag @cfg member tid assertFlagNoConfigWithLockStatus @cfg flag expectedStatus expectedLockStatus - getFeatureConfig :: HasCallStack => Public.FeatureStatus -> Public.LockStatus -> TestM () + getFeatureConfig :: HasCallStack => FeatureStatus -> LockStatus -> TestM () getFeatureConfig expectedStatus expectedLockStatus = do actual <- Util.getFeatureConfig @cfg member - liftIO $ Public.wsStatus actual @?= expectedStatus - liftIO $ Public.wsLockStatus actual @?= expectedLockStatus + liftIO $ wsStatus actual @?= expectedStatus + liftIO $ wsLockStatus actual @?= expectedLockStatus - getFlagInternal :: HasCallStack => Public.FeatureStatus -> Public.LockStatus -> TestM () + getFlagInternal :: HasCallStack => FeatureStatus -> LockStatus -> TestM () getFlagInternal expectedStatus expectedLockStatus = do let flag = getTeamFeatureFlagInternal @cfg tid assertFlagNoConfigWithLockStatus @cfg flag expectedStatus expectedLockStatus @@ -755,19 +780,19 @@ testSimpleFlagWithLockStatus defaultStatus defaultLockStatus = do getFeatureConfig expectedStatus expectedLockStatus getFlagInternal expectedStatus expectedLockStatus - setFlagWithGalley :: Public.FeatureStatus -> TestM () + setFlagWithGalley :: FeatureStatus -> TestM () setFlagWithGalley statusValue = - putTeamFeatureFlagWithGalley @cfg galley owner tid (Public.WithStatusNoLock statusValue (Public.trivialConfig @cfg) Public.FeatureTTLUnlimited) + putTeamFeatureFlagWithGalley @cfg galley owner tid (WithStatusNoLock statusValue (trivialConfig @cfg) FeatureTTLUnlimited) !!! statusCode === const 200 - assertSetStatusForbidden :: Public.FeatureStatus -> TestM () + assertSetStatusForbidden :: FeatureStatus -> TestM () assertSetStatusForbidden statusValue = - putTeamFeatureFlagWithGalley @cfg galley owner tid (Public.WithStatusNoLock statusValue (Public.trivialConfig @cfg) Public.FeatureTTLUnlimited) + putTeamFeatureFlagWithGalley @cfg galley owner tid (WithStatusNoLock statusValue (trivialConfig @cfg) FeatureTTLUnlimited) !!! statusCode === const 409 - setLockStatus :: Public.LockStatus -> TestM () + setLockStatus :: LockStatus -> TestM () setLockStatus lockStatus = Util.setLockStatusInternal @cfg galley tid lockStatus !!! statusCode @@ -776,14 +801,14 @@ testSimpleFlagWithLockStatus defaultStatus defaultLockStatus = do assertFlagForbidden $ getTeamFeatureFlag @cfg nonMember tid let otherStatus = case defaultStatus of - Public.FeatureStatusDisabled -> Public.FeatureStatusEnabled - Public.FeatureStatusEnabled -> Public.FeatureStatusDisabled + FeatureStatusDisabled -> FeatureStatusEnabled + FeatureStatusEnabled -> FeatureStatusDisabled -- Initial status and lock status should be the defaults getFlags defaultStatus defaultLockStatus -- unlock feature if it is locked - when (defaultLockStatus == Public.LockStatusLocked) $ setLockStatus Public.LockStatusUnlocked + when (defaultLockStatus == LockStatusLocked) $ setLockStatus LockStatusUnlocked -- setting should work cannon <- view tsCannon @@ -792,19 +817,19 @@ testSimpleFlagWithLockStatus defaultStatus defaultLockStatus = do setFlagWithGalley otherStatus void . liftIO $ WS.assertMatch (5 # Second) ws $ - wsAssertFeatureConfigWithLockStatusUpdate @cfg otherStatus Public.LockStatusUnlocked + wsAssertFeatureConfigWithLockStatusUpdate @cfg otherStatus LockStatusUnlocked - getFlags otherStatus Public.LockStatusUnlocked + getFlags otherStatus LockStatusUnlocked -- lock feature - setLockStatus Public.LockStatusLocked + setLockStatus LockStatusLocked -- feature status should now be the default again - getFlags defaultStatus Public.LockStatusLocked + getFlags defaultStatus LockStatusLocked assertSetStatusForbidden defaultStatus -- unlock feature - setLockStatus Public.LockStatusUnlocked + setLockStatus LockStatusUnlocked -- feature status should be the previously set value - getFlags otherStatus Public.LockStatusUnlocked + getFlags otherStatus LockStatusUnlocked -- clean up setFlagWithGalley defaultStatus @@ -813,34 +838,34 @@ testSimpleFlagWithLockStatus defaultStatus defaultLockStatus = do testSelfDeletingMessages :: TestM () testSelfDeletingMessages = do - defLockStatus :: Public.LockStatus <- + defLockStatus :: LockStatus <- view ( tsGConf . optSettings . setFeatureFlags . flagSelfDeletingMessages . unDefaults - . to Public.wsLockStatus + . to wsLockStatus ) -- personal users - let settingWithoutLockStatus :: FeatureStatus -> Int32 -> Public.WithStatusNoLock Public.SelfDeletingMessagesConfig + let settingWithoutLockStatus :: FeatureStatus -> Int32 -> WithStatusNoLock SelfDeletingMessagesConfig settingWithoutLockStatus stat tout = - Public.WithStatusNoLock + WithStatusNoLock stat - (Public.SelfDeletingMessagesConfig tout) - Public.FeatureTTLUnlimited - settingWithLockStatus :: FeatureStatus -> Int32 -> Public.LockStatus -> Public.WithStatus Public.SelfDeletingMessagesConfig + (SelfDeletingMessagesConfig tout) + FeatureTTLUnlimited + settingWithLockStatus :: FeatureStatus -> Int32 -> LockStatus -> WithStatus SelfDeletingMessagesConfig settingWithLockStatus stat tout lockStatus = - Public.withStatus + withStatus stat lockStatus - (Public.SelfDeletingMessagesConfig tout) - Public.FeatureTTLUnlimited + (SelfDeletingMessagesConfig tout) + FeatureTTLUnlimited personalUser <- randomUser do - result <- Util.getFeatureConfig @Public.SelfDeletingMessagesConfig personalUser + result <- Util.getFeatureConfig @SelfDeletingMessagesConfig personalUser liftIO $ result @?= settingWithLockStatus FeatureStatusEnabled 0 defLockStatus -- team users @@ -850,7 +875,7 @@ testSelfDeletingMessages = do let checkSet :: FeatureStatus -> Int32 -> Int -> TestM () checkSet stat tout expectedStatusCode = do - putTeamFeatureFlagInternal @Public.SelfDeletingMessagesConfig + putTeamFeatureFlagInternal @SelfDeletingMessagesConfig galley tid (settingWithoutLockStatus stat tout) @@ -858,21 +883,21 @@ testSelfDeletingMessages = do === const expectedStatusCode -- internal, public (/team/:tid/features), and team-agnostic (/feature-configs). - checkGet :: HasCallStack => FeatureStatus -> Int32 -> Public.LockStatus -> TestM () + checkGet :: HasCallStack => FeatureStatus -> Int32 -> LockStatus -> TestM () checkGet stat tout lockStatus = do let expected = settingWithLockStatus stat tout lockStatus forM_ - [ getTeamFeatureFlagInternal @Public.SelfDeletingMessagesConfig tid, - getTeamFeatureFlagWithGalley @Public.SelfDeletingMessagesConfig galley owner tid + [ getTeamFeatureFlagInternal @SelfDeletingMessagesConfig tid, + getTeamFeatureFlagWithGalley @SelfDeletingMessagesConfig galley owner tid ] (!!! responseJsonEither === const (Right expected)) - result <- Util.getFeatureConfig @Public.SelfDeletingMessagesConfig owner + result <- Util.getFeatureConfig @SelfDeletingMessagesConfig owner liftIO $ result @?= expected - checkSetLockStatus :: HasCallStack => Public.LockStatus -> TestM () + checkSetLockStatus :: HasCallStack => LockStatus -> TestM () checkSetLockStatus status = do - Util.setLockStatusInternal @Public.SelfDeletingMessagesConfig galley tid status + Util.setLockStatusInternal @SelfDeletingMessagesConfig galley tid status !!! statusCode === const 200 @@ -882,100 +907,100 @@ testSelfDeletingMessages = do checkGet FeatureStatusEnabled 0 defLockStatus case defLockStatus of - Public.LockStatusLocked -> do + LockStatusLocked -> do checkSet FeatureStatusDisabled 0 409 - Public.LockStatusUnlocked -> do + LockStatusUnlocked -> do checkSet FeatureStatusDisabled 0 200 - checkGet FeatureStatusDisabled 0 Public.LockStatusUnlocked + checkGet FeatureStatusDisabled 0 LockStatusUnlocked checkSet FeatureStatusEnabled 0 200 - checkGet FeatureStatusEnabled 0 Public.LockStatusUnlocked + checkGet FeatureStatusEnabled 0 LockStatusUnlocked -- now don't worry about what's in the config, write something to cassandra, and test with that. - checkSetLockStatus Public.LockStatusLocked - checkGet FeatureStatusEnabled 0 Public.LockStatusLocked + checkSetLockStatus LockStatusLocked + checkGet FeatureStatusEnabled 0 LockStatusLocked checkSet FeatureStatusDisabled 0 409 - checkGet FeatureStatusEnabled 0 Public.LockStatusLocked + checkGet FeatureStatusEnabled 0 LockStatusLocked checkSet FeatureStatusEnabled 30 409 - checkGet FeatureStatusEnabled 0 Public.LockStatusLocked - checkSetLockStatus Public.LockStatusUnlocked - checkGet FeatureStatusEnabled 0 Public.LockStatusUnlocked + checkGet FeatureStatusEnabled 0 LockStatusLocked + checkSetLockStatus LockStatusUnlocked + checkGet FeatureStatusEnabled 0 LockStatusUnlocked checkSet FeatureStatusDisabled 0 200 - checkGet FeatureStatusDisabled 0 Public.LockStatusUnlocked + checkGet FeatureStatusDisabled 0 LockStatusUnlocked checkSet FeatureStatusEnabled 30 200 - checkGet FeatureStatusEnabled 30 Public.LockStatusUnlocked + checkGet FeatureStatusEnabled 30 LockStatusUnlocked checkSet FeatureStatusDisabled 30 200 - checkGet FeatureStatusDisabled 30 Public.LockStatusUnlocked - checkSetLockStatus Public.LockStatusLocked - checkGet FeatureStatusEnabled 0 Public.LockStatusLocked + checkGet FeatureStatusDisabled 30 LockStatusUnlocked + checkSetLockStatus LockStatusLocked + checkGet FeatureStatusEnabled 0 LockStatusLocked checkSet FeatureStatusEnabled 50 409 - checkSetLockStatus Public.LockStatusUnlocked - checkGet FeatureStatusDisabled 30 Public.LockStatusUnlocked + checkSetLockStatus LockStatusUnlocked + checkGet FeatureStatusDisabled 30 LockStatusUnlocked testGuestLinksInternal :: TestM () testGuestLinksInternal = do galley <- viewGalley testGuestLinks - (const $ getTeamFeatureFlagInternal @Public.GuestLinksConfig) - (const $ putTeamFeatureFlagInternal @Public.GuestLinksConfig galley) - (Util.setLockStatusInternal @Public.GuestLinksConfig galley) + (const $ getTeamFeatureFlagInternal @GuestLinksConfig) + (const $ putTeamFeatureFlagInternal @GuestLinksConfig galley) + (Util.setLockStatusInternal @GuestLinksConfig galley) testGuestLinksPublic :: TestM () testGuestLinksPublic = do galley <- viewGalley testGuestLinks - (getTeamFeatureFlagWithGalley @Public.GuestLinksConfig galley) - (putTeamFeatureFlagWithGalley @Public.GuestLinksConfig galley) - (Util.setLockStatusInternal @Public.GuestLinksConfig galley) + (getTeamFeatureFlagWithGalley @GuestLinksConfig galley) + (putTeamFeatureFlagWithGalley @GuestLinksConfig galley) + (Util.setLockStatusInternal @GuestLinksConfig galley) testGuestLinks :: (UserId -> TeamId -> TestM ResponseLBS) -> - (UserId -> TeamId -> Public.WithStatusNoLock Public.GuestLinksConfig -> TestM ResponseLBS) -> - (TeamId -> Public.LockStatus -> TestM ResponseLBS) -> + (UserId -> TeamId -> WithStatusNoLock GuestLinksConfig -> TestM ResponseLBS) -> + (TeamId -> LockStatus -> TestM ResponseLBS) -> TestM () testGuestLinks getStatus putStatus setLockStatusInternal = do (owner, tid, []) <- createBindingTeamWithNMembers 0 - let checkGet :: HasCallStack => Public.FeatureStatus -> Public.LockStatus -> TestM () + let checkGet :: HasCallStack => FeatureStatus -> LockStatus -> TestM () checkGet status lock = getStatus owner tid !!! do statusCode === const 200 - responseJsonEither === const (Right (Public.withStatus status lock Public.GuestLinksConfig Public.FeatureTTLUnlimited)) + responseJsonEither === const (Right (withStatus status lock GuestLinksConfig FeatureTTLUnlimited)) - checkSet :: HasCallStack => Public.FeatureStatus -> Int -> TestM () + checkSet :: HasCallStack => FeatureStatus -> Int -> TestM () checkSet status expectedStatusCode = - putStatus owner tid (Public.WithStatusNoLock status Public.GuestLinksConfig Public.FeatureTTLUnlimited) !!! statusCode === const expectedStatusCode + putStatus owner tid (WithStatusNoLock status GuestLinksConfig FeatureTTLUnlimited) !!! statusCode === const expectedStatusCode - checkSetLockStatusInternal :: HasCallStack => Public.LockStatus -> TestM () + checkSetLockStatusInternal :: HasCallStack => LockStatus -> TestM () checkSetLockStatusInternal lockStatus = setLockStatusInternal tid lockStatus !!! statusCode === const 200 - checkGet Public.FeatureStatusEnabled Public.LockStatusUnlocked - checkSet Public.FeatureStatusDisabled 200 - checkGet Public.FeatureStatusDisabled Public.LockStatusUnlocked - checkSet Public.FeatureStatusEnabled 200 - checkGet Public.FeatureStatusEnabled Public.LockStatusUnlocked - checkSet Public.FeatureStatusDisabled 200 - checkGet Public.FeatureStatusDisabled Public.LockStatusUnlocked + checkGet FeatureStatusEnabled LockStatusUnlocked + checkSet FeatureStatusDisabled 200 + checkGet FeatureStatusDisabled LockStatusUnlocked + checkSet FeatureStatusEnabled 200 + checkGet FeatureStatusEnabled LockStatusUnlocked + checkSet FeatureStatusDisabled 200 + checkGet FeatureStatusDisabled LockStatusUnlocked -- when locks status is locked the team default feature status should be returned -- and the team feature status can not be changed - checkSetLockStatusInternal Public.LockStatusLocked - checkGet Public.FeatureStatusEnabled Public.LockStatusLocked - checkSet Public.FeatureStatusDisabled 409 + checkSetLockStatusInternal LockStatusLocked + checkGet FeatureStatusEnabled LockStatusLocked + checkSet FeatureStatusDisabled 409 -- when lock status is unlocked again the previously set feature status is restored - checkSetLockStatusInternal Public.LockStatusUnlocked - checkGet Public.FeatureStatusDisabled Public.LockStatusUnlocked + checkSetLockStatusInternal LockStatusUnlocked + checkGet FeatureStatusDisabled LockStatusUnlocked -- | Call 'GET /teams/:tid/features' and 'GET /feature-configs', and check if all -- features are there. testAllFeatures :: TestM () testAllFeatures = do - defLockStatus :: Public.LockStatus <- + defLockStatus :: LockStatus <- view ( tsGConf . optSettings . setFeatureFlags . flagSelfDeletingMessages . unDefaults - . to Public.wsLockStatus + . to wsLockStatus ) (_owner, tid, member : _) <- createBindingTeamWithNMembers 1 @@ -983,12 +1008,12 @@ testAllFeatures = do statusCode === const 200 responseJsonMaybe === const (Just (expected FeatureStatusEnabled defLockStatus {- determined by default in galley -})) - -- This block catches potential errors in the logic that reverts to default if there is a disinction made between + -- This block catches potential errors in the logic that reverts to default if there is a distinction made between -- 1. there is no row for a team_id in galley.team_features -- 2. there is a row for team_id in galley.team_features but the feature has a no entry (null value) galley <- 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) + putTeamFeatureFlagInternal @GuestLinksConfig galley tid (WithStatusNoLock FeatureStatusEnabled GuestLinksConfig FeatureTTLUnlimited) !!! statusCode === const 200 getAllTeamFeatures member tid !!! do @@ -1005,22 +1030,24 @@ testAllFeatures = do responseJsonMaybe === const (Just (expected FeatureStatusEnabled defLockStatus {- determined by 'getAfcConferenceCallingDefNew' in brig -})) where expected confCalling lockStateSelfDeleting = - Public.AllFeatureConfigs - { Public.afcLegalholdStatus = Public.withStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.LegalholdConfig Public.FeatureTTLUnlimited, - Public.afcSSOStatus = Public.withStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.SSOConfig Public.FeatureTTLUnlimited, - Public.afcTeamSearchVisibilityAvailable = Public.withStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.SearchVisibilityAvailableConfig Public.FeatureTTLUnlimited, - Public.afcValidateSAMLEmails = Public.withStatus FeatureStatusEnabled Public.LockStatusUnlocked Public.ValidateSAMLEmailsConfig Public.FeatureTTLUnlimited, - Public.afcDigitalSignatures = Public.withStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.DigitalSignaturesConfig Public.FeatureTTLUnlimited, - Public.afcAppLock = Public.withStatus FeatureStatusEnabled Public.LockStatusUnlocked (Public.AppLockConfig (Public.EnforceAppLock False) (60 :: Int32)) Public.FeatureTTLUnlimited, - Public.afcFileSharing = Public.withStatus FeatureStatusEnabled Public.LockStatusUnlocked Public.FileSharingConfig Public.FeatureTTLUnlimited, - Public.afcClassifiedDomains = Public.withStatus FeatureStatusEnabled Public.LockStatusUnlocked (Public.ClassifiedDomainsConfig [Domain "example.com"]) Public.FeatureTTLUnlimited, - Public.afcConferenceCalling = Public.withStatus confCalling Public.LockStatusUnlocked Public.ConferenceCallingConfig Public.FeatureTTLUnlimited, - Public.afcSelfDeletingMessages = Public.withStatus FeatureStatusEnabled lockStateSelfDeleting (Public.SelfDeletingMessagesConfig 0) Public.FeatureTTLUnlimited, - Public.afcGuestLink = Public.withStatus FeatureStatusEnabled Public.LockStatusUnlocked Public.GuestLinksConfig Public.FeatureTTLUnlimited, - Public.afcSndFactorPasswordChallenge = Public.withStatus FeatureStatusDisabled Public.LockStatusLocked Public.SndFactorPasswordChallengeConfig Public.FeatureTTLUnlimited, - Public.afcMLS = Public.withStatus FeatureStatusDisabled Public.LockStatusUnlocked (Public.MLSConfig [] ProtocolProteusTag [MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519] MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519) Public.FeatureTTLUnlimited, - Public.afcSearchVisibilityInboundConfig = Public.withStatus FeatureStatusDisabled Public.LockStatusUnlocked Public.SearchVisibilityInboundConfig Public.FeatureTTLUnlimited, - Public.afcExposeInvitationURLsToTeamAdmin = Public.withStatus FeatureStatusDisabled Public.LockStatusLocked Public.ExposeInvitationURLsToTeamAdminConfig Public.FeatureTTLUnlimited + AllFeatureConfigs + { afcLegalholdStatus = withStatus FeatureStatusDisabled LockStatusUnlocked LegalholdConfig FeatureTTLUnlimited, + afcSSOStatus = withStatus FeatureStatusDisabled LockStatusUnlocked SSOConfig FeatureTTLUnlimited, + afcTeamSearchVisibilityAvailable = withStatus FeatureStatusDisabled LockStatusUnlocked SearchVisibilityAvailableConfig FeatureTTLUnlimited, + afcValidateSAMLEmails = withStatus FeatureStatusEnabled LockStatusUnlocked ValidateSAMLEmailsConfig FeatureTTLUnlimited, + afcDigitalSignatures = withStatus FeatureStatusDisabled LockStatusUnlocked DigitalSignaturesConfig FeatureTTLUnlimited, + afcAppLock = withStatus FeatureStatusEnabled LockStatusUnlocked (AppLockConfig (EnforceAppLock False) (60 :: Int32)) FeatureTTLUnlimited, + afcFileSharing = withStatus FeatureStatusEnabled LockStatusUnlocked FileSharingConfig FeatureTTLUnlimited, + afcClassifiedDomains = withStatus FeatureStatusEnabled LockStatusUnlocked (ClassifiedDomainsConfig [Domain "example.com"]) FeatureTTLUnlimited, + afcConferenceCalling = withStatus confCalling LockStatusUnlocked ConferenceCallingConfig FeatureTTLUnlimited, + afcSelfDeletingMessages = withStatus FeatureStatusEnabled lockStateSelfDeleting (SelfDeletingMessagesConfig 0) FeatureTTLUnlimited, + afcGuestLink = withStatus FeatureStatusEnabled LockStatusUnlocked GuestLinksConfig FeatureTTLUnlimited, + afcSndFactorPasswordChallenge = withStatus FeatureStatusDisabled LockStatusLocked SndFactorPasswordChallengeConfig FeatureTTLUnlimited, + afcMLS = withStatus FeatureStatusDisabled LockStatusUnlocked (MLSConfig [] ProtocolProteusTag [MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519] MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519) FeatureTTLUnlimited, + afcSearchVisibilityInboundConfig = withStatus FeatureStatusDisabled LockStatusUnlocked SearchVisibilityInboundConfig FeatureTTLUnlimited, + afcExposeInvitationURLsToTeamAdmin = withStatus FeatureStatusDisabled LockStatusLocked ExposeInvitationURLsToTeamAdminConfig FeatureTTLUnlimited, + afcOutlookCalIntegration = withStatus FeatureStatusDisabled LockStatusLocked OutlookCalIntegrationConfig FeatureTTLUnlimited, + afcMlsE2EId = withStatus FeatureStatusDisabled LockStatusUnlocked (MlsE2EIdConfig Nothing) FeatureTTLUnlimited } testFeatureConfigConsistency :: TestM () @@ -1049,17 +1076,17 @@ testSearchVisibilityInbound = do let defaultValue = FeatureStatusDisabled (_owner, tid, _) <- createBindingTeamWithNMembers 1 - let getFlagInternal :: HasCallStack => Public.FeatureStatus -> TestM () + let getFlagInternal :: HasCallStack => FeatureStatus -> TestM () getFlagInternal expected = - flip (assertFlagNoConfig @Public.SearchVisibilityInboundConfig) expected $ getTeamFeatureFlagInternal @Public.SearchVisibilityInboundConfig tid + flip (assertFlagNoConfig @SearchVisibilityInboundConfig) expected $ getTeamFeatureFlagInternal @SearchVisibilityInboundConfig tid - setFlagInternal :: Public.FeatureStatus -> TestM () + setFlagInternal :: FeatureStatus -> TestM () setFlagInternal statusValue = - void $ putTeamFeatureFlagInternal @Public.SearchVisibilityInboundConfig expect2xx tid (Public.WithStatusNoLock statusValue Public.SearchVisibilityInboundConfig Public.FeatureTTLUnlimited) + void $ putTeamFeatureFlagInternal @SearchVisibilityInboundConfig expect2xx tid (WithStatusNoLock statusValue SearchVisibilityInboundConfig FeatureTTLUnlimited) let otherValue = case defaultValue of - Public.FeatureStatusDisabled -> Public.FeatureStatusEnabled - Public.FeatureStatusEnabled -> Public.FeatureStatusDisabled + FeatureStatusDisabled -> FeatureStatusEnabled + FeatureStatusEnabled -> FeatureStatusDisabled -- Initial value should be the default value getFlagInternal defaultValue @@ -1071,27 +1098,114 @@ testFeatureNoConfigMultiSearchVisibilityInbound = do (_owner1, team1, _) <- createBindingTeamWithNMembers 0 (_owner2, team2, _) <- createBindingTeamWithNMembers 0 - let setFlagInternal :: TeamId -> Public.FeatureStatus -> TestM () + let setFlagInternal :: TeamId -> FeatureStatus -> TestM () setFlagInternal tid statusValue = - void $ putTeamFeatureFlagInternal @Public.SearchVisibilityInboundConfig expect2xx tid (Public.WithStatusNoLock statusValue Public.SearchVisibilityInboundConfig Public.FeatureTTLUnlimited) + void $ putTeamFeatureFlagInternal @SearchVisibilityInboundConfig expect2xx tid (WithStatusNoLock statusValue SearchVisibilityInboundConfig FeatureTTLUnlimited) - setFlagInternal team2 Public.FeatureStatusEnabled + setFlagInternal team2 FeatureStatusEnabled r <- - getFeatureStatusMulti @Public.SearchVisibilityInboundConfig (Multi.TeamFeatureNoConfigMultiRequest [team1, team2]) + getFeatureStatusMulti @SearchVisibilityInboundConfig (Multi.TeamFeatureNoConfigMultiRequest [team1, team2]) + WithStatus cfg -> + TestM () +testNonTrivialConfigNoTTL defaultCfg = do + (owner, tid, member : _) <- createBindingTeamWithNMembers 1 + nonMember <- randomUser + + galley <- viewGalley + cannon <- view tsCannon + + let getForTeam :: HasCallStack => WithStatusNoLock cfg -> TestM () + getForTeam expected = + flip assertFlagWithConfig expected $ getTeamFeatureFlag @cfg member tid + + getForTeamInternal :: HasCallStack => WithStatusNoLock cfg -> TestM () + getForTeamInternal expected = + flip assertFlagWithConfig expected $ getTeamFeatureFlagInternal @cfg tid + + getForUser :: HasCallStack => WithStatusNoLock cfg -> TestM () + getForUser expected = do + result <- Util.getFeatureConfig @cfg member + liftIO $ wsStatus result @?= wssStatus expected + liftIO $ wsConfig result @?= wssConfig expected + + getViaEndpoints :: HasCallStack => WithStatusNoLock cfg -> TestM () + getViaEndpoints expected = do + getForTeam expected + getForTeamInternal expected + getForUser expected + + setForTeam :: HasCallStack => WithStatusNoLock cfg -> TestM () + setForTeam wsnl = + putTeamFeatureFlagWithGalley @cfg galley owner tid wsnl + !!! statusCode + === const 200 + + setForTeamInternal :: HasCallStack => WithStatusNoLock cfg -> TestM () + setForTeamInternal wsnl = + void $ putTeamFeatureFlagInternal @cfg expect2xx tid wsnl + setLockStatus :: LockStatus -> TestM () + setLockStatus lockStatus = + Util.setLockStatusInternal @cfg galley tid lockStatus + !!! statusCode + === const 200 + + assertFlagForbidden $ getTeamFeatureFlag @cfg nonMember tid + + getViaEndpoints (forgetLock defaultCfg) + + -- unlock feature + setLockStatus LockStatusUnlocked + + config2 <- liftIO $ generate arbitrary <&> (forgetLock . setTTL FeatureTTLUnlimited) + config3 <- liftIO $ generate arbitrary <&> (forgetLock . setTTL FeatureTTLUnlimited) + + WS.bracketR cannon member $ \ws -> do + setForTeam config2 + void . liftIO $ + WS.assertMatch (5 # Second) ws $ + wsAssertFeatureConfigUpdate @cfg config2 LockStatusUnlocked + getViaEndpoints config2 + + WS.bracketR cannon member $ \ws -> do + setForTeamInternal config3 + void . liftIO $ + WS.assertMatch (5 # Second) ws $ + wsAssertFeatureConfigUpdate @cfg config3 LockStatusUnlocked + getViaEndpoints config3 + + -- lock the feature + setLockStatus LockStatusLocked + -- feature status should now be the default again + getViaEndpoints (forgetLock defaultCfg) + -- unlock feature + setLockStatus LockStatusUnlocked + -- feature status should be the previously set value + getViaEndpoints config3 testMLS :: TestM () testMLS = do @@ -1100,49 +1214,49 @@ testMLS = do galley <- viewGalley cannon <- view tsCannon - let getForTeam :: HasCallStack => Public.WithStatusNoLock MLSConfig -> TestM () + let getForTeam :: HasCallStack => WithStatusNoLock MLSConfig -> TestM () getForTeam expected = flip assertFlagWithConfig expected $ getTeamFeatureFlag @MLSConfig member tid - getForTeamInternal :: HasCallStack => Public.WithStatusNoLock MLSConfig -> TestM () + getForTeamInternal :: HasCallStack => WithStatusNoLock MLSConfig -> TestM () getForTeamInternal expected = - flip assertFlagWithConfig expected $ getTeamFeatureFlagInternal @Public.MLSConfig tid + flip assertFlagWithConfig expected $ getTeamFeatureFlagInternal @MLSConfig tid - getForUser :: HasCallStack => Public.WithStatusNoLock MLSConfig -> TestM () + getForUser :: HasCallStack => WithStatusNoLock MLSConfig -> TestM () getForUser expected = do result <- Util.getFeatureConfig @MLSConfig member - liftIO $ Public.wsStatus result @?= Public.wssStatus expected - liftIO $ Public.wsConfig result @?= Public.wssConfig expected + liftIO $ wsStatus result @?= wssStatus expected + liftIO $ wsConfig result @?= wssConfig expected - getViaEndpoints :: HasCallStack => Public.WithStatusNoLock MLSConfig -> TestM () + getViaEndpoints :: HasCallStack => WithStatusNoLock MLSConfig -> TestM () getViaEndpoints expected = do getForTeam expected getForTeamInternal expected getForUser expected - setForTeam :: HasCallStack => Public.WithStatusNoLock MLSConfig -> TestM () + setForTeam :: HasCallStack => WithStatusNoLock MLSConfig -> TestM () setForTeam wsnl = putTeamFeatureFlagWithGalley @MLSConfig galley owner tid wsnl !!! statusCode === const 200 - setForTeamInternal :: HasCallStack => Public.WithStatusNoLock MLSConfig -> TestM () + setForTeamInternal :: HasCallStack => WithStatusNoLock MLSConfig -> TestM () setForTeamInternal wsnl = - void $ putTeamFeatureFlagInternal @Public.MLSConfig expect2xx tid wsnl + void $ putTeamFeatureFlagInternal @MLSConfig expect2xx tid wsnl let cipherSuite = MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 let defaultConfig = - Public.WithStatusNoLock + WithStatusNoLock FeatureStatusDisabled (MLSConfig [] ProtocolProteusTag [cipherSuite] cipherSuite) FeatureTTLUnlimited let config2 = - Public.WithStatusNoLock + WithStatusNoLock FeatureStatusEnabled (MLSConfig [member] ProtocolMLSTag [] cipherSuite) FeatureTTLUnlimited let config3 = - Public.WithStatusNoLock + WithStatusNoLock FeatureStatusDisabled (MLSConfig [] ProtocolMLSTag [cipherSuite] cipherSuite) FeatureTTLUnlimited @@ -1167,31 +1281,31 @@ testExposeInvitationURLsToTeamAdminTeamIdInAllowList :: TestM () testExposeInvitationURLsToTeamAdminTeamIdInAllowList = do owner <- randomUser tid <- createBindingTeamInternal "foo" owner - assertQueue "create team" tActivate + assertTeamActivate "create team" tid void $ withSettingsOverrides (\opts -> opts & optSettings . setExposeInvitationURLsTeamAllowlist ?~ [tid]) $ do g <- viewGalley - assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusDisabled Public.LockStatusUnlocked - let enabled = Public.WithStatusNoLock Public.FeatureStatusEnabled ExposeInvitationURLsToTeamAdminConfig Public.FeatureTTLUnlimited + assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusDisabled LockStatusUnlocked + let enabled = WithStatusNoLock FeatureStatusEnabled ExposeInvitationURLsToTeamAdminConfig FeatureTTLUnlimited void $ putTeamFeatureFlagWithGalley @ExposeInvitationURLsToTeamAdminConfig g owner tid enabled !!! do const 200 === statusCode - assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusEnabled Public.LockStatusUnlocked + assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusEnabled LockStatusUnlocked testExposeInvitationURLsToTeamAdminEmptyAllowList :: TestM () testExposeInvitationURLsToTeamAdminEmptyAllowList = do owner <- randomUser tid <- createBindingTeamInternal "foo" owner - assertQueue "create team" tActivate + assertTeamActivate "create team" tid void $ withSettingsOverrides (\opts -> opts & optSettings . setExposeInvitationURLsTeamAllowlist .~ Nothing) $ do g <- viewGalley - assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusDisabled Public.LockStatusLocked - let enabled = Public.WithStatusNoLock Public.FeatureStatusEnabled ExposeInvitationURLsToTeamAdminConfig Public.FeatureTTLUnlimited + assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusDisabled LockStatusLocked + let enabled = WithStatusNoLock FeatureStatusEnabled ExposeInvitationURLsToTeamAdminConfig FeatureTTLUnlimited void $ putTeamFeatureFlagWithGalley @ExposeInvitationURLsToTeamAdminConfig g owner tid enabled !!! do const 409 === statusCode - assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusDisabled Public.LockStatusLocked + assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusDisabled LockStatusLocked -- | Ensure that the server config takes precedence over a saved team config. -- @@ -1203,32 +1317,32 @@ testExposeInvitationURLsToTeamAdminServerConfigTakesPrecedence :: TestM () testExposeInvitationURLsToTeamAdminServerConfigTakesPrecedence = do owner <- randomUser tid <- createBindingTeamInternal "foo" owner - assertQueue "create team" tActivate + assertTeamActivate "create team" tid void $ withSettingsOverrides (\opts -> opts & optSettings . setExposeInvitationURLsTeamAllowlist ?~ [tid]) $ do g <- viewGalley - assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusDisabled Public.LockStatusUnlocked - let enabled = Public.WithStatusNoLock Public.FeatureStatusEnabled ExposeInvitationURLsToTeamAdminConfig Public.FeatureTTLUnlimited + assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusDisabled LockStatusUnlocked + let enabled = WithStatusNoLock FeatureStatusEnabled ExposeInvitationURLsToTeamAdminConfig FeatureTTLUnlimited void $ putTeamFeatureFlagWithGalley @ExposeInvitationURLsToTeamAdminConfig g owner tid enabled !!! do const 200 === statusCode - assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusEnabled Public.LockStatusUnlocked + assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusEnabled LockStatusUnlocked void $ withSettingsOverrides (\opts -> opts & optSettings . setExposeInvitationURLsTeamAllowlist .~ Nothing) $ do g <- viewGalley - assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusDisabled Public.LockStatusLocked - let enabled = Public.WithStatusNoLock Public.FeatureStatusEnabled ExposeInvitationURLsToTeamAdminConfig Public.FeatureTTLUnlimited + assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusDisabled LockStatusLocked + let enabled = WithStatusNoLock FeatureStatusEnabled ExposeInvitationURLsToTeamAdminConfig FeatureTTLUnlimited void $ putTeamFeatureFlagWithGalley @ExposeInvitationURLsToTeamAdminConfig g owner tid enabled !!! do const 409 === statusCode - assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusDisabled Public.LockStatusLocked + assertExposeInvitationURLsToTeamAdminConfigStatus owner tid FeatureStatusDisabled LockStatusLocked assertExposeInvitationURLsToTeamAdminConfigStatus :: UserId -> TeamId -> FeatureStatus -> LockStatus -> TestM () assertExposeInvitationURLsToTeamAdminConfigStatus owner tid fStatus lStatus = do g <- viewGalley Util.getTeamFeatureFlagWithGalley @ExposeInvitationURLsToTeamAdminConfig g owner tid !!! do const 200 === statusCode - const (Right (Public.withStatus fStatus lStatus Public.ExposeInvitationURLsToTeamAdminConfig Public.FeatureTTLUnlimited)) === responseJsonEither + const (Right (withStatus fStatus lStatus ExposeInvitationURLsToTeamAdminConfig FeatureTTLUnlimited)) === responseJsonEither assertFlagForbidden :: HasCallStack => TestM ResponseLBS -> TestM () assertFlagForbidden res = do @@ -1240,18 +1354,16 @@ assertFlagNoConfig :: forall cfg. ( HasCallStack, Typeable cfg, - Public.IsFeatureConfig cfg, - Public.FeatureTrivialConfig cfg, - FromJSON (Public.WithStatusNoLock cfg) + FromJSON (WithStatusNoLock cfg) ) => TestM ResponseLBS -> - Public.FeatureStatus -> + FeatureStatus -> TestM () assertFlagNoConfig res expected = do res !!! do statusCode === const 200 - ( fmap Public.wssStatus - . responseJsonEither @(Public.WithStatusNoLock cfg) + ( fmap wssStatus + . responseJsonEither @(WithStatusNoLock cfg) ) === const (Right expected) @@ -1259,21 +1371,20 @@ assertFlagNoConfigWithLockStatus :: forall cfg. ( HasCallStack, Typeable cfg, - Public.IsFeatureConfig cfg, - Public.FeatureTrivialConfig cfg, - FromJSON (Public.WithStatus cfg), + FeatureTrivialConfig cfg, + FromJSON (WithStatus cfg), Eq cfg, Show cfg ) => TestM ResponseLBS -> - Public.FeatureStatus -> - Public.LockStatus -> + FeatureStatus -> + LockStatus -> TestM () assertFlagNoConfigWithLockStatus res expectedStatus expectedLockStatus = do res !!! do statusCode === const 200 - responseJsonEither @(Public.WithStatus cfg) - === const (Right (Public.withStatus expectedStatus expectedLockStatus (Public.trivialConfig @cfg) Public.FeatureTTLUnlimited)) + responseJsonEither @(WithStatus cfg) + === const (Right (withStatus expectedStatus expectedLockStatus (trivialConfig @cfg) FeatureTTLUnlimited)) assertFlagWithConfig :: forall cfg m. @@ -1282,72 +1393,68 @@ assertFlagWithConfig :: ToSchema cfg, Show cfg, Typeable cfg, - Public.IsFeatureConfig cfg, + IsFeatureConfig cfg, MonadIO m, MonadCatch m ) => m ResponseLBS -> - Public.WithStatusNoLock cfg -> + WithStatusNoLock cfg -> m () assertFlagWithConfig response expected = do r <- response - let rJson = responseJsonEither @(Public.WithStatusNoLock cfg) r + let rJson = responseJsonEither @(WithStatusNoLock cfg) r pure r !!! statusCode === const 200 liftIO $ do - fmap Public.wssStatus rJson @?= (Right . Public.wssStatus $ expected) - fmap Public.wssConfig rJson @?= (Right . Public.wssConfig $ expected) + fmap wssStatus rJson @?= (Right . wssStatus $ expected) + fmap wssConfig rJson @?= (Right . wssConfig $ expected) wsAssertFeatureTrivialConfigUpdate :: forall cfg. - ( Public.IsFeatureConfig cfg, - KnownSymbol (Public.FeatureSymbol cfg), - ToJSON (Public.WithStatusNoLock cfg), - Public.FeatureTrivialConfig cfg, + ( IsFeatureConfig cfg, + KnownSymbol (FeatureSymbol cfg), + FeatureTrivialConfig cfg, ToSchema cfg ) => - Public.FeatureStatus -> - Public.FeatureTTL -> + FeatureStatus -> + FeatureTTL -> Notification -> IO () wsAssertFeatureTrivialConfigUpdate status ttl notification = do let e :: FeatureConfig.Event = List1.head (WS.unpackPayload notification) FeatureConfig._eventType e @?= FeatureConfig.Update - FeatureConfig._eventFeatureName e @?= Public.featureName @cfg + FeatureConfig._eventFeatureName e @?= featureName @cfg FeatureConfig._eventData e @?= Aeson.toJSON - (Public.withStatus status (Public.wsLockStatus (Public.defFeatureStatus @cfg)) (Public.trivialConfig @cfg) ttl) + (withStatus status (wsLockStatus (defFeatureStatus @cfg)) (trivialConfig @cfg) ttl) wsAssertFeatureConfigWithLockStatusUpdate :: forall cfg. - ( Public.IsFeatureConfig cfg, + ( IsFeatureConfig cfg, ToSchema cfg, - KnownSymbol (Public.FeatureSymbol cfg), - ToJSON (Public.WithStatusNoLock cfg), - Public.FeatureTrivialConfig cfg + KnownSymbol (FeatureSymbol cfg), + FeatureTrivialConfig cfg ) => - Public.FeatureStatus -> - Public.LockStatus -> + FeatureStatus -> + LockStatus -> Notification -> IO () wsAssertFeatureConfigWithLockStatusUpdate status lockStatus notification = do let e :: FeatureConfig.Event = List1.head (WS.unpackPayload notification) FeatureConfig._eventType e @?= FeatureConfig.Update - FeatureConfig._eventFeatureName e @?= (Public.featureName @cfg) - FeatureConfig._eventData e @?= Aeson.toJSON (Public.withStatus status lockStatus (Public.trivialConfig @cfg) FeatureTTLUnlimited) + FeatureConfig._eventFeatureName e @?= (featureName @cfg) + FeatureConfig._eventData e @?= Aeson.toJSON (withStatus status lockStatus (trivialConfig @cfg) FeatureTTLUnlimited) wsAssertFeatureConfigUpdate :: forall cfg. - ( Public.IsFeatureConfig cfg, - KnownSymbol (Public.FeatureSymbol cfg), - ToJSON (Public.WithStatus cfg), - ToSchema cfg + ( KnownSymbol (FeatureSymbol cfg), + ToJSON (WithStatus cfg) ) => - Public.WithStatusNoLock cfg -> - Public.LockStatus -> + WithStatusNoLock cfg -> + LockStatus -> Notification -> IO () wsAssertFeatureConfigUpdate config lockStatus notification = do let e :: FeatureConfig.Event = List1.head (WS.unpackPayload notification) FeatureConfig._eventType e @?= FeatureConfig.Update - FeatureConfig._eventFeatureName e @?= Public.featureName @cfg - FeatureConfig._eventData e @?= Aeson.toJSON (Public.withLockStatus lockStatus config) + FeatureConfig._eventFeatureName e @?= featureName @cfg + FeatureConfig._eventData e @?= Aeson.toJSON (withLockStatus lockStatus config) diff --git a/services/galley/test/integration/API/Teams/LegalHold.hs b/services/galley/test/integration/API/Teams/LegalHold.hs index b3bee3fe84..1f568245f3 100644 --- a/services/galley/test/integration/API/Teams/LegalHold.hs +++ b/services/galley/test/integration/API/Teams/LegalHold.hs @@ -1,5 +1,7 @@ {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} {-# OPTIONS_GHC -Wno-orphans #-} +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- @@ -23,8 +25,7 @@ module API.Teams.LegalHold ) where -import API.SQS -import qualified API.SQS as SQS +import API.Teams.LegalHold.Util import API.Util import Bilge hiding (accept, head, timeout, trace) import Bilge.Assert @@ -32,31 +33,17 @@ import Brig.Types.Intra (UserSet (..)) import Brig.Types.Test.Arbitrary () import qualified Brig.Types.User.Event as Ev import qualified Cassandra.Exec as Cql -import qualified Control.Concurrent.Async as Async import Control.Concurrent.Chan -import Control.Concurrent.Timeout hiding (threadDelay) -import Control.Exception (asyncExceptionFromException) import Control.Lens hiding ((#)) -import Control.Monad.Catch -import Control.Retry (RetryPolicy, RetryStatus, exponentialBackoff, limitRetries, retrying) -import qualified Data.Aeson as Aeson -import Data.Aeson.Types (FromJSON, withObject, (.:)) -import qualified Data.ByteString as BS -import qualified Data.ByteString.Char8 as BS -import Data.ByteString.Conversion import Data.Id import Data.LegalHold import Data.List.NonEmpty (NonEmpty (..)) -import qualified Data.List.NonEmpty as NonEmpty import qualified Data.List1 as List1 import qualified Data.Map.Strict as Map -import Data.Misc (PlainTextPassword) import Data.PEM import Data.Qualified (Qualified (..)) import Data.Range import qualified Data.Set as Set -import Data.String.Conversions (LBS, cs) -import Data.Text.Encoding (encodeUtf8) import qualified Data.Time.Clock as Time import Data.Timeout import Galley.Cassandra.Client (lookupClients) @@ -67,13 +54,10 @@ import Galley.Options (optSettings, setFeatureFlags) import qualified Galley.Types.Clients as Clients import Galley.Types.Teams import Imports -import Network.HTTP.Types.Status (status200, status400, status404) +import Network.HTTP.Types.Status (status200, status404) import Network.Wai as Wai import qualified Network.Wai.Handler.Warp as Warp -import qualified Network.Wai.Handler.Warp.Internal as Warp -import qualified Network.Wai.Handler.WarpTLS as Warp import qualified Network.Wai.Utilities.Error as Error -import qualified Network.Wai.Utilities.Response as Wai import System.IO (hPutStrLn) import Test.QuickCheck.Instances () import Test.Tasty @@ -84,7 +68,6 @@ import TestSetup import Wire.API.Connection (UserConnection) import qualified Wire.API.Connection as Conn import Wire.API.Conversation.Role (roleNameWireAdmin, roleNameWireMember) -import Wire.API.Internal.Notification (ntfPayload) import qualified Wire.API.Message as Msg import Wire.API.Provider.Service import Wire.API.Routes.Internal.Brig.Connection @@ -95,7 +78,6 @@ import Wire.API.Team.Member import qualified Wire.API.Team.Member as Team import Wire.API.Team.Permission import Wire.API.Team.Role -import Wire.API.User (UserProfile (..)) import Wire.API.User.Client import qualified Wire.API.User.Client as Client @@ -123,21 +105,21 @@ testsPublic s = testGroup "Teams LegalHold API (with flag whitelist-teams-and-implicit-consent)" [ -- device handling (CRUD) - test s "POST /teams/{tid}/legalhold/{uid}" (onlyIfLhWhitelisted testRequestLegalHoldDevice), - test s "PUT /teams/{tid}/legalhold/approve" (onlyIfLhWhitelisted testApproveLegalHoldDevice), + testOnlyIfLhWhitelisted s "POST /teams/{tid}/legalhold/{uid}" testRequestLegalHoldDevice, + testOnlyIfLhWhitelisted s "PUT /teams/{tid}/legalhold/approve" testApproveLegalHoldDevice, test s "(user denies approval: nothing needs to be done in backend)" (pure ()), - test s "GET /teams/{tid}/legalhold/{uid}" (onlyIfLhWhitelisted testGetLegalHoldDeviceStatus), - test s "DELETE /teams/{tid}/legalhold/{uid}" (onlyIfLhWhitelisted testDisableLegalHoldForUser), + testOnlyIfLhWhitelisted s "GET /teams/{tid}/legalhold/{uid}" testGetLegalHoldDeviceStatus, + testOnlyIfLhWhitelisted s "DELETE /teams/{tid}/legalhold/{uid}" testDisableLegalHoldForUser, -- legal hold settings - test s "POST /teams/{tid}/legalhold/settings" (onlyIfLhWhitelisted testCreateLegalHoldTeamSettings), - test s "GET /teams/{tid}/legalhold/settings" (onlyIfLhWhitelisted testGetLegalHoldTeamSettings), - test s "Not implemented: DELETE /teams/{tid}/legalhold/settings" (onlyIfLhWhitelisted testRemoveLegalHoldFromTeam), - test s "GET [/i]?/teams/{tid}/legalhold" (onlyIfLhWhitelisted testEnablePerTeam), + testOnlyIfLhWhitelisted s "POST /teams/{tid}/legalhold/settings" testCreateLegalHoldTeamSettings, + testOnlyIfLhWhitelisted s "GET /teams/{tid}/legalhold/settings" testGetLegalHoldTeamSettings, + testOnlyIfLhWhitelisted s "Not implemented: DELETE /teams/{tid}/legalhold/settings" testRemoveLegalHoldFromTeam, + testOnlyIfLhWhitelisted s "GET [/i]?/teams/{tid}/legalhold" testEnablePerTeam, -- behavior of existing end-points - test s "POST /clients" (onlyIfLhWhitelisted testCannotCreateLegalHoldDeviceOldAPI), - test s "GET /teams/{tid}/members" (onlyIfLhWhitelisted testGetTeamMembersIncludesLHStatus), - test s "POST /register - can add team members above fanout limit when whitelisting is enabled" (onlyIfLhWhitelisted testAddTeamUserTooLargeWithLegalholdWhitelisted), - test s "GET legalhold status in user profile" (onlyIfLhWhitelisted testGetLegalholdStatus), + testOnlyIfLhWhitelisted s "POST /clients" testCannotCreateLegalHoldDeviceOldAPI, + testOnlyIfLhWhitelisted s "GET /teams/{tid}/members" testGetTeamMembersIncludesLHStatus, + testOnlyIfLhWhitelisted s "POST /register - can add team members above fanout limit when whitelisting is enabled" testAddTeamUserTooLargeWithLegalholdWhitelisted, + testOnlyIfLhWhitelisted s "GET legalhold status in user profile" testGetLegalholdStatus, {- TODO: conversations/{cnv}/otr/messages - possibly show the legal hold device (if missing) as a different device type (or show that on device level, depending on how client teams prefer) GET /team/{tid}/members - show legal hold status of all members @@ -158,9 +140,9 @@ testsPublic s = pure . test s name $ testNoConsentBlockOne2OneConv (snd connectFirst) (snd teamPeer) (snd approveLH) (snd testPendingConnection), testGroup "Legalhold is activated for user A in a group conversation" - [ test s "All admins are consenting: all non-consenters get removed from conversation" (onlyIfLhWhitelisted (testNoConsentRemoveFromGroupConv LegalholderIsAdmin)), - test s "Some admins are consenting: all non-consenters get removed from conversation" (onlyIfLhWhitelisted (testNoConsentRemoveFromGroupConv BothAreAdmins)), - test s "No admins are consenting: all LH activated/pending users get removed from conversation" (onlyIfLhWhitelisted (testNoConsentRemoveFromGroupConv PeerIsAdmin)) + [ testOnlyIfLhWhitelisted s "All admins are consenting: all non-consenters get removed from conversation" (testNoConsentRemoveFromGroupConv LegalholderIsAdmin), + testOnlyIfLhWhitelisted s "Some admins are consenting: all non-consenters get removed from conversation" (testNoConsentRemoveFromGroupConv BothAreAdmins), + testOnlyIfLhWhitelisted s "No admins are consenting: all LH activated/pending users get removed from conversation" (testNoConsentRemoveFromGroupConv PeerIsAdmin) ], testGroup "Users are invited to a group conversation." @@ -177,10 +159,10 @@ testsPublic s = ], testGroup "The group conversation contains legalhold activated users." - [ test s "If any user in the invite has not given consent then the invite fails" (onlyIfLhWhitelisted testNoConsentCannotBeInvited) + [ testOnlyIfLhWhitelisted s "If any user in the invite has not given consent then the invite fails" testNoConsentCannotBeInvited ] ], - test s "Cannot create conversation with both LH activated and non-consenting users" (onlyIfLhWhitelisted testCannotCreateGroupWithUsersInConflict), + testOnlyIfLhWhitelisted s "Cannot create conversation with both LH activated and non-consenting users" testCannotCreateGroupWithUsersInConflict, test s "bench hack" testBenchHack, test s "User cannot fetch prekeys of LH users if consent is missing" (testClaimKeys TCKConsentMissing), test s "User cannot fetch prekeys of LH users: if user has old client" (testClaimKeys TCKOldClient), @@ -193,7 +175,7 @@ testsInternal :: IO TestSetup -> TestTree testsInternal s = testGroup "Legalhold Internal API" - [test s "PUT, DELETE /i/legalhold/whitelisted-teams" (onlyIfLhWhitelisted testWhitelistingTeams)] + [testOnlyIfLhWhitelisted s "PUT, DELETE /i/legalhold/whitelisted-teams" testWhitelistingTeams] testWhitelistingTeams :: TestM () testWhitelistingTeams = do @@ -214,19 +196,17 @@ testWhitelistingTeams = do pure tid expectWhitelisted False tid - ensureQueueEmpty testRequestLegalHoldDevice :: TestM () testRequestLegalHoldDevice = withTeam $ \owner tid -> do member <- randomUser addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing - ensureQueueEmpty -- Can't request a device if team feature flag is disabled requestLegalHoldDevice owner member tid !!! testResponse 403 (Just "legalhold-not-enabled") cannon <- view tsCannon -- Assert that the appropriate LegalHold Request notification is sent to the user's -- clients - WS.bracketR2 cannon member member $ \(ws, ws') -> withDummyTestServiceForTeamNoService $ \_chan -> do + WS.bracketR2 cannon member member $ \(ws, ws') -> withDummyTestServiceForTeamNoService $ \lhPort _chan -> do do -- test device creation without consent requestLegalHoldDevice member member tid !!! testResponse 403 (Just "legalhold-not-enabled") @@ -247,7 +227,7 @@ testRequestLegalHoldDevice = withTeam $ \owner tid -> do userStatus putLHWhitelistTeam tid !!! const 200 === statusCode - newService <- newLegalHoldService + newService <- newLegalHoldService lhPort postSettings owner tid newService !!! testResponse 201 Nothing do @@ -289,12 +269,10 @@ testRequestLegalHoldDevice = withTeam $ \owner tid -> do assertNotification ws pluck -- all devices get notified. assertNotification ws' pluck - ensureQueueEmpty testApproveLegalHoldDevice :: TestM () testApproveLegalHoldDevice = do (owner, tid) <- createBindingTeam - ensureQueueEmpty member <- do usr <- randomUser addTeamMemberInternal tid usr (rolePermissions RoleMember) Nothing @@ -309,7 +287,6 @@ testApproveLegalHoldDevice = do pure usr stranger <- randomUser putLHWhitelistTeam tid !!! const 200 === statusCode - ensureQueueEmpty approveLegalHoldDevice (Just defPassword) owner member tid !!! testResponse 403 (Just "access-denied") cannon <- view tsCannon @@ -363,7 +340,6 @@ testGetLegalHoldDeviceStatus = do (owner, tid) <- createBindingTeam member <- randomUser addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing - ensureQueueEmpty forM_ [owner, member] $ \uid -> do status <- getUserStatusTyped uid tid liftIO $ @@ -373,7 +349,7 @@ testGetLegalHoldDeviceStatus = do status putLHWhitelistTeam tid !!! const 200 === statusCode - withDummyTestServiceForTeamNoService $ \_chan -> do + withDummyTestServiceForTeamNoService $ \lhPort _chan -> do do UserLegalHoldStatusResponse userStatus lastPrekey' clientId' <- getUserStatusTyped member tid liftIO $ @@ -383,7 +359,7 @@ testGetLegalHoldDeviceStatus = do assertEqual "client.id should be Nothing when LH is disabled" Nothing clientId' do - newService <- newLegalHoldService + newService <- newLegalHoldService lhPort postSettings owner tid newService !!! testResponse 201 Nothing requestLegalHoldDevice owner member tid !!! testResponse 201 Nothing assertZeroLegalHoldDevices member @@ -416,7 +392,6 @@ testDisableLegalHoldForUser :: TestM () testDisableLegalHoldForUser = withTeam $ \owner tid -> do member <- randomUser addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing - ensureQueueEmpty cannon <- view tsCannon putLHWhitelistTeam tid !!! const 200 === statusCode WS.bracketR2 cannon owner member $ \(ows, mws) -> withDummyTestServiceForTeam owner tid $ \chan -> do @@ -458,17 +433,17 @@ testCreateLegalHoldTeamSettings = withTeam $ \owner tid -> do putLHWhitelistTeam tid !!! const 200 === statusCode member <- randomUser addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing - ensureQueueEmpty - newService <- newLegalHoldService + -- Random port, hopefully nothing is runing here! + brokenService <- newLegalHoldService 4242 -- not allowed to create if team is not whitelisted - postSettings owner tid newService !!! testResponse 412 (Just "legalhold-unavailable") + postSettings owner tid brokenService !!! testResponse 412 (Just "legalhold-unavailable") putLHWhitelistTeam tid !!! const 200 === statusCode -- not allowed for users with corresp. permission bit missing - postSettings member tid newService !!! testResponse 403 (Just "operation-denied") + postSettings member tid brokenService !!! testResponse 403 (Just "operation-denied") -- rejected if service is not available - postSettings owner tid newService !!! testResponse 412 (Just "legalhold-unavailable") + postSettings owner tid brokenService !!! testResponse 412 (Just "legalhold-unavailable") -- checks /status of legal hold service (boolean argument says whether the service is -- behaving or not) let lhapp :: HasCallStack => IsWorking -> Chan Void -> Application @@ -482,11 +457,12 @@ testCreateLegalHoldTeamSettings = withTeam $ \owner tid -> do respondOk = responseLBS status200 mempty mempty respondBad :: Wai.Response respondBad = responseLBS status404 mempty mempty - lhtest :: HasCallStack => IsWorking -> Chan Void -> TestM () - lhtest NotWorking _ = do - postSettings owner tid newService !!! testResponse 412 (Just "legalhold-unavailable") - lhtest Working _ = do + lhtest :: HasCallStack => IsWorking -> Warp.Port -> Chan Void -> TestM () + lhtest NotWorking _ _ = do + postSettings owner tid brokenService !!! testResponse 412 (Just "legalhold-unavailable") + lhtest Working lhPort _ = do let Right [k] = pemParseBS "-----BEGIN PUBLIC KEY-----\n\n-----END PUBLIC KEY-----" + newService <- newLegalHoldService lhPort let badServiceBadKey = newService {newLegalHoldServiceKey = ServiceKeyPEM k} postSettings owner tid badServiceBadKey !!! testResponse 400 (Just "legalhold-invalid-key") postSettings owner tid newService !!! testResponse 201 Nothing @@ -517,11 +493,10 @@ testGetLegalHoldTeamSettings = do stranger <- randomUser member <- randomUser addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing - ensureQueueEmpty - newService <- newLegalHoldService let lhapp :: Chan () -> Application lhapp _ch _req res = res $ responseLBS status200 mempty mempty - withTestService lhapp $ \_ -> do + withTestService lhapp $ \lhPort _ -> do + newService <- newLegalHoldService lhPort -- returns 403 if user is not in team. getSettings stranger tid !!! testResponse 403 (Just "no-team-member") -- returns 200 with corresp. status if legalhold for team is disabled @@ -561,7 +536,6 @@ testRemoveLegalHoldFromTeam = do (owner, tid) <- createBindingTeam member <- randomUser addTeamMemberInternal tid member noPermissions Nothing - ensureQueueEmpty -- fails if LH for team is disabled deleteSettings (Just defPassword) owner tid !!! testResponse 403 (Just "legalhold-disable-unimplemented") @@ -569,7 +543,6 @@ testEnablePerTeam :: TestM () testEnablePerTeam = withTeam $ \owner tid -> do member <- randomUser addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing - ensureQueueEmpty do status :: Public.WithStatusNoLock Public.LegalholdConfig <- responseJsonUnsafe <$> (getEnabled tid do forM_ [2 .. (fanoutLimit + 5)] $ \_n -> do addUserToTeam' owner tid !!! do const 201 === statusCode - ensureQueueEmpty testCannotCreateLegalHoldDeviceOldAPI :: TestM () testCannotCreateLegalHoldDeviceOldAPI = do member <- randomUser - ensureQueueEmpty (owner, tid) <- createBindingTeam - ensureQueueEmpty -- user without team can't add LH device tryout member -- team member can't add LH device addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing - ensureQueueEmpty tryout member -- team owner can't add LH device tryout owner @@ -639,7 +608,6 @@ testGetTeamMembersIncludesLHStatus = do (owner, tid) <- createBindingTeam member <- randomUser addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing - ensureQueueEmpty let findMemberStatus :: [TeamMember] -> Maybe UserLegalHoldStatus findMemberStatus ms = @@ -655,11 +623,11 @@ testGetTeamMembersIncludesLHStatus = do (findMemberStatus members') check UserLegalHoldNoConsent "disabled when it is disabled for the team" - withDummyTestServiceForTeamNoService $ \_chan -> do + withDummyTestServiceForTeamNoService $ \lhPort _chan -> do check UserLegalHoldNoConsent "no_consent on new team members" putLHWhitelistTeam tid !!! const 200 === statusCode - newService <- newLegalHoldService + newService <- newLegalHoldService lhPort postSettings owner tid newService !!! testResponse 201 Nothing check UserLegalHoldDisabled "disabled on team members that have granted consent" @@ -674,7 +642,6 @@ testInWhitelist = do (owner, tid) <- createBindingTeam member <- randomUser addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing - ensureQueueEmpty cannon <- view tsCannon putLHWhitelistTeam tid !!! const 200 === statusCode @@ -746,11 +713,9 @@ testOldClientsBlockDeviceHandshake = do (legalholder, tid) <- createBindingTeam legalholder2 <- view Team.userId <$> addUserToTeam legalholder tid - ensureQueueEmpty (peer, tid2) <- -- has to be a team member, granting LH consent for personal users is not supported. createBindingTeam - ensureQueueEmpty let doEnableLH :: HasCallStack => UserId -> UserId -> TestM ClientId doEnableLH owner uid = do @@ -873,8 +838,6 @@ testNoConsentBlockOne2OneConv connectFirst teamPeer approveLH testPendingConnect res <- putConnection peer legalholder Conn.Accepted withDummyTestServiceForTeam legalholder tid $ \_chan -> do - ensureQueueEmpty - postConnection legalholder peer !!! const 201 === statusCode void $ putConnection peer legalholder Conn.Accepted addUserToTeam legalholder tid pure $ Qualified uid localDomain - ensureQueueEmpty putLHWhitelistTeam tid !!! const 200 === statusCode -- team without legalhold (peer :: UserId, teamPeer) <- createBindingTeam peer2 <- (^. Team.userId) <$> addUserToTeam peer teamPeer let qpeer2 = Qualified peer2 localDomain - ensureQueueEmpty do postConnection userWithConsent peer !!! const 201 === statusCode @@ -1081,7 +1040,6 @@ testNoConsentCannotBeInvited = do -- team that is legalhold whitelisted (legalholder :: UserId, tid) <- createBindingTeam userLHNotActivated <- (^. Team.userId) <$> addUserToTeam legalholder tid - ensureQueueEmpty putLHWhitelistTeam tid !!! const 200 === statusCode -- team without legalhold @@ -1089,7 +1047,6 @@ testNoConsentCannotBeInvited = do let qpeer = Qualified peer localDomain peer2 <- (^. Team.userId) <$> addUserToTeam peer teamPeer let qpeer2 = Qualified peer2 localDomain - ensureQueueEmpty do postConnection userLHNotActivated peer !!! const 201 === statusCode @@ -1125,13 +1082,11 @@ testCannotCreateGroupWithUsersInConflict = do -- team that is legalhold whitelisted (legalholder :: UserId, tid) <- createBindingTeam userLHNotActivated <- (^. Team.userId) <$> addUserToTeam legalholder tid - ensureQueueEmpty putLHWhitelistTeam tid !!! const 200 === statusCode -- team without legalhold (peer :: UserId, teamPeer) <- createBindingTeam peer2 <- (^. Team.userId) <$> addUserToTeam peer teamPeer - ensureQueueEmpty do postConnection userLHNotActivated peer !!! const 201 === statusCode @@ -1164,9 +1119,7 @@ testClaimKeys :: TestClaimKeys -> TestM () testClaimKeys testcase = do -- "cannot fetch prekeys of LH users if requester did not give consent or has old clients" (legalholder, tid) <- createBindingTeam - ensureQueueEmpty (peer, teamPeer) <- createBindingTeam - ensureQueueEmpty let doEnableLH :: HasCallStack => TeamId -> UserId -> UserId -> TestM ClientId doEnableLH team owner uid = do @@ -1279,7 +1232,6 @@ testBenchHack' numPeers = do for_ peers $ \peer -> do postConnection legalholder peer !!! const 201 === statusCode void $ putConnection peer legalholder Conn.Accepted TeamId -> TestM ResponseLBS -getEnabled tid = do - g <- viewGalley - get $ - g - . paths ["i", "teams", toByteString' tid, "features", "legalhold"] - -renewToken :: HasCallStack => Text -> TestM () -renewToken tok = do - b <- viewBrig - void . post $ - b - . paths ["access"] - . cookieRaw "zuid" (toByteString' tok) - . expect2xx - -_putEnabled :: HasCallStack => TeamId -> Public.FeatureStatus -> TestM () -_putEnabled tid enabled = do - g <- viewGalley - putEnabledM g tid enabled - -putEnabledM :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyR -> TeamId -> Public.FeatureStatus -> m () -putEnabledM g tid enabled = void $ putEnabledM' g expect2xx tid enabled - -putEnabled' :: HasCallStack => (Bilge.Request -> Bilge.Request) -> TeamId -> Public.FeatureStatus -> TestM ResponseLBS -putEnabled' extra tid enabled = do - g <- viewGalley - putEnabledM' g extra tid enabled - -putEnabledM' :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyR -> (Bilge.Request -> Bilge.Request) -> TeamId -> Public.FeatureStatus -> m ResponseLBS -putEnabledM' g extra tid enabled = do - put $ - g - . paths ["i", "teams", toByteString' tid, "features", "legalhold"] - . json (Public.WithStatusNoLock enabled Public.LegalholdConfig Public.FeatureTTLUnlimited) - . extra - -postSettings :: HasCallStack => UserId -> TeamId -> NewLegalHoldService -> TestM ResponseLBS -postSettings uid tid new = - -- Retry calls to this endpoint, on k8s it sometimes takes a while to establish a working - -- connection. - retrying policy only412 $ \_ -> do - g <- viewGalley - post $ - g - . paths ["teams", toByteString' tid, "legalhold", "settings"] - . zUser uid - . zConn "conn" - . zType "access" - . json new - where - policy :: RetryPolicy - policy = exponentialBackoff 50 <> limitRetries 5 - only412 :: RetryStatus -> ResponseLBS -> TestM Bool - only412 _ resp = pure $ statusCode resp == 412 - -getSettingsTyped :: HasCallStack => UserId -> TeamId -> TestM ViewLegalHoldService -getSettingsTyped uid tid = responseJsonUnsafe <$> (getSettings uid tid UserId -> TeamId -> TestM ResponseLBS -getSettings uid tid = do - g <- viewGalley - get $ - g - . paths ["teams", toByteString' tid, "legalhold", "settings"] - . zUser uid - . zConn "conn" - . zType "access" - -deleteSettings :: HasCallStack => Maybe PlainTextPassword -> UserId -> TeamId -> TestM ResponseLBS -deleteSettings mPassword uid tid = do - g <- viewGalley - delete $ - g - . paths ["teams", toByteString' tid, "legalhold", "settings"] - . zUser uid - . zConn "conn" - . zType "access" - . json (RemoveLegalHoldSettingsRequest mPassword) - -getUserStatusTyped :: HasCallStack => UserId -> TeamId -> TestM UserLegalHoldStatusResponse -getUserStatusTyped uid tid = do - g <- viewGalley - getUserStatusTyped' g uid tid - -getUserStatusTyped' :: (HasCallStack, MonadHttp m, MonadIO m, MonadCatch m) => GalleyR -> UserId -> TeamId -> m UserLegalHoldStatusResponse -getUserStatusTyped' g uid tid = do - resp <- getUserStatus' g uid tid GalleyR -> UserId -> TeamId -> m ResponseLBS -getUserStatus' g uid tid = do - get $ - g - . paths ["teams", toByteString' tid, "legalhold", toByteString' uid] - . zUser uid - . zConn "conn" - . zType "access" - -approveLegalHoldDevice :: HasCallStack => Maybe PlainTextPassword -> UserId -> UserId -> TeamId -> TestM ResponseLBS -approveLegalHoldDevice mPassword zusr uid tid = do - g <- viewGalley - approveLegalHoldDevice' g mPassword zusr uid tid - -approveLegalHoldDevice' :: - (HasCallStack, MonadHttp m, MonadIO m) => - GalleyR -> - Maybe PlainTextPassword -> - UserId -> - UserId -> - TeamId -> - m ResponseLBS -approveLegalHoldDevice' g mPassword zusr uid tid = do - put $ - g - . paths ["teams", toByteString' tid, "legalhold", toByteString' uid, "approve"] - . zUser zusr - . zConn "conn" - . zType "access" - . json (ApproveLegalHoldForUserRequest mPassword) - -disableLegalHoldForUser :: - HasCallStack => - Maybe PlainTextPassword -> - TeamId -> - UserId -> - UserId -> - TestM ResponseLBS -disableLegalHoldForUser mPassword tid zusr uid = do - g <- viewGalley - disableLegalHoldForUser' g mPassword tid zusr uid - -disableLegalHoldForUser' :: - (HasCallStack, MonadHttp m, MonadIO m) => - GalleyR -> - Maybe PlainTextPassword -> - TeamId -> - UserId -> - UserId -> - m ResponseLBS -disableLegalHoldForUser' g mPassword tid zusr uid = do - delete $ - g - . paths ["teams", toByteString' tid, "legalhold", toByteString' uid] - . zUser zusr - . zType "access" - . json (DisableLegalHoldForUserRequest mPassword) - -assertExactlyOneLegalHoldDevice :: HasCallStack => UserId -> TestM () -assertExactlyOneLegalHoldDevice uid = do - clients :: [Client] <- - getClients uid >>= responseJsonError - liftIO $ do - let numdevs = length $ clientType <$> clients - assertEqual ("expected exactly one legal hold device for user: " <> show uid) numdevs 1 - -assertZeroLegalHoldDevices :: HasCallStack => UserId -> TestM () -assertZeroLegalHoldDevices uid = do - clients :: [Client] <- - getClients uid >>= responseJsonError - liftIO $ do - let numdevs = length $ clientType <$> clients - assertBool - ( "a legal hold device was found when none was expected for user" - <> show uid - ) - (numdevs == 0) - ---------------------------------------------------------------------- ---- Device helpers - -requestLegalHoldDevice :: HasCallStack => UserId -> UserId -> TeamId -> TestM ResponseLBS -requestLegalHoldDevice zusr uid tid = do - g <- viewGalley - requestLegalHoldDevice' g zusr uid tid - -requestLegalHoldDevice' :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyR -> UserId -> UserId -> TeamId -> m ResponseLBS -requestLegalHoldDevice' g zusr uid tid = do - post $ - g - . paths ["teams", toByteString' tid, "legalhold", toByteString' uid] - . zUser zusr - . zConn "conn" - . zType "access" - --------------------------------------------------------------------- --- setup helpers - --- | Create a new legal hold service creation request with the URL from the integration test --- config. -newLegalHoldService :: HasCallStack => TestM NewLegalHoldService -newLegalHoldService = do - config <- view (tsIConf . to provider) - key' <- liftIO $ readServiceKey (publicKey config) - let Just url = - fromByteString $ - encodeUtf8 (botHost config) <> ":" <> cs (show (botPort config)) <> "/legalhold" - pure - NewLegalHoldService - { newLegalHoldServiceUrl = url, - newLegalHoldServiceKey = key', - newLegalHoldServiceToken = ServiceToken "tok" - } - --- | FUTUREWORK: reduce duplication (copied from brig/Provider.hs) -readServiceKey :: (HasCallStack, MonadIO m) => FilePath -> m ServiceKeyPEM -readServiceKey fp = liftIO $ do - bs <- BS.readFile fp - let Right [k] = pemParseBS bs - pure (ServiceKeyPEM k) - -withDummyTestServiceForTeam :: - forall a. - HasCallStack => - UserId -> - TeamId -> - -- | the test - (Chan (Wai.Request, LBS) -> TestM a) -> - TestM a -withDummyTestServiceForTeam owner tid go = - withDummyTestServiceForTeamNoService $ \chan -> do - newService <- newLegalHoldService - postSettings owner tid newService !!! testResponse 201 Nothing - go chan - --- FUTUREWORK: run this test suite against an actual LH service (by changing URL and key in --- the config file), and see if it works as well as with our mock service. -withDummyTestServiceForTeamNoService :: - forall a. - HasCallStack => - -- | the test - (Chan (Wai.Request, LBS) -> TestM a) -> - TestM a -withDummyTestServiceForTeamNoService go = do - withTestService dummyService runTest - where - runTest :: Chan (Wai.Request, LBS) -> TestM a - runTest chan = do - go chan - - dummyService :: Chan (Wai.Request, LBS) -> Wai.Application - dummyService ch req cont = do - reqBody <- Wai.strictRequestBody req - writeChan ch (req, reqBody) - case (pathInfo req, requestMethod req, getRequestHeader "Authorization" req) of - (["legalhold", "status"], "GET", _) -> cont respondOk - (_, _, Nothing) -> cont missingAuth - (["legalhold", "initiate"], "POST", Just _) -> cont initiateResp - (["legalhold", "confirm"], "POST", Just _) -> - cont respondOk - (["legalhold", "remove"], "POST", Just _) -> cont respondOk - _ -> cont respondBad - - initiateResp :: Wai.Response - initiateResp = - Wai.json $ - -- FUTUREWORK: use another key to prevent collisions with keys used by tests - NewLegalHoldClient somePrekeys (head $ someLastPrekeys) - - respondOk :: Wai.Response - respondOk = responseLBS status200 mempty mempty - - respondBad :: Wai.Response - respondBad = responseLBS status404 mempty mempty - - missingAuth :: Wai.Response - missingAuth = responseLBS status400 mempty "no authorization header" - - getRequestHeader :: String -> Wai.Request -> Maybe ByteString - getRequestHeader name req = lookup (fromString name) $ requestHeaders req - --- | FUTUREWORK: this function calls an internal end-point to whitelist a team. It only --- appears to bracket this state change and undo it in a finalizer. --- --- We should probably not have this function, just do the call inline, and use the 'TestM' --- actions again rather than the polymorphic ones that we have here. --- --- it's here for historical reason because we did this in galley.yaml --- at some point in the past rather than in an internal end-point, and that required spawning --- another galley 'Application' with 'withSettingsOverrides'. -withLHWhitelist :: forall a. HasCallStack => TeamId -> TestM a -> TestM a -withLHWhitelist tid action = do - void $ putLHWhitelistTeam tid - opts <- view tsGConf - withSettingsOverrides (const opts) action - --- | If you play with whitelists, you should use this one. Every whitelisted team that does --- not get fully deleted will blow up the whitelist that is cached in every warp handler. -withTeam :: forall a. HasCallStack => (HasCallStack => UserId -> TeamId -> TestM a) -> TestM a -withTeam action = - bracket - createBindingTeam - (uncurry deleteTeam >=> const waitForDeleteEvent) - (uncurry action) - where - waitForDeleteEvent :: TestM () - waitForDeleteEvent = - tryAssertQueue 10 "waitForDeleteEvent" SQS.tDelete - --- | Run a test with an mock legal hold service application. The mock service is also binding --- to a TCP socket for the backend to connect to. The mock service can expose internal --- details to the test (for both read and write) via a 'Chan'. --- --- WARNINGS: (1) This is not concurrency-proof! (2) tests need to be written in a way that --- they can be run several times if they fail the first time. this is the allow for the ssl --- service to have some time to propagate through the test system (needed on k8s). -withTestService :: - HasCallStack => - -- | the mock service - (Chan e -> Application) -> - -- | the test - (Chan e -> TestM a) -> - TestM a -withTestService mkApp go = do - config <- view (tsIConf . to provider) - let tlss = Warp.tlsSettings (cert config) (privateKey config) - let defs = Warp.defaultSettings {Warp.settingsPort = botPort config} - buf <- liftIO newChan - srv <- - liftIO . Async.async $ - Warp.runTLS tlss defs $ - mkApp buf - go buf `finally` liftIO (Async.cancel srv) - -publicKeyNotMatchingService :: PEM -publicKeyNotMatchingService = - let Right [k] = - pemParseBS . BS.unlines $ - [ "-----BEGIN PUBLIC KEY-----", - "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+Kg/PHHU3atXrUbKnw0", - "G06FliXcNt3lMwl2os5twEDcPPFw/feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPH", - "WvUBdiLfGrZqJO223DB6D8K2Su/odmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKV", - "VPOaOzgtAB21XKRiQ4ermqgi3/njr03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiS", - "bUKr/BeArYRcjzr/h5m1In6fG/if9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg8", - "7X883H+LA/d6X5CTiPv1VMxXdBUiGPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7la", - "nQIDAQAZ", - "-----END PUBLIC KEY-----" - ] - in k - -testGetLegalholdStatus :: TestM () -testGetLegalholdStatus = do - (owner1, tid1) <- createBindingTeam - member1 <- view Team.userId <$> addUserToTeam owner1 tid1 - ensureQueueEmpty - - (owner2, tid2) <- createBindingTeam - member2 <- view Team.userId <$> addUserToTeam owner2 tid2 - ensureQueueEmpty - - personal <- randomUser - - let check :: HasCallStack => UserId -> UserId -> Maybe TeamId -> UserLegalHoldStatus -> TestM () - check getter targetUser targetTeam stat = do - profile <- getUserProfile getter targetUser - when (profileLegalholdStatus profile /= stat) $ do - meminfo <- getUserStatusTyped targetUser `mapM` targetTeam - - liftIO . forM_ meminfo $ \mem -> do - assertEqual "member LH status" stat (ulhsrStatus mem) - assertEqual "team id in brig user record" targetTeam (profileTeam profile) - - liftIO $ assertEqual "user profile status info" stat (profileLegalholdStatus profile) - - requestDev :: HasCallStack => UserId -> UserId -> TeamId -> TestM () - requestDev requestor target tid = do - requestLegalHoldDevice requestor target tid !!! testResponse 201 Nothing - - approveDev :: HasCallStack => UserId -> TeamId -> TestM () - approveDev target tid = do - approveLegalHoldDevice (Just defPassword) target target tid !!! testResponse 200 Nothing - - check owner1 member1 (Just tid1) UserLegalHoldNoConsent - check member1 member1 (Just tid1) UserLegalHoldNoConsent - check owner2 member1 (Just tid1) UserLegalHoldNoConsent - check member2 member1 (Just tid1) UserLegalHoldNoConsent - check personal member1 (Just tid1) UserLegalHoldNoConsent - check owner1 personal Nothing UserLegalHoldNoConsent - check member1 personal Nothing UserLegalHoldNoConsent - check owner2 personal Nothing UserLegalHoldNoConsent - check member2 personal Nothing UserLegalHoldNoConsent - check personal personal Nothing UserLegalHoldNoConsent - - putLHWhitelistTeam tid1 !!! const 200 === statusCode - - withDummyTestServiceForTeam owner1 tid1 $ \_chan -> do - check owner1 member1 (Just tid1) UserLegalHoldDisabled - check member2 member1 (Just tid1) UserLegalHoldDisabled - check personal member1 (Just tid1) UserLegalHoldDisabled - - requestDev owner1 member1 tid1 - check personal member1 (Just tid1) UserLegalHoldPending - - approveDev member1 tid1 - check personal member1 (Just tid1) UserLegalHoldEnabled - - ensureQueueEmpty - ----------------------------------------------------------------------- --- test helpers - -deriving instance Show Ev.Event - -deriving instance Show Ev.UserEvent - -deriving instance Show Ev.ClientEvent - -deriving instance Show Ev.PropertyEvent - -deriving instance Show Ev.ConnectionEvent - --- (partial implementation, just good enough to make the tests work) -instance FromJSON Ev.Event where - parseJSON ev = flip (withObject "Ev.Event") ev $ \o -> do - typ :: Text <- o .: "type" - if - | typ `elem` ["user.legalhold-request", "user.legalhold-enable", "user.legalhold-disable"] -> Ev.UserEvent <$> Aeson.parseJSON ev - | typ `elem` ["user.client-add", "user.client-remove"] -> Ev.ClientEvent <$> Aeson.parseJSON ev - | typ == "user.connection" -> Ev.ConnectionEvent <$> Aeson.parseJSON ev - | otherwise -> fail $ "Ev.Event: unsupported event type: " <> show typ - --- (partial implementation, just good enough to make the tests work) -instance FromJSON Ev.UserEvent where - parseJSON = withObject "Ev.UserEvent" $ \o -> do - tag :: Text <- o .: "type" - case tag of - "user.legalhold-enable" -> Ev.UserLegalHoldEnabled <$> o .: "id" - "user.legalhold-disable" -> Ev.UserLegalHoldDisabled <$> o .: "id" - "user.legalhold-request" -> - Ev.LegalHoldClientRequested - <$> ( Ev.LegalHoldClientRequestedData - <$> o .: "id" -- this is the target user - <*> o .: "last_prekey" - <*> (o .: "client" >>= withObject "id" (.: "id")) - ) - x -> fail $ "Ev.UserEvent: unsupported event type: " ++ show x - --- (partial implementation, just good enough to make the tests work) -instance FromJSON Ev.ClientEvent where - parseJSON = withObject "Ev.ClientEvent" $ \o -> do - tag :: Text <- o .: "type" - case tag of - "user.client-add" -> Ev.ClientAdded fakeuid <$> o .: "client" - "user.client-remove" -> Ev.ClientRemoved fakeuid <$> (o .: "client" >>= withObject "id" (.: "id")) - x -> fail $ "Ev.ClientEvent: unsupported event type: " ++ show x - where - fakeuid = read @UserId "6980fb5e-ba64-11eb-a339-0b3625bf01be" - -instance FromJSON Ev.ConnectionEvent where - parseJSON = Aeson.withObject "ConnectionEvent" $ \o -> do - tag :: Text <- o .: "type" - case tag of - "user.connection" -> - Ev.ConnectionUpdated - <$> o .: "connection" - <*> pure Nothing - <*> pure Nothing - x -> fail $ "unspported event type: " ++ show x - -assertNotification :: (HasCallStack, FromJSON a, MonadIO m) => WS.WebSocket -> (a -> Assertion) -> m () -assertNotification ws predicate = - void . liftIO . WS.assertMatch (5 WS.# WS.Second) ws $ \notif -> do - unless ((NonEmpty.length . List1.toNonEmpty $ ntfPayload $ notif) == 1) $ - error $ - "not suppored by test helper: event with more than one object in the payload: " <> cs (Aeson.encode notif) - let j = Aeson.Object $ List1.head (ntfPayload notif) - case Aeson.fromJSON j of - Aeson.Success x -> predicate x - Aeson.Error s -> error $ s ++ " in " ++ cs (Aeson.encode j) - -assertNoNotification :: (HasCallStack, MonadIO m) => WS.WebSocket -> m () -assertNoNotification ws = void . liftIO $ WS.assertNoEvent (5 WS.# WS.Second) [ws] - -assertMatchJSON :: (HasCallStack, FromJSON a, MonadThrow m, MonadCatch m, MonadIO m) => Chan (Wai.Request, LBS) -> (a -> m ()) -> m () -assertMatchJSON c match = do - assertMatchChan c $ \(_, reqBody) -> do - case Aeson.eitherDecode reqBody of - Right x -> match x - Left s -> error $ s ++ " in " ++ cs reqBody - -assertMatchChan :: (HasCallStack, MonadThrow m, MonadCatch m, MonadIO m) => Chan a -> (a -> m ()) -> m () -assertMatchChan c match = go [] - where - refill = mapM_ (liftIO <$> writeChan c) - go buf = do - m <- liftIO . timeout (5 WS.# WS.Second) . readChan $ c - case m of - Just n -> - do - match n - refill buf - `catchAll` \e -> case asyncExceptionFromException e of - Just x -> error $ show (x :: SomeAsyncException) - Nothing -> go (n : buf) - Nothing -> do - refill buf - error "Timeout" - -getLHWhitelistedTeam :: HasCallStack => TeamId -> TestM ResponseLBS -getLHWhitelistedTeam tid = do - galley <- viewGalley - getLHWhitelistedTeam' galley tid - -getLHWhitelistedTeam' :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyR -> TeamId -> m ResponseLBS -getLHWhitelistedTeam' g tid = do - get - ( g - . paths ["i", "legalhold", "whitelisted-teams", toByteString' tid] - ) - -putLHWhitelistTeam :: HasCallStack => TeamId -> TestM ResponseLBS -putLHWhitelistTeam tid = do - galley <- viewGalley - putLHWhitelistTeam' galley tid - -putLHWhitelistTeam' :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyR -> TeamId -> m ResponseLBS -putLHWhitelistTeam' g tid = do - put - ( g - . paths ["i", "legalhold", "whitelisted-teams", toByteString' tid] - ) - -_deleteLHWhitelistTeam :: HasCallStack => TeamId -> TestM ResponseLBS -_deleteLHWhitelistTeam tid = do - galley <- viewGalley - deleteLHWhitelistTeam' galley tid - -deleteLHWhitelistTeam' :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyR -> TeamId -> m ResponseLBS -deleteLHWhitelistTeam' g tid = do - delete - ( g - . paths ["i", "legalhold", "whitelisted-teams", toByteString' tid] - ) - -errWith :: (HasCallStack, Typeable a, FromJSON a) => Int -> (a -> Bool) -> ResponseLBS -> TestM () -errWith wantStatus wantBody rsp = liftIO $ do - assertEqual "" wantStatus (statusCode rsp) - assertBool - (show $ responseBody rsp) - ( maybe False wantBody (responseJsonMaybe rsp) - ) diff --git a/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs b/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs index 69b722e4b7..bc67f939f4 100644 --- a/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs +++ b/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs @@ -1,5 +1,7 @@ {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} {-# OPTIONS_GHC -Wno-orphans #-} +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- @@ -23,7 +25,7 @@ module API.Teams.LegalHold.DisabledByDefault ) where -import API.SQS +import API.Teams.LegalHold.Util import API.Util import Bilge hiding (accept, head, timeout, trace) import Bilge.Assert @@ -31,52 +33,33 @@ import Brig.Types.Intra (UserSet (..)) import Brig.Types.Test.Arbitrary () import qualified Brig.Types.User.Event as Ev import qualified Cassandra.Exec as Cql -import qualified Control.Concurrent.Async as Async import Control.Concurrent.Chan -import Control.Concurrent.Timeout hiding (threadDelay) -import Control.Exception (asyncExceptionFromException) import Control.Lens -import Control.Monad.Catch -import Control.Retry (RetryPolicy, RetryStatus, exponentialBackoff, limitRetries, retrying) -import qualified Data.Aeson as Aeson -import Data.Aeson.Types (FromJSON, withObject, (.:)) -import qualified Data.ByteString as BS -import qualified Data.ByteString.Char8 as BS -import Data.ByteString.Conversion import Data.Id import Data.LegalHold -import qualified Data.List.NonEmpty as NonEmpty import qualified Data.List1 as List1 import qualified Data.Map.Strict as Map -import Data.Misc (PlainTextPassword) import Data.PEM import Data.Range import qualified Data.Set as Set -import Data.String.Conversions (LBS, cs) -import Data.Text.Encoding (encodeUtf8) +import Data.String.Conversions (LBS) import Galley.Cassandra.Client import Galley.Cassandra.LegalHold import qualified Galley.Cassandra.LegalHold as LegalHoldData import qualified Galley.Env as Galley -import Galley.Options (optSettings, setFeatureFlags) import qualified Galley.Types.Clients as Clients import Galley.Types.Teams import Imports -import Network.HTTP.Types.Status (status200, status400, status404) +import Network.HTTP.Types.Status (status200, status404) import Network.Wai as Wai import qualified Network.Wai.Handler.Warp as Warp -import qualified Network.Wai.Handler.Warp.Internal as Warp -import qualified Network.Wai.Handler.WarpTLS as Warp import qualified Network.Wai.Utilities.Error as Error -import qualified Network.Wai.Utilities.Response as Wai -import System.IO (hPutStrLn) import Test.QuickCheck.Instances () import Test.Tasty import qualified Test.Tasty.Cannon as WS import Test.Tasty.HUnit import TestHelpers import TestSetup -import Wire.API.Internal.Notification (ntfPayload) import qualified Wire.API.Message as Msg import Wire.API.Provider.Service import qualified Wire.API.Team.Feature as Public @@ -86,52 +69,40 @@ import Wire.API.Team.Member import qualified Wire.API.Team.Member as Team import Wire.API.Team.Permission import Wire.API.Team.Role -import Wire.API.User (UserProfile (..)) import Wire.API.User.Client import qualified Wire.API.User.Client as Client -onlyIfLhEnabled :: TestM () -> TestM () -onlyIfLhEnabled action = do - featureLegalHold <- view (tsGConf . optSettings . setFeatureFlags . flagLegalHold) - case featureLegalHold of - FeatureLegalHoldDisabledPermanently -> - liftIO $ hPutStrLn stderr "*** legalhold feature flag disabled, not running this test case" - FeatureLegalHoldDisabledByDefault -> - action - FeatureLegalHoldWhitelistTeamsAndImplicitConsent -> - liftIO $ hPutStrLn stderr "*** legalhold feature flag set to whitelisted only, not running this test case" - tests :: IO TestSetup -> TestTree tests s = -- See also Client Tests in Brig; where behaviour around deleting/adding LH clients is tested testGroup "Teams LegalHold API (with flag disabled-by-default)" [ -- device handling (CRUD) - test s "POST /teams/{tid}/legalhold/{uid}" (onlyIfLhEnabled testRequestLegalHoldDevice), - test s "PUT /teams/{tid}/legalhold/approve" (onlyIfLhEnabled testApproveLegalHoldDevice), + testOnlyIfLhEnabled s "POST /teams/{tid}/legalhold/{uid}" testRequestLegalHoldDevice, + testOnlyIfLhEnabled s "PUT /teams/{tid}/legalhold/approve" testApproveLegalHoldDevice, test s "(user denies approval: nothing needs to be done in backend)" (pure ()), - test s "GET /teams/{tid}/legalhold/{uid}" (onlyIfLhEnabled testGetLegalHoldDeviceStatus), - test s "DELETE /teams/{tid}/legalhold/{uid}" (onlyIfLhEnabled testDisableLegalHoldForUser), + testOnlyIfLhEnabled s "GET /teams/{tid}/legalhold/{uid}" testGetLegalHoldDeviceStatus, + testOnlyIfLhEnabled s "DELETE /teams/{tid}/legalhold/{uid}" testDisableLegalHoldForUser, -- legal hold settings - test s "POST /teams/{tid}/legalhold/settings" (onlyIfLhEnabled testCreateLegalHoldTeamSettings), - test s "GET /teams/{tid}/legalhold/settings" (onlyIfLhEnabled testGetLegalHoldTeamSettings), - test s "DELETE /teams/{tid}/legalhold/settings" (onlyIfLhEnabled testRemoveLegalHoldFromTeam), - test s "GET, PUT [/i]?/teams/{tid}/legalhold" (onlyIfLhEnabled testEnablePerTeam), - test s "GET, PUT [/i]?/teams/{tid}/legalhold - too large" (onlyIfLhEnabled testEnablePerTeamTooLarge), + testOnlyIfLhEnabled s "POST /teams/{tid}/legalhold/settings" testCreateLegalHoldTeamSettings, + testOnlyIfLhEnabled s "GET /teams/{tid}/legalhold/settings" testGetLegalHoldTeamSettings, + testOnlyIfLhEnabled s "DELETE /teams/{tid}/legalhold/settings" testRemoveLegalHoldFromTeam, + testOnlyIfLhEnabled s "GET, PUT [/i]?/teams/{tid}/legalhold" testEnablePerTeam, + testOnlyIfLhEnabled s "GET, PUT [/i]?/teams/{tid}/legalhold - too large" testEnablePerTeamTooLarge, -- behavior of existing end-points - test s "POST /clients" (onlyIfLhEnabled testCannotCreateLegalHoldDeviceOldAPI), - test s "GET /teams/{tid}/members" (onlyIfLhEnabled testGetTeamMembersIncludesLHStatus), - test s "POST /register - cannot add team members above fanout limit" (onlyIfLhEnabled testAddTeamUserTooLargeWithLegalhold), - test s "GET legalhold status in user profile" (onlyIfLhEnabled testGetLegalholdStatus), + testOnlyIfLhEnabled s "POST /clients" testCannotCreateLegalHoldDeviceOldAPI, + testOnlyIfLhEnabled s "GET /teams/{tid}/members" testGetTeamMembersIncludesLHStatus, + testOnlyIfLhEnabled s "POST /register - cannot add team members above fanout limit" testAddTeamUserTooLargeWithLegalhold, + testOnlyIfLhEnabled s "GET legalhold status in user profile" testGetLegalholdStatus, {- TODO: conversations/{cnv}/otr/messages - possibly show the legal hold device (if missing) as a different device type (or show that on device level, depending on how client teams prefer) GET /team/{tid}/members - show legal hold status of all members -} - test s "handshake between LH device and user with old clients is blocked" (onlyIfLhEnabled testOldClientsBlockDeviceHandshake), - test s "User cannot fetch prekeys of LH users if consent is missing" (onlyIfLhEnabled (testClaimKeys TCKConsentMissing)), - test s "User cannot fetch prekeys of LH users: if user has old client" (onlyIfLhEnabled (testClaimKeys TCKOldClient)), - test s "User can fetch prekeys of LH users if consent is given and user has only new clients" (onlyIfLhEnabled (testClaimKeys TCKConsentAndNewClients)) + testOnlyIfLhEnabled s "handshake between LH device and user with old clients is blocked" testOldClientsBlockDeviceHandshake, + testOnlyIfLhEnabled s "User cannot fetch prekeys of LH users if consent is missing" (testClaimKeys TCKConsentMissing), + testOnlyIfLhEnabled s "User cannot fetch prekeys of LH users: if user has old client" (testClaimKeys TCKOldClient), + testOnlyIfLhEnabled s "User can fetch prekeys of LH users if consent is given and user has only new clients" (testClaimKeys TCKConsentAndNewClients) ] testRequestLegalHoldDevice :: TestM () @@ -139,13 +110,12 @@ testRequestLegalHoldDevice = do (owner, tid) <- createBindingTeam member <- randomUser addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing - ensureQueueEmpty -- Can't request a device if team feature flag is disabled requestLegalHoldDevice owner member tid !!! testResponse 403 (Just "legalhold-not-enabled") cannon <- view tsCannon -- Assert that the appropriate LegalHold Request notification is sent to the user's -- clients - WS.bracketR2 cannon member member $ \(ws, ws') -> withDummyTestServiceForTeam owner tid $ \_chan -> do + WS.bracketR2 cannon member member $ \(ws, ws') -> withDummyTestServiceForTeam' owner tid $ \_ _chan -> do do -- test device creation without consent requestLegalHoldDevice member member tid !!! testResponse 403 (Just "operation-denied") @@ -237,13 +207,12 @@ testApproveLegalHoldDevice = do grantConsent tid owner grantConsent tid member grantConsent tid member2 - ensureQueueEmpty -- not allowed to approve if team setting is disabled approveLegalHoldDevice (Just defPassword) owner member tid !!! testResponse 403 (Just "legalhold-not-enabled") cannon <- view tsCannon WS.bracketRN cannon [owner, member, member, member2, outsideContact, stranger] $ - \[ows, mws, mws', member2Ws, outsideContactWs, strangerWs] -> withDummyTestServiceForTeam owner tid $ \chan -> do + \[ows, mws, mws', member2Ws, outsideContactWs, strangerWs] -> withDummyTestServiceForTeam' owner tid $ \_ chan -> do requestLegalHoldDevice owner member tid !!! testResponse 201 Nothing liftIO . assertMatchJSON chan $ \(RequestNewLegalHoldClient userId' teamId') -> do assertEqual "userId == member" userId' member @@ -292,7 +261,6 @@ testGetLegalHoldDeviceStatus = do (owner, tid) <- createBindingTeam member <- randomUser addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing - ensureQueueEmpty forM_ [owner, member] $ \uid -> do status <- getUserStatusTyped uid tid liftIO $ @@ -300,7 +268,7 @@ testGetLegalHoldDeviceStatus = do "unexpected status" (UserLegalHoldStatusResponse UserLegalHoldNoConsent Nothing Nothing) status - withDummyTestServiceForTeam owner tid $ \_chan -> do + withDummyTestServiceForTeam' owner tid $ \_ _chan -> do grantConsent tid member do UserLegalHoldStatusResponse userStatus lastPrekey' clientId' <- getUserStatusTyped member tid @@ -342,9 +310,8 @@ testDisableLegalHoldForUser = do (owner, tid) <- createBindingTeam member <- randomUser addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing - ensureQueueEmpty cannon <- view tsCannon - WS.bracketR2 cannon owner member $ \(ows, mws) -> withDummyTestServiceForTeam owner tid $ \chan -> do + WS.bracketR2 cannon owner member $ \(ows, mws) -> withDummyTestServiceForTeam' owner tid $ \_ chan -> do grantConsent tid member requestLegalHoldDevice owner member tid !!! testResponse 201 Nothing approveLegalHoldDevice (Just defPassword) member member tid !!! testResponse 200 Nothing @@ -384,16 +351,16 @@ testCreateLegalHoldTeamSettings = do (owner, tid) <- createBindingTeam member <- randomUser addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing - ensureQueueEmpty - newService <- newLegalHoldService + -- Random port, hopefully nothing is runing here! + brokenService <- newLegalHoldService 4242 -- not allowed to create if team setting is disabled - postSettings owner tid newService !!! testResponse 403 (Just "legalhold-not-enabled") + postSettings owner tid brokenService !!! testResponse 403 (Just "legalhold-not-enabled") putEnabled tid Public.FeatureStatusEnabled -- enable it for this team -- not allowed for users with corresp. permission bit missing - postSettings member tid newService !!! testResponse 403 (Just "operation-denied") + postSettings member tid brokenService !!! testResponse 403 (Just "operation-denied") -- rejected if service is not available - postSettings owner tid newService !!! testResponse 412 (Just "legalhold-unavailable") + postSettings owner tid brokenService !!! testResponse 412 (Just "legalhold-unavailable") -- checks /status of legal hold service (boolean argument says whether the service is -- behaving or not) let lhapp :: HasCallStack => IsWorking -> Chan Void -> Application @@ -407,11 +374,12 @@ testCreateLegalHoldTeamSettings = do respondOk = responseLBS status200 mempty mempty respondBad :: Wai.Response respondBad = responseLBS status404 mempty mempty - lhtest :: HasCallStack => IsWorking -> Chan Void -> TestM () - lhtest NotWorking _ = do - postSettings owner tid newService !!! testResponse 412 (Just "legalhold-unavailable") - lhtest Working _ = do + lhtest :: HasCallStack => IsWorking -> Warp.Port -> Chan Void -> TestM () + lhtest NotWorking _ _ = do + postSettings owner tid brokenService !!! testResponse 412 (Just "legalhold-unavailable") + lhtest Working lhPort _ = do let Right [k] = pemParseBS "-----BEGIN PUBLIC KEY-----\n\n-----END PUBLIC KEY-----" + newService <- newLegalHoldService lhPort let badServiceBadKey = newService {newLegalHoldServiceKey = ServiceKeyPEM k} postSettings owner tid badServiceBadKey !!! testResponse 400 (Just "legalhold-invalid-key") postSettings owner tid newService !!! testResponse 201 Nothing @@ -426,7 +394,7 @@ testCreateLegalHoldTeamSettings = do -- this request would actually return a 201 let badServiceValidKey = newService {newLegalHoldServiceKey = ServiceKeyPEM publicKeyNotMatchingService} postSettings owner tid badServiceValidKey !!! testResponse 412 (Just "legalhold-unavailable") - -- We do not use the higher level withDummyTestServiceForTeam here because we want to make + -- We do not use the higher level withDummyTestServiceForTeam' here because we want to make -- legalhold service misbehave on purpose in certain cases -- if no valid service response can be obtained, responds with 400 withTestService (lhapp NotWorking) (lhtest NotWorking) @@ -442,12 +410,11 @@ testGetLegalHoldTeamSettings = do stranger <- randomUser member <- randomUser addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing - ensureQueueEmpty - newService <- newLegalHoldService let lhapp :: Chan () -> Application lhapp _ch _req res = res $ responseLBS status200 mempty mempty - withTestService lhapp $ \_ -> do + withTestService lhapp $ \lhPort _ -> do -- returns 403 if user is not in team. + newService <- newLegalHoldService lhPort getSettings stranger tid !!! testResponse 403 (Just "no-team-member") -- returns 200 with corresp. status if legalhold for team is disabled do @@ -486,11 +453,10 @@ testRemoveLegalHoldFromTeam = do stranger <- randomUser member <- randomUser addTeamMemberInternal tid member noPermissions Nothing - ensureQueueEmpty -- fails if LH for team is disabled deleteSettings (Just defPassword) owner tid !!! testResponse 403 (Just "legalhold-not-enabled") - withDummyTestServiceForTeam owner tid $ \chan -> do - newService <- newLegalHoldService + withDummyTestServiceForTeam' owner tid $ \lhPort chan -> do + newService <- newLegalHoldService lhPort postSettings owner tid newService !!! testResponse 201 Nothing -- enable legalhold for member do @@ -537,7 +503,6 @@ testEnablePerTeam = do (owner, tid) <- createBindingTeam member <- randomUser addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing - ensureQueueEmpty do status :: Public.WithStatusNoLock Public.LegalholdConfig <- responseJsonUnsafe <$> (getEnabled tid (getEnabled tid do + withDummyTestServiceForTeam' owner tid $ \_ _chan -> do grantConsent tid member requestLegalHoldDevice owner member tid !!! const 201 === statusCode approveLegalHoldDevice (Just defPassword) member member tid !!! testResponse 200 Nothing @@ -604,12 +569,10 @@ testCannotCreateLegalHoldDeviceOldAPI :: TestM () testCannotCreateLegalHoldDeviceOldAPI = do member <- randomUser (owner, tid) <- createBindingTeam - ensureQueueEmpty -- user without team can't add LH device tryout member -- team member can't add LH device addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing - ensureQueueEmpty tryout member -- team owner can't add LH device tryout owner @@ -635,7 +598,6 @@ testGetTeamMembersIncludesLHStatus = do (owner, tid) <- createBindingTeam member <- randomUser addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing - ensureQueueEmpty let findMemberStatus :: [TeamMember] -> Maybe UserLegalHoldStatus findMemberStatus ms = @@ -651,7 +613,7 @@ testGetTeamMembersIncludesLHStatus = do (findMemberStatus members') check UserLegalHoldNoConsent "disabled when it is disabled for the team" - withDummyTestServiceForTeam owner tid $ \_chan -> do + withDummyTestServiceForTeam' owner tid $ \_ _chan -> do check UserLegalHoldNoConsent "no_consent on new team members" grantConsent tid member check UserLegalHoldDisabled "disabled on team members that have granted consent" @@ -676,11 +638,9 @@ testOldClientsBlockDeviceHandshake = do (legalholder, tid) <- createBindingTeam legalholder2 <- view Team.userId <$> addUserToTeam legalholder tid - ensureQueueEmpty (peer, tid2) <- -- has to be a team member, granting LH consent for personal users is not supported. createBindingTeam - ensureQueueEmpty let doEnableLH :: HasCallStack => UserId -> UserId -> TestM ClientId doEnableLH owner uid = do @@ -696,7 +656,7 @@ testOldClientsBlockDeviceHandshake = do <&> (\[x] -> x) <&> clientId - withDummyTestServiceForTeam legalholder tid $ \_chan -> do + withDummyTestServiceForTeam' legalholder tid $ \_ _chan -> do grantConsent tid legalholder grantConsent tid legalholder2 @@ -735,14 +695,6 @@ testOldClientsBlockDeviceHandshake = do -- legalholder2 LH device missing ] - errWith :: (HasCallStack, Typeable a, FromJSON a) => Int -> (a -> Bool) -> ResponseLBS -> TestM () - errWith wantStatus wantBody rsp = liftIO $ do - assertEqual "" wantStatus (statusCode rsp) - assertBool - (show $ responseBody rsp) - ( maybe False wantBody (responseJsonMaybe rsp) - ) - -- LH devices are treated as clients that have the ClientSupportsLegalholdImplicitConsent -- capability (so LH doesn't break for users who have LH devices; it sounds silly, but -- it's good to test this, since it did require adding a few lines of production code in @@ -765,9 +717,7 @@ testClaimKeys :: TestClaimKeys -> TestM () testClaimKeys testcase = do -- "cannot fetch prekeys of LH users if requester did not give consent or has old clients" (legalholder, tid) <- createBindingTeam - ensureQueueEmpty (peer, teamPeer) <- createBindingTeam - ensureQueueEmpty let doEnableLH :: HasCallStack => TeamId -> UserId -> UserId -> TestM ClientId doEnableLH team owner uid = do @@ -812,489 +762,20 @@ testClaimKeys testcase = do let userClients = UserClients (Map.fromList [(legalholder, Set.fromList [legalholderLHDevice])]) getMultiUserPrekeyBundleUnqualified peer userClients !!! assertResponse' - withDummyTestServiceForTeam legalholder tid $ \_chan -> do + withDummyTestServiceForTeam' legalholder tid $ \_ _chan -> do grantConsent tid legalholder legalholderLHDevice <- doEnableLH tid legalholder legalholder makePeerClient fetchKeys legalholderLHDevice ----------------------------------------------------------------------- --- API helpers - -getEnabled :: HasCallStack => TeamId -> TestM ResponseLBS -getEnabled tid = do - g <- viewGalley - get $ - g - . paths ["i", "teams", toByteString' tid, "features", "legalhold"] - -renewToken :: HasCallStack => Text -> TestM () -renewToken tok = do - b <- viewBrig - void . post $ - b - . paths ["access"] - . cookieRaw "zuid" (toByteString' tok) - . expect2xx - -putEnabled :: HasCallStack => TeamId -> Public.FeatureStatus -> TestM () -putEnabled tid enabled = do - g <- viewGalley - putEnabledM g tid enabled - -putEnabledM :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyR -> TeamId -> Public.FeatureStatus -> m () -putEnabledM g tid enabled = void $ putEnabledM' g expect2xx tid enabled - -putEnabled' :: HasCallStack => (Bilge.Request -> Bilge.Request) -> TeamId -> Public.FeatureStatus -> TestM ResponseLBS -putEnabled' extra tid enabled = do - g <- viewGalley - putEnabledM' g extra tid enabled - -putEnabledM' :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyR -> (Bilge.Request -> Bilge.Request) -> TeamId -> Public.FeatureStatus -> m ResponseLBS -putEnabledM' g extra tid enabled = do - put $ - g - . paths ["i", "teams", toByteString' tid, "features", "legalhold"] - . json (Public.WithStatusNoLock enabled Public.LegalholdConfig Public.FeatureTTLUnlimited) - . extra - -postSettings :: HasCallStack => UserId -> TeamId -> NewLegalHoldService -> TestM ResponseLBS -postSettings uid tid new = - -- Retry calls to this endpoint, on k8s it sometimes takes a while to establish a working - -- connection. - retrying policy only412 $ \_ -> do - g <- viewGalley - post $ - g - . paths ["teams", toByteString' tid, "legalhold", "settings"] - . zUser uid - . zConn "conn" - . zType "access" - . json new - where - policy :: RetryPolicy - policy = exponentialBackoff 50 <> limitRetries 5 - only412 :: RetryStatus -> ResponseLBS -> TestM Bool - only412 _ resp = pure $ statusCode resp == 412 - -getSettingsTyped :: HasCallStack => UserId -> TeamId -> TestM ViewLegalHoldService -getSettingsTyped uid tid = responseJsonUnsafe <$> (getSettings uid tid UserId -> TeamId -> TestM ResponseLBS -getSettings uid tid = do - g <- viewGalley - get $ - g - . paths ["teams", toByteString' tid, "legalhold", "settings"] - . zUser uid - . zConn "conn" - . zType "access" - -deleteSettings :: HasCallStack => Maybe PlainTextPassword -> UserId -> TeamId -> TestM ResponseLBS -deleteSettings mPassword uid tid = do - g <- viewGalley - delete $ - g - . paths ["teams", toByteString' tid, "legalhold", "settings"] - . zUser uid - . zConn "conn" - . zType "access" - . json (RemoveLegalHoldSettingsRequest mPassword) - -getUserStatusTyped :: HasCallStack => UserId -> TeamId -> TestM UserLegalHoldStatusResponse -getUserStatusTyped uid tid = do - g <- viewGalley - getUserStatusTyped' g uid tid - -getUserStatusTyped' :: (HasCallStack, MonadHttp m, MonadIO m, MonadCatch m) => GalleyR -> UserId -> TeamId -> m UserLegalHoldStatusResponse -getUserStatusTyped' g uid tid = do - resp <- getUserStatus' g uid tid GalleyR -> UserId -> TeamId -> m ResponseLBS -getUserStatus' g uid tid = do - get $ - g - . paths ["teams", toByteString' tid, "legalhold", toByteString' uid] - . zUser uid - . zConn "conn" - . zType "access" - -approveLegalHoldDevice :: HasCallStack => Maybe PlainTextPassword -> UserId -> UserId -> TeamId -> TestM ResponseLBS -approveLegalHoldDevice mPassword zusr uid tid = do - g <- viewGalley - approveLegalHoldDevice' g mPassword zusr uid tid - -approveLegalHoldDevice' :: - (HasCallStack, MonadHttp m, MonadIO m) => - GalleyR -> - Maybe PlainTextPassword -> - UserId -> - UserId -> - TeamId -> - m ResponseLBS -approveLegalHoldDevice' g mPassword zusr uid tid = do - put $ - g - . paths ["teams", toByteString' tid, "legalhold", toByteString' uid, "approve"] - . zUser zusr - . zConn "conn" - . zType "access" - . json (ApproveLegalHoldForUserRequest mPassword) - -disableLegalHoldForUser :: - HasCallStack => - Maybe PlainTextPassword -> - TeamId -> - UserId -> - UserId -> - TestM ResponseLBS -disableLegalHoldForUser mPassword tid zusr uid = do - g <- viewGalley - disableLegalHoldForUser' g mPassword tid zusr uid - -disableLegalHoldForUser' :: - (HasCallStack, MonadHttp m, MonadIO m) => - GalleyR -> - Maybe PlainTextPassword -> - TeamId -> - UserId -> - UserId -> - m ResponseLBS -disableLegalHoldForUser' g mPassword tid zusr uid = do - delete $ - g - . paths ["teams", toByteString' tid, "legalhold", toByteString' uid] - . zUser zusr - . zType "access" - . json (DisableLegalHoldForUserRequest mPassword) - -assertExactlyOneLegalHoldDevice :: HasCallStack => UserId -> TestM () -assertExactlyOneLegalHoldDevice uid = do - clients :: [Client] <- - getClients uid >>= responseJsonError - liftIO $ do - let numdevs = length $ clientType <$> clients - assertEqual ("expected exactly one legal hold device for user: " <> show uid) numdevs 1 - -assertZeroLegalHoldDevices :: HasCallStack => UserId -> TestM () -assertZeroLegalHoldDevices uid = do - clients :: [Client] <- - getClients uid >>= responseJsonError - liftIO $ do - let numdevs = length $ clientType <$> clients - assertBool - ( "a legal hold device was found when none was expected for user" - <> show uid - ) - (numdevs == 0) - ---------------------------------------------------------------------- ---- Device helpers - -grantConsent :: HasCallStack => TeamId -> UserId -> TestM () -grantConsent tid zusr = do - g <- viewGalley - grantConsent' g tid zusr - -grantConsent' :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyR -> TeamId -> UserId -> m () -grantConsent' = grantConsent'' expect2xx - -grantConsent'' :: (HasCallStack, MonadHttp m, MonadIO m) => (Bilge.Request -> Bilge.Request) -> GalleyR -> TeamId -> UserId -> m () -grantConsent'' expectation g tid zusr = do - void . post $ - g - . paths ["teams", toByteString' tid, "legalhold", "consent"] - . zUser zusr - . zConn "conn" - . zType "access" - . expectation - -requestLegalHoldDevice :: HasCallStack => UserId -> UserId -> TeamId -> TestM ResponseLBS -requestLegalHoldDevice zusr uid tid = do - g <- viewGalley - requestLegalHoldDevice' g zusr uid tid - -requestLegalHoldDevice' :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyR -> UserId -> UserId -> TeamId -> m ResponseLBS -requestLegalHoldDevice' g zusr uid tid = do - post $ - g - . paths ["teams", toByteString' tid, "legalhold", toByteString' uid] - . zUser zusr - . zConn "conn" - . zType "access" - -------------------------------------------------------------------- -- setup helpers --- | Create a new legal hold service creation request with the URL from the integration test --- config. -newLegalHoldService :: HasCallStack => TestM NewLegalHoldService -newLegalHoldService = do - config <- view (tsIConf . to provider) - key' <- liftIO $ readServiceKey (publicKey config) - let Just url = - fromByteString $ - encodeUtf8 (botHost config) <> ":" <> cs (show (botPort config)) <> "/legalhold" - pure - NewLegalHoldService - { newLegalHoldServiceUrl = url, - newLegalHoldServiceKey = key', - newLegalHoldServiceToken = ServiceToken "tok" - } - --- | FUTUREWORK: reduce duplication (copied from brig/Provider.hs) -readServiceKey :: (HasCallStack, MonadIO m) => FilePath -> m ServiceKeyPEM -readServiceKey fp = liftIO $ do - bs <- BS.readFile fp - let Right [k] = pemParseBS bs - pure (ServiceKeyPEM k) - --- FUTUREWORK: run this test suite against an actual LH service (by changing URL and key in --- the config file), and see if it works as well as with our mock service. -withDummyTestServiceForTeam :: - forall a. - HasCallStack => - UserId -> - TeamId -> - -- | the test - (Chan (Wai.Request, LBS) -> TestM a) -> - TestM a -withDummyTestServiceForTeam owner tid go = do - withTestService dummyService runTest - where - runTest :: Chan (Wai.Request, LBS) -> TestM a - runTest chan = do - newService <- newLegalHoldService - putEnabled tid Public.FeatureStatusEnabled -- enable it for this team - postSettings owner tid newService !!! testResponse 201 Nothing - go chan - - dummyService :: Chan (Wai.Request, LBS) -> Wai.Application - dummyService ch req cont = do - reqBody <- Wai.strictRequestBody req - writeChan ch (req, reqBody) - case (pathInfo req, requestMethod req, getRequestHeader "Authorization" req) of - (["legalhold", "status"], "GET", _) -> cont respondOk - (_, _, Nothing) -> cont missingAuth - (["legalhold", "initiate"], "POST", Just _) -> cont initiateResp - (["legalhold", "confirm"], "POST", Just _) -> - cont respondOk - (["legalhold", "remove"], "POST", Just _) -> cont respondOk - _ -> cont respondBad - - initiateResp :: Wai.Response - initiateResp = - Wai.json $ - -- FUTUREWORK: use another key to prevent collisions with keys used by tests - NewLegalHoldClient somePrekeys (head $ someLastPrekeys) - - respondOk :: Wai.Response - respondOk = responseLBS status200 mempty mempty - - respondBad :: Wai.Response - respondBad = responseLBS status404 mempty mempty - - missingAuth :: Wai.Response - missingAuth = responseLBS status400 mempty "no authorization header" - - getRequestHeader :: String -> Wai.Request -> Maybe ByteString - getRequestHeader name req = lookup (fromString name) $ requestHeaders req - --- | Run a test with an mock legal hold service application. The mock service is also binding --- to a TCP socket for the backend to connect to. The mock service can expose internal --- details to the test (for both read and write) via a 'Chan'. --- --- WARNINGS: (1) This is not concurrency-proof! (2) tests need to be written in a way that --- they can be run several times if they fail the first time. this is the allow for the ssl --- service to have some time to propagate through the test system (needed on k8s). -withTestService :: - HasCallStack => - -- | the mock service - (Chan e -> Application) -> - -- | the test - (Chan e -> TestM a) -> - TestM a -withTestService mkApp go = do - config <- view (tsIConf . to provider) - let tlss = Warp.tlsSettings (cert config) (privateKey config) - let defs = Warp.defaultSettings {Warp.settingsPort = botPort config} - buf <- liftIO newChan - srv <- - liftIO . Async.async $ - Warp.runTLS tlss defs $ - mkApp buf - go buf `finally` liftIO (Async.cancel srv) - -publicKeyNotMatchingService :: PEM -publicKeyNotMatchingService = - let Right [k] = - pemParseBS . BS.unlines $ - [ "-----BEGIN PUBLIC KEY-----", - "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+Kg/PHHU3atXrUbKnw0", - "G06FliXcNt3lMwl2os5twEDcPPFw/feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPH", - "WvUBdiLfGrZqJO223DB6D8K2Su/odmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKV", - "VPOaOzgtAB21XKRiQ4ermqgi3/njr03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiS", - "bUKr/BeArYRcjzr/h5m1In6fG/if9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg8", - "7X883H+LA/d6X5CTiPv1VMxXdBUiGPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7la", - "nQIDAQAZ", - "-----END PUBLIC KEY-----" - ] - in k - -testGetLegalholdStatus :: TestM () -testGetLegalholdStatus = do - (owner1, tid1) <- createBindingTeam - member1 <- view Team.userId <$> addUserToTeam owner1 tid1 - ensureQueueEmpty - - (owner2, tid2) <- createBindingTeam - member2 <- view Team.userId <$> addUserToTeam owner2 tid2 - ensureQueueEmpty - - personal <- randomUser - - let check :: HasCallStack => UserId -> UserId -> Maybe TeamId -> UserLegalHoldStatus -> TestM () - check getter targetUser targetTeam stat = do - profile <- getUserProfile getter targetUser - when (profileLegalholdStatus profile /= stat) $ do - meminfo <- getUserStatusTyped targetUser `mapM` targetTeam - - liftIO . forM_ meminfo $ \mem -> do - assertEqual "member LH status" stat (ulhsrStatus mem) - assertEqual "team id in brig user record" targetTeam (profileTeam profile) - - liftIO $ assertEqual "user profile status info" stat (profileLegalholdStatus profile) - - requestDev :: HasCallStack => UserId -> UserId -> TeamId -> TestM () - requestDev requestor target tid = do - requestLegalHoldDevice requestor target tid !!! testResponse 201 Nothing - - approveDev :: HasCallStack => UserId -> TeamId -> TestM () - approveDev target tid = do - approveLegalHoldDevice (Just defPassword) target target tid !!! testResponse 200 Nothing - - check owner1 member1 (Just tid1) UserLegalHoldNoConsent - check member1 member1 (Just tid1) UserLegalHoldNoConsent - check owner2 member1 (Just tid1) UserLegalHoldNoConsent - check member2 member1 (Just tid1) UserLegalHoldNoConsent - check personal member1 (Just tid1) UserLegalHoldNoConsent - check owner1 personal Nothing UserLegalHoldNoConsent - check member1 personal Nothing UserLegalHoldNoConsent - check owner2 personal Nothing UserLegalHoldNoConsent - check member2 personal Nothing UserLegalHoldNoConsent - check personal personal Nothing UserLegalHoldNoConsent - - onlyIfLhEnabled $ - withDummyTestServiceForTeam owner1 tid1 $ \_chan -> do - grantConsent tid1 member1 - check owner1 member1 (Just tid1) UserLegalHoldDisabled - check member2 member1 (Just tid1) UserLegalHoldDisabled - check personal member1 (Just tid1) UserLegalHoldDisabled - - requestDev owner1 member1 tid1 - check personal member1 (Just tid1) UserLegalHoldPending - - approveDev member1 tid1 - check personal member1 (Just tid1) UserLegalHoldEnabled - ----------------------------------------------------------------------- --- test helpers - -deriving instance Show Ev.Event - -deriving instance Show Ev.UserEvent - -deriving instance Show Ev.ClientEvent - -deriving instance Show Ev.PropertyEvent - -deriving instance Show Ev.ConnectionEvent - --- (partial implementation, just good enough to make the tests work) -instance FromJSON Ev.Event where - parseJSON ev = flip (withObject "Ev.Event") ev $ \o -> do - typ :: Text <- o .: "type" - if - | typ `elem` ["user.legalhold-request", "user.legalhold-enable", "user.legalhold-disable"] -> Ev.UserEvent <$> Aeson.parseJSON ev - | typ `elem` ["user.client-add", "user.client-remove"] -> Ev.ClientEvent <$> Aeson.parseJSON ev - | typ == "user.connection" -> Ev.ConnectionEvent <$> Aeson.parseJSON ev - | otherwise -> fail $ "Ev.Event: unsupported event type: " <> show typ - --- (partial implementation, just good enough to make the tests work) -instance FromJSON Ev.UserEvent where - parseJSON = withObject "Ev.UserEvent" $ \o -> do - tag :: Text <- o .: "type" - case tag of - "user.legalhold-enable" -> Ev.UserLegalHoldEnabled <$> o .: "id" - "user.legalhold-disable" -> Ev.UserLegalHoldDisabled <$> o .: "id" - "user.legalhold-request" -> - Ev.LegalHoldClientRequested - <$> ( Ev.LegalHoldClientRequestedData - <$> o .: "id" -- this is the target user - <*> o .: "last_prekey" - <*> (o .: "client" >>= withObject "id" (.: "id")) - ) - x -> fail $ "Ev.UserEvent: unsupported event type: " ++ show x - --- (partial implementation, just good enough to make the tests work) -instance FromJSON Ev.ClientEvent where - parseJSON = withObject "Ev.ClientEvent" $ \o -> do - tag :: Text <- o .: "type" - case tag of - "user.client-add" -> Ev.ClientAdded fakeuid <$> o .: "client" - "user.client-remove" -> Ev.ClientRemoved fakeuid <$> (o .: "client" >>= withObject "id" (.: "id")) - x -> fail $ "Ev.ClientEvent: unsupported event type: " ++ show x - where - fakeuid = read @UserId "6980fb5e-ba64-11eb-a339-0b3625bf01be" - -instance FromJSON Ev.ConnectionEvent where - parseJSON = Aeson.withObject "ConnectionEvent" $ \o -> do - tag :: Text <- o .: "type" - case tag of - "user.connection" -> - Ev.ConnectionUpdated - <$> o .: "connection" - <*> pure Nothing - <*> pure Nothing - x -> fail $ "unspported event type: " ++ show x - -assertNotification :: (HasCallStack, FromJSON a, MonadIO m) => WS.WebSocket -> (a -> Assertion) -> m () -assertNotification ws predicate = - void . liftIO . WS.assertMatch (5 WS.# WS.Second) ws $ \notif -> do - unless ((NonEmpty.length . List1.toNonEmpty $ ntfPayload $ notif) == 1) $ - error $ - "not suppored by test helper: event with more than one object in the payload: " <> cs (Aeson.encode notif) - let j = Aeson.Object $ List1.head (ntfPayload notif) - case Aeson.fromJSON j of - Aeson.Success x -> predicate x - Aeson.Error s -> error $ s ++ " in " ++ cs (Aeson.encode j) - -assertNoNotification :: (HasCallStack, MonadIO m) => WS.WebSocket -> m () -assertNoNotification ws = void . liftIO $ WS.assertNoEvent (5 WS.# WS.Second) [ws] - -assertMatchJSON :: (HasCallStack, FromJSON a, MonadThrow m, MonadCatch m, MonadIO m) => Chan (Wai.Request, LBS) -> (a -> m ()) -> m () -assertMatchJSON c match = do - assertMatchChan c $ \(_, reqBody) -> do - case Aeson.eitherDecode reqBody of - Right x -> match x - Left s -> error $ s ++ " in " ++ cs reqBody - -assertMatchChan :: (HasCallStack, MonadThrow m, MonadCatch m, MonadIO m) => Chan a -> (a -> m ()) -> m () -assertMatchChan c match = go [] - where - refill = mapM_ (liftIO <$> writeChan c) - go buf = do - m <- liftIO . timeout (5 WS.# WS.Second) . readChan $ c - case m of - Just n -> - do - match n - refill buf - `catchAll` \e -> case asyncExceptionFromException e of - Just x -> error $ show (x :: SomeAsyncException) - Nothing -> go (n : buf) - Nothing -> do - refill buf - error "Timeout" +withDummyTestServiceForTeam' :: HasCallStack => UserId -> TeamId -> (Warp.Port -> Chan (Wai.Request, LBS) -> TestM a) -> TestM a +withDummyTestServiceForTeam' owner tid go = do + withDummyTestServiceForTeamNoService $ \lhPort chan -> do + newService <- newLegalHoldService lhPort + putEnabled tid Public.FeatureStatusEnabled + postSettings owner tid newService !!! testResponse 201 Nothing + go lhPort chan diff --git a/services/galley/test/integration/API/Teams/LegalHold/Util.hs b/services/galley/test/integration/API/Teams/LegalHold/Util.hs new file mode 100644 index 0000000000..62fc82bb4a --- /dev/null +++ b/services/galley/test/integration/API/Teams/LegalHold/Util.hs @@ -0,0 +1,700 @@ +{-# LANGUAGE RecordWildCards #-} +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} +{-# OPTIONS_GHC -Wno-orphans #-} +-- Disabling for HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + +module API.Teams.LegalHold.Util where + +import API.SQS +import API.Util +import Bilge hiding (accept, head, timeout, trace) +import Bilge.Assert +import Brig.Types.Test.Arbitrary () +import qualified Brig.Types.User.Event as Ev +import qualified Control.Concurrent.Async as Async +import Control.Concurrent.Chan +import Control.Concurrent.Timeout hiding (threadDelay) +import Control.Exception (asyncExceptionFromException) +import Control.Lens hiding ((#)) +import Control.Monad.Catch +import Control.Retry (RetryPolicy, RetryStatus, exponentialBackoff, limitRetries, retrying) +import qualified Data.Aeson as Aeson +import Data.Aeson.Types (FromJSON, withObject, (.:)) +import qualified Data.ByteString as BS +import qualified Data.ByteString.Char8 as BS +import Data.ByteString.Conversion +import Data.CallStack +import Data.Id +import Data.LegalHold +import qualified Data.List.NonEmpty as NonEmpty +import qualified Data.List1 as List1 +import Data.Misc (PlainTextPassword) +import Data.PEM +import Data.Streaming.Network (bindRandomPortTCP) +import Data.String.Conversions (LBS, cs) +import Data.Tagged +import Data.Text.Encoding (encodeUtf8) +import Galley.Options +import Galley.Types.Teams +import Imports +import Network.HTTP.Types.Status (status200, status400, status404) +import Network.Socket (Socket) +import qualified Network.Socket as Socket +import Network.Wai as Wai +import qualified Network.Wai.Handler.Warp as Warp +import qualified Network.Wai.Handler.Warp.Internal as Warp +import qualified Network.Wai.Handler.WarpTLS as Warp +import qualified Network.Wai.Utilities.Response as Wai +import Test.QuickCheck.Instances () +import qualified Test.Tasty.Cannon as WS +import Test.Tasty.HUnit +import Test.Tasty.Options +import Test.Tasty.Providers +import Test.Tasty.Providers.ConsoleFormat +import Test.Tasty.Runners +import TestSetup +import Wire.API.Internal.Notification (ntfPayload) +import Wire.API.Provider.Service +import qualified Wire.API.Team.Feature as Public +import Wire.API.Team.LegalHold +import Wire.API.Team.LegalHold.External +import qualified Wire.API.Team.Member as Team +import Wire.API.User (UserProfile (..)) +import Wire.API.User.Client + +-------------------------------------------------------------------- +-- setup helpers + +-- | Create a new legal hold service creation request with the URL from the integration test +-- config. +newLegalHoldService :: HasCallStack => Warp.Port -> TestM NewLegalHoldService +newLegalHoldService lhPort = do + config <- view (tsIConf . to provider) + key' <- liftIO $ readServiceKey (publicKey config) + let Just url = + fromByteString $ + encodeUtf8 (botHost config) <> ":" <> cs (show lhPort) <> "/legalhold" + pure + NewLegalHoldService + { newLegalHoldServiceUrl = url, + newLegalHoldServiceKey = key', + newLegalHoldServiceToken = ServiceToken "tok" + } + +-- | FUTUREWORK: reduce duplication (copied from brig/Provider.hs) +readServiceKey :: (HasCallStack, MonadIO m) => FilePath -> m ServiceKeyPEM +readServiceKey fp = liftIO $ do + bs <- BS.readFile fp + let Right [k] = pemParseBS bs + pure (ServiceKeyPEM k) + +withDummyTestServiceForTeam :: + forall a. + HasCallStack => + UserId -> + TeamId -> + -- | the test + (Chan (Wai.Request, LBS) -> TestM a) -> + TestM a +withDummyTestServiceForTeam owner tid go = + withDummyTestServiceForTeamNoService $ \lhPort chan -> do + newService <- newLegalHoldService lhPort + postSettings owner tid newService !!! testResponse 201 Nothing + go chan + +-- FUTUREWORK: run this test suite against an actual LH service (by changing URL and key in +-- the config file), and see if it works as well as with our mock service. +withDummyTestServiceForTeamNoService :: + forall a. + HasCallStack => + -- | the test + (Warp.Port -> Chan (Wai.Request, LBS) -> TestM a) -> + TestM a +withDummyTestServiceForTeamNoService go = do + withTestService dummyService go + where + dummyService :: Chan (Wai.Request, LBS) -> Wai.Application + dummyService ch req cont = do + reqBody <- Wai.strictRequestBody req + writeChan ch (req, reqBody) + case (pathInfo req, requestMethod req, getRequestHeader "Authorization" req) of + (["legalhold", "status"], "GET", _) -> cont respondOk + (_, _, Nothing) -> cont missingAuth + (["legalhold", "initiate"], "POST", Just _) -> cont initiateResp + (["legalhold", "confirm"], "POST", Just _) -> + cont respondOk + (["legalhold", "remove"], "POST", Just _) -> cont respondOk + _ -> cont respondBad + + initiateResp :: Wai.Response + initiateResp = + Wai.json $ + -- FUTUREWORK: use another key to prevent collisions with keys used by tests + NewLegalHoldClient somePrekeys (head $ someLastPrekeys) + + respondOk :: Wai.Response + respondOk = responseLBS status200 mempty mempty + + respondBad :: Wai.Response + respondBad = responseLBS status404 mempty mempty + + missingAuth :: Wai.Response + missingAuth = responseLBS status400 mempty "no authorization header" + + getRequestHeader :: String -> Wai.Request -> Maybe ByteString + getRequestHeader name req = lookup (fromString name) $ requestHeaders req + +-- | FUTUREWORK: this function calls an internal end-point to whitelist a team. It only +-- appears to bracket this state change and undo it in a finalizer. +-- +-- We should probably not have this function, just do the call inline, and use the 'TestM' +-- actions again rather than the polymorphic ones that we have here. +-- +-- it's here for historical reason because we did this in galley.yaml +-- at some point in the past rather than in an internal end-point, and that required spawning +-- another galley 'Application' with 'withSettingsOverrides'. +withLHWhitelist :: forall a. HasCallStack => TeamId -> TestM a -> TestM a +withLHWhitelist tid action = do + void $ putLHWhitelistTeam tid + opts <- view tsGConf + withSettingsOverrides (const opts) action + +-- | If you play with whitelists, you should use this one. Every whitelisted team that does +-- not get fully deleted will blow up the whitelist that is cached in every warp handler. +withTeam :: forall a. HasCallStack => (HasCallStack => UserId -> TeamId -> TestM a) -> TestM a +withTeam action = + bracket + createBindingTeam + (\(owner, tid) -> deleteTeam owner tid >> waitForDeleteEvent tid) + (uncurry action) + where + waitForDeleteEvent :: TeamId -> TestM () + waitForDeleteEvent tid = + assertTeamDelete 10 "waitForDeleteEvent" tid + +withFreePortAnyAddr :: (MonadMask m, MonadIO m) => ((Warp.Port, Socket) -> m a) -> m a +withFreePortAnyAddr = bracket openFreePortAnyAddr (liftIO . Socket.close . snd) + +openFreePortAnyAddr :: MonadIO m => m (Warp.Port, Socket) +openFreePortAnyAddr = liftIO $ bindRandomPortTCP "*" + +-- | Run a test with an mock legal hold service application. The mock service is also binding +-- to a TCP socket for the backend to connect to. The mock service can expose internal +-- details to the test (for both read and write) via a 'Chan'. +-- +-- WARNINGS: (1) This is not concurrency-proof! (2) tests need to be written in a way that +-- they can be run several times if they fail the first time. this is the allow for the ssl +-- service to have some time to propagate through the test system (needed on k8s). +withTestService :: + HasCallStack => + -- | the mock service + (Chan e -> Application) -> + -- | the test + (Warp.Port -> Chan e -> TestM a) -> + TestM a +withTestService mkApp go = withFreePortAnyAddr $ \(sPort, sock) -> do + config <- view (tsIConf . to provider) + let tlss = Warp.tlsSettings (cert config) (privateKey config) + let defs = Warp.defaultSettings {Warp.settingsPort = sPort} + buf <- liftIO newChan + srv <- + liftIO . Async.async $ + Warp.runTLSSocket tlss defs sock $ + mkApp buf + go sPort buf `finally` liftIO (Async.cancel srv) + +publicKeyNotMatchingService :: PEM +publicKeyNotMatchingService = + let Right [k] = + pemParseBS . BS.unlines $ + [ "-----BEGIN PUBLIC KEY-----", + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+Kg/PHHU3atXrUbKnw0", + "G06FliXcNt3lMwl2os5twEDcPPFw/feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPH", + "WvUBdiLfGrZqJO223DB6D8K2Su/odmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKV", + "VPOaOzgtAB21XKRiQ4ermqgi3/njr03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiS", + "bUKr/BeArYRcjzr/h5m1In6fG/if9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg8", + "7X883H+LA/d6X5CTiPv1VMxXdBUiGPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7la", + "nQIDAQAZ", + "-----END PUBLIC KEY-----" + ] + in k + +testGetLegalholdStatus :: TestM () +testGetLegalholdStatus = do + (owner1, tid1) <- createBindingTeam + member1 <- view Team.userId <$> addUserToTeam owner1 tid1 + + (owner2, tid2) <- createBindingTeam + member2 <- view Team.userId <$> addUserToTeam owner2 tid2 + + personal <- randomUser + + let check :: HasCallStack => UserId -> UserId -> Maybe TeamId -> UserLegalHoldStatus -> TestM () + check getter targetUser targetTeam stat = do + profile <- getUserProfile getter targetUser + when (profileLegalholdStatus profile /= stat) $ do + meminfo <- getUserStatusTyped targetUser `mapM` targetTeam + + liftIO . forM_ meminfo $ \mem -> do + assertEqual "member LH status" stat (ulhsrStatus mem) + assertEqual "team id in brig user record" targetTeam (profileTeam profile) + + liftIO $ assertEqual "user profile status info" stat (profileLegalholdStatus profile) + + requestDev :: HasCallStack => UserId -> UserId -> TeamId -> TestM () + requestDev requestor target tid = do + requestLegalHoldDevice requestor target tid !!! testResponse 201 Nothing + + approveDev :: HasCallStack => UserId -> TeamId -> TestM () + approveDev target tid = do + approveLegalHoldDevice (Just defPassword) target target tid !!! testResponse 200 Nothing + + check owner1 member1 (Just tid1) UserLegalHoldNoConsent + check member1 member1 (Just tid1) UserLegalHoldNoConsent + check owner2 member1 (Just tid1) UserLegalHoldNoConsent + check member2 member1 (Just tid1) UserLegalHoldNoConsent + check personal member1 (Just tid1) UserLegalHoldNoConsent + check owner1 personal Nothing UserLegalHoldNoConsent + check member1 personal Nothing UserLegalHoldNoConsent + check owner2 personal Nothing UserLegalHoldNoConsent + check member2 personal Nothing UserLegalHoldNoConsent + check personal personal Nothing UserLegalHoldNoConsent + + putLHWhitelistTeam tid1 !!! const 200 === statusCode + + withDummyTestServiceForTeam owner1 tid1 $ \_chan -> do + check owner1 member1 (Just tid1) UserLegalHoldDisabled + check member2 member1 (Just tid1) UserLegalHoldDisabled + check personal member1 (Just tid1) UserLegalHoldDisabled + + requestDev owner1 member1 tid1 + check personal member1 (Just tid1) UserLegalHoldPending + + approveDev member1 tid1 + check personal member1 (Just tid1) UserLegalHoldEnabled + +---------------------------------------------------------------------- +-- API helpers + +getEnabled :: HasCallStack => TeamId -> TestM ResponseLBS +getEnabled tid = do + g <- viewGalley + get $ + g + . paths ["i", "teams", toByteString' tid, "features", "legalhold"] + +renewToken :: HasCallStack => Text -> TestM () +renewToken tok = do + b <- viewBrig + void . post $ + b + . paths ["access"] + . cookieRaw "zuid" (toByteString' tok) + . expect2xx + +putEnabled :: HasCallStack => TeamId -> Public.FeatureStatus -> TestM () +putEnabled tid enabled = do + g <- viewGalley + putEnabledM g tid enabled + +putEnabledM :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyR -> TeamId -> Public.FeatureStatus -> m () +putEnabledM g tid enabled = void $ putEnabledM' g expect2xx tid enabled + +putEnabled' :: HasCallStack => (Bilge.Request -> Bilge.Request) -> TeamId -> Public.FeatureStatus -> TestM ResponseLBS +putEnabled' extra tid enabled = do + g <- viewGalley + putEnabledM' g extra tid enabled + +putEnabledM' :: (HasCallStack, MonadHttp m) => GalleyR -> (Bilge.Request -> Bilge.Request) -> TeamId -> Public.FeatureStatus -> m ResponseLBS +putEnabledM' g extra tid enabled = do + put $ + g + . paths ["i", "teams", toByteString' tid, "features", "legalhold"] + . json (Public.WithStatusNoLock enabled Public.LegalholdConfig Public.FeatureTTLUnlimited) + . extra + +postSettings :: HasCallStack => UserId -> TeamId -> NewLegalHoldService -> TestM ResponseLBS +postSettings uid tid new = + -- Retry calls to this endpoint, on k8s it sometimes takes a while to establish a working + -- connection. + retrying policy only412 $ \_ -> do + g <- viewGalley + post $ + g + . paths ["teams", toByteString' tid, "legalhold", "settings"] + . zUser uid + . zConn "conn" + . zType "access" + . json new + where + policy :: RetryPolicy + policy = exponentialBackoff 50 <> limitRetries 5 + only412 :: RetryStatus -> ResponseLBS -> TestM Bool + only412 _ resp = pure $ statusCode resp == 412 + +getSettingsTyped :: HasCallStack => UserId -> TeamId -> TestM ViewLegalHoldService +getSettingsTyped uid tid = responseJsonUnsafe <$> (getSettings uid tid UserId -> TeamId -> TestM ResponseLBS +getSettings uid tid = do + g <- viewGalley + get $ + g + . paths ["teams", toByteString' tid, "legalhold", "settings"] + . zUser uid + . zConn "conn" + . zType "access" + +deleteSettings :: HasCallStack => Maybe PlainTextPassword -> UserId -> TeamId -> TestM ResponseLBS +deleteSettings mPassword uid tid = do + g <- viewGalley + delete $ + g + . paths ["teams", toByteString' tid, "legalhold", "settings"] + . zUser uid + . zConn "conn" + . zType "access" + . json (RemoveLegalHoldSettingsRequest mPassword) + +getUserStatusTyped :: HasCallStack => UserId -> TeamId -> TestM UserLegalHoldStatusResponse +getUserStatusTyped uid tid = do + g <- viewGalley + getUserStatusTyped' g uid tid + +getUserStatusTyped' :: (HasCallStack, MonadHttp m, MonadIO m, MonadCatch m) => GalleyR -> UserId -> TeamId -> m UserLegalHoldStatusResponse +getUserStatusTyped' g uid tid = do + resp <- getUserStatus' g uid tid GalleyR -> UserId -> TeamId -> m ResponseLBS +getUserStatus' g uid tid = do + get $ + g + . paths ["teams", toByteString' tid, "legalhold", toByteString' uid] + . zUser uid + . zConn "conn" + . zType "access" + +approveLegalHoldDevice :: HasCallStack => Maybe PlainTextPassword -> UserId -> UserId -> TeamId -> TestM ResponseLBS +approveLegalHoldDevice mPassword zusr uid tid = do + g <- viewGalley + approveLegalHoldDevice' g mPassword zusr uid tid + +approveLegalHoldDevice' :: + (HasCallStack, MonadHttp m) => + GalleyR -> + Maybe PlainTextPassword -> + UserId -> + UserId -> + TeamId -> + m ResponseLBS +approveLegalHoldDevice' g mPassword zusr uid tid = do + put $ + g + . paths ["teams", toByteString' tid, "legalhold", toByteString' uid, "approve"] + . zUser zusr + . zConn "conn" + . zType "access" + . json (ApproveLegalHoldForUserRequest mPassword) + +disableLegalHoldForUser :: + HasCallStack => + Maybe PlainTextPassword -> + TeamId -> + UserId -> + UserId -> + TestM ResponseLBS +disableLegalHoldForUser mPassword tid zusr uid = do + g <- viewGalley + disableLegalHoldForUser' g mPassword tid zusr uid + +disableLegalHoldForUser' :: + (HasCallStack, MonadHttp m) => + GalleyR -> + Maybe PlainTextPassword -> + TeamId -> + UserId -> + UserId -> + m ResponseLBS +disableLegalHoldForUser' g mPassword tid zusr uid = do + delete $ + g + . paths ["teams", toByteString' tid, "legalhold", toByteString' uid] + . zUser zusr + . zType "access" + . json (DisableLegalHoldForUserRequest mPassword) + +assertExactlyOneLegalHoldDevice :: HasCallStack => UserId -> TestM () +assertExactlyOneLegalHoldDevice uid = do + clients :: [Client] <- + getClients uid >>= responseJsonError + liftIO $ do + let numdevs = length $ clientType <$> clients + assertEqual ("expected exactly one legal hold device for user: " <> show uid) numdevs 1 + +assertZeroLegalHoldDevices :: HasCallStack => UserId -> TestM () +assertZeroLegalHoldDevices uid = do + clients :: [Client] <- + getClients uid >>= responseJsonError + liftIO $ do + let numdevs = length $ clientType <$> clients + assertBool + ( "a legal hold device was found when none was expected for user" + <> show uid + ) + (numdevs == 0) + +--------------------------------------------------------------------- +--- Device helpers + +---------------------------------------------------------------------- +---- Device helpers + +grantConsent :: HasCallStack => TeamId -> UserId -> TestM () +grantConsent tid zusr = do + g <- viewGalley + grantConsent' g tid zusr + +grantConsent' :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyR -> TeamId -> UserId -> m () +grantConsent' = grantConsent'' expect2xx + +grantConsent'' :: (HasCallStack, MonadHttp m, MonadIO m) => (Bilge.Request -> Bilge.Request) -> GalleyR -> TeamId -> UserId -> m () +grantConsent'' expectation g tid zusr = do + void . post $ + g + . paths ["teams", toByteString' tid, "legalhold", "consent"] + . zUser zusr + . zConn "conn" + . zType "access" + . expectation + +requestLegalHoldDevice :: HasCallStack => UserId -> UserId -> TeamId -> TestM ResponseLBS +requestLegalHoldDevice zusr uid tid = do + g <- viewGalley + requestLegalHoldDevice' g zusr uid tid + +requestLegalHoldDevice' :: (HasCallStack, MonadHttp m) => GalleyR -> UserId -> UserId -> TeamId -> m ResponseLBS +requestLegalHoldDevice' g zusr uid tid = do + post $ + g + . paths ["teams", toByteString' tid, "legalhold", toByteString' uid] + . zUser zusr + . zConn "conn" + . zType "access" + +---------------------------------------------------------------------- +-- test helpers + +deriving instance Show Ev.Event + +deriving instance Show Ev.UserEvent + +deriving instance Show Ev.ClientEvent + +deriving instance Show Ev.PropertyEvent + +deriving instance Show Ev.ConnectionEvent + +-- (partial implementation, just good enough to make the tests work) +instance FromJSON Ev.Event where + parseJSON ev = flip (withObject "Ev.Event") ev $ \o -> do + typ :: Text <- o .: "type" + if + | typ `elem` ["user.legalhold-request", "user.legalhold-enable", "user.legalhold-disable"] -> Ev.UserEvent <$> Aeson.parseJSON ev + | typ `elem` ["user.client-add", "user.client-remove"] -> Ev.ClientEvent <$> Aeson.parseJSON ev + | typ == "user.connection" -> Ev.ConnectionEvent <$> Aeson.parseJSON ev + | otherwise -> fail $ "Ev.Event: unsupported event type: " <> show typ + +-- (partial implementation, just good enough to make the tests work) +instance FromJSON Ev.UserEvent where + parseJSON = withObject "Ev.UserEvent" $ \o -> do + tag :: Text <- o .: "type" + case tag of + "user.legalhold-enable" -> Ev.UserLegalHoldEnabled <$> o .: "id" + "user.legalhold-disable" -> Ev.UserLegalHoldDisabled <$> o .: "id" + "user.legalhold-request" -> + Ev.LegalHoldClientRequested + <$> ( Ev.LegalHoldClientRequestedData + <$> o .: "id" -- this is the target user + <*> o .: "last_prekey" + <*> (o .: "client" >>= withObject "id" (.: "id")) + ) + x -> fail $ "Ev.UserEvent: unsupported event type: " ++ show x + +-- (partial implementation, just good enough to make the tests work) +instance FromJSON Ev.ClientEvent where + parseJSON = withObject "Ev.ClientEvent" $ \o -> do + tag :: Text <- o .: "type" + case tag of + "user.client-add" -> Ev.ClientAdded fakeuid <$> o .: "client" + "user.client-remove" -> Ev.ClientRemoved fakeuid <$> (o .: "client" >>= withObject "id" (.: "id")) + x -> fail $ "Ev.ClientEvent: unsupported event type: " ++ show x + where + fakeuid = read @UserId "6980fb5e-ba64-11eb-a339-0b3625bf01be" + +instance FromJSON Ev.ConnectionEvent where + parseJSON = Aeson.withObject "ConnectionEvent" $ \o -> do + tag :: Text <- o .: "type" + case tag of + "user.connection" -> + Ev.ConnectionUpdated + <$> o .: "connection" + <*> pure Nothing + <*> pure Nothing + x -> fail $ "unspported event type: " ++ show x + +assertNotification :: (HasCallStack, FromJSON a, MonadIO m) => WS.WebSocket -> (a -> Assertion) -> m () +assertNotification ws predicate = + void . liftIO . WS.assertMatch (5 WS.# WS.Second) ws $ \notif -> do + unless ((NonEmpty.length . List1.toNonEmpty $ ntfPayload $ notif) == 1) $ + error $ + "not suppored by test helper: event with more than one object in the payload: " <> cs (Aeson.encode notif) + let j = Aeson.Object $ List1.head (ntfPayload notif) + case Aeson.fromJSON j of + Aeson.Success x -> predicate x + Aeson.Error s -> error $ s ++ " in " ++ cs (Aeson.encode j) + +assertNoNotification :: (HasCallStack, MonadIO m) => WS.WebSocket -> m () +assertNoNotification ws = void . liftIO $ WS.assertNoEvent (5 WS.# WS.Second) [ws] + +assertMatchJSON :: (HasCallStack, FromJSON a, MonadCatch m, MonadIO m) => Chan (Wai.Request, LBS) -> (a -> m ()) -> m () +assertMatchJSON c match = do + assertMatchChan c $ \(_, reqBody) -> do + case Aeson.eitherDecode reqBody of + Right x -> match x + Left s -> error $ s ++ " in " ++ cs reqBody + +assertMatchChan :: (HasCallStack, MonadCatch m, MonadIO m) => Chan a -> (a -> m ()) -> m () +assertMatchChan c match = go [] + where + refill = mapM_ (liftIO <$> writeChan c) + go buf = do + m <- liftIO . timeout (5 WS.# WS.Second) . readChan $ c + case m of + Just n -> + do + match n + refill buf + `catchAll` \e -> case asyncExceptionFromException e of + Just x -> error $ show (x :: SomeAsyncException) + Nothing -> go (n : buf) + Nothing -> do + refill buf + error "Timeout" + +getLHWhitelistedTeam :: HasCallStack => TeamId -> TestM ResponseLBS +getLHWhitelistedTeam tid = do + galley <- viewGalley + getLHWhitelistedTeam' galley tid + +getLHWhitelistedTeam' :: (HasCallStack, MonadHttp m) => GalleyR -> TeamId -> m ResponseLBS +getLHWhitelistedTeam' g tid = do + get + ( g + . paths ["i", "legalhold", "whitelisted-teams", toByteString' tid] + ) + +putLHWhitelistTeam :: HasCallStack => TeamId -> TestM ResponseLBS +putLHWhitelistTeam tid = do + galley <- viewGalley + putLHWhitelistTeam' galley tid + +putLHWhitelistTeam' :: (HasCallStack, MonadHttp m) => GalleyR -> TeamId -> m ResponseLBS +putLHWhitelistTeam' g tid = do + put + ( g + . paths ["i", "legalhold", "whitelisted-teams", toByteString' tid] + ) + +_deleteLHWhitelistTeam :: HasCallStack => TeamId -> TestM ResponseLBS +_deleteLHWhitelistTeam tid = do + galley <- viewGalley + deleteLHWhitelistTeam' galley tid + +deleteLHWhitelistTeam' :: (HasCallStack, MonadHttp m) => GalleyR -> TeamId -> m ResponseLBS +deleteLHWhitelistTeam' g tid = do + delete + ( g + . paths ["i", "legalhold", "whitelisted-teams", toByteString' tid] + ) + +errWith :: (HasCallStack, Typeable a, FromJSON a) => Int -> (a -> Bool) -> ResponseLBS -> TestM () +errWith wantStatus wantBody rsp = liftIO $ do + assertEqual "" wantStatus (statusCode rsp) + assertBool + (show $ responseBody rsp) + ( maybe False wantBody (responseJsonMaybe rsp) + ) + +------------------------------------ + +testOnlyIfLhEnabled :: IO TestSetup -> TestName -> TestM () -> TestTree +testOnlyIfLhEnabled setupAction name testAction = do + singleTest name $ LHTest FeatureLegalHoldDisabledByDefault setupAction testAction + +testOnlyIfLhWhitelisted :: IO TestSetup -> TestName -> TestM () -> TestTree +testOnlyIfLhWhitelisted setupAction name testAction = do + singleTest name $ LHTest FeatureLegalHoldWhitelistTeamsAndImplicitConsent setupAction testAction + +data LHTest = LHTest FeatureLegalHold (IO TestSetup) (TestM ()) + +instance IsTest LHTest where + run :: OptionSet -> LHTest -> (Progress -> IO ()) -> IO Result + run _ (LHTest expectedFlag setupAction testAction) _ = do + setup <- setupAction + let featureLegalHold = setup ^. tsGConf . optSettings . setFeatureFlags . flagLegalHold + if featureLegalHold == expectedFlag + then do + hunitResult <- try $ void . flip runReaderT setup . runTestM $ testAction + pure $ + case hunitResult of + Right _ -> testPassed "" + Left (HUnitFailure stack message) -> testFailed $ prependCallStack stack message + else pure . skipTest $ "test skipped because: \n " <> show expectedFlag <> " /= " <> show featureLegalHold + + testOptions :: Tagged LHTest [OptionDescription] + testOptions = pure [] + +-- | Skipped tests are to be marked as failed in tasty. See this comment for +-- details: +-- https://github.com/UnkindPartition/tasty/blob/0debac85701560e8c96cd3705988c50197cb214e/core/Test/Tasty/Core.hs#L99-L119 +skipTest :: String -> Result +skipTest reason = + Result + { resultOutcome = Success, + resultDescription = reason, + resultShortDescription = "SKIP", + resultTime = 0, + resultDetailsPrinter = noResultDetails + } + +prependCallStack :: CallStack -> String -> String +prependCallStack stack s = + "Error message: " <> s <> "\n\n" <> prettyCallStack stack + +prettyCallStack :: CallStack -> String +prettyCallStack = intercalate "\n" . prettyCallStackLines + +prettyCallStackLines :: CallStack -> [String] +prettyCallStackLines stack = case stack of + [] -> [] + stk -> + "CallStack (from HasCallStack):" + : map ((" " ++) . prettyCallSite) stk + where + prettyCallSite (f, loc) = f ++ ", called at " ++ prettySrcLoc loc + +prettySrcLoc :: SrcLoc -> String +prettySrcLoc SrcLoc {..} = + concat + [ srcLocFile, + ":", + show srcLocStartLine, + ":", + show srcLocStartCol, + " in ", + srcLocPackage, + ":", + srcLocModule + ] diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 12a928e19d..ade69830f1 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -15,6 +15,8 @@ -- 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 #-} +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} module API.Util where @@ -49,6 +51,7 @@ import qualified Data.Handle as Handle import qualified Data.HashMap.Strict as HashMap import Data.Id import Data.Json.Util hiding ((#)) +import Data.Kind import Data.LegalHold (defUserLegalHoldStatus) import Data.List.NonEmpty (NonEmpty) import Data.List1 as List1 @@ -73,13 +76,12 @@ import Data.UUID.V4 import Federator.MockServer import qualified Federator.MockServer as Mock import GHC.TypeLits (KnownSymbol) +import GHC.TypeNats import Galley.Intra.User (chunkify) import qualified Galley.Options as Opts import qualified Galley.Run as Run -import Galley.Types.Conversations.Intra import Galley.Types.Conversations.One2One import qualified Galley.Types.Teams as Team -import Galley.Types.Teams.Intra import Galley.Types.UserList import Imports import qualified Network.HTTP.Client as HTTP @@ -123,7 +125,9 @@ import Wire.API.MLS.Serialisation import Wire.API.Message import qualified Wire.API.Message.Proto as Proto import Wire.API.Routes.Internal.Brig.Connection +import Wire.API.Routes.Internal.Galley.ConversationsIntra import qualified Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti as Multi +import Wire.API.Routes.Internal.Galley.TeamsIntra import Wire.API.Routes.MultiTablePaging import Wire.API.Routes.Version import Wire.API.Team @@ -184,7 +188,7 @@ createBindingTeam' = do teams <- getTeams (userId owner) [] let [team] = view teamListTeams teams let tid = view teamId team - SQS.assertQueue "create team" SQS.tActivate + SQS.assertTeamActivate "create team" tid refreshIndex pure (owner, tid) @@ -193,7 +197,7 @@ createBindingTeamWithMembers numUsers = do (owner, tid) <- createBindingTeam members <- forM [2 .. numUsers] $ \n -> do mem <- addUserToTeam owner tid - SQS.assertQueue "add member" $ SQS.tUpdate (fromIntegral n) [owner] + SQS.assertTeamUpdate "add member" tid (fromIntegral n) [owner] -- 'refreshIndex' needs to happen here to make tests more realistic. one effect of -- refreshing the index once at the end would be that the hard member limit wouldn't hold -- any more. @@ -235,7 +239,6 @@ createBindingTeamWithNMembersWithHandles withHandles n = do addTeamMemberInternal tid member1 (Team.rolePermissions RoleMember) Nothing setHandle member1 pure member1 - SQS.ensureQueueEmpty pure (owner, tid, mems) where mkRandomHandle :: MonadIO m => m Text @@ -609,7 +612,7 @@ 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 post ( g . path "/conversations" @@ -645,14 +648,14 @@ createMLSTeamConv lusr c tid users name access role timer convRole = do newConvMessageTimer = timer, newConvUsersRole = fromMaybe roleNameWireAdmin convRole, newConvReceiptMode = Nothing, - newConvProtocol = ProtocolMLSTag, - newConvCreatorClient = Just c + newConvProtocol = ProtocolMLSTag } r <- post ( g . path "/conversations" . zUser (tUnqualified lusr) + . zClient c . zConn "conn" . zType "access" . json conv @@ -676,7 +679,7 @@ 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 post $ g . path "/conversations/one2one" . zUser u1 . zConn "conn" . zType "access" . json conv postConv :: @@ -690,25 +693,26 @@ postConv :: postConv u us name a r mtimer = postConvWithRole u us name a r mtimer roleNameWireAdmin defNewProteusConv :: NewConv -defNewProteusConv = NewConv [] [] Nothing mempty Nothing Nothing Nothing Nothing roleNameWireAdmin ProtocolProteusTag Nothing +defNewProteusConv = NewConv [] [] Nothing mempty Nothing Nothing Nothing Nothing roleNameWireAdmin ProtocolProteusTag -defNewMLSConv :: ClientId -> NewConv -defNewMLSConv c = +defNewMLSConv :: NewConv +defNewMLSConv = defNewProteusConv - { newConvProtocol = ProtocolMLSTag, - newConvCreatorClient = Just c + { newConvProtocol = ProtocolMLSTag } postConvQualified :: UserId -> + Maybe ClientId -> NewConv -> TestM ResponseLBS -postConvQualified u n = do +postConvQualified u c n = do g <- viewGalley post $ g . path "/conversations" . zUser u + . maybe id zClient c . zConn "conn" . zType "access" . json n @@ -716,23 +720,24 @@ postConvQualified u n = do postConvWithRemoteUsers :: HasCallStack => UserId -> + Maybe ClientId -> NewConv -> TestM (Response (Maybe LByteString)) -postConvWithRemoteUsers u n = +postConvWithRemoteUsers u c n = fmap fst $ withTempMockFederator' (mockReply ()) $ - postConvQualified u n {newConvName = setName (newConvName n)} + postConvQualified u c n {newConvName = setName (newConvName n)} Maybe (Range n m Text) -> Maybe (Range n m Text) + setName :: (KnownNat n, KnownNat m, Within Text n m) => Maybe (Range n m Text) -> Maybe (Range n m Text) setName Nothing = checked "federated gossip" setName x = x postTeamConv :: TeamId -> UserId -> [UserId] -> Maybe Text -> [Access] -> Maybe (Set AccessRole) -> 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 post $ g . path "/conversations" . zUser u . zConn "conn" . zType "access" . json conv deleteTeamConv :: (HasGalley m, MonadIO m, MonadHttp m) => TeamId -> ConvId -> UserId -> m ResponseLBS @@ -757,6 +762,7 @@ postConvWithRole :: postConvWithRole u members name access arole timer role = postConvQualified u + Nothing defNewProteusConv { newConvUsers = members, newConvName = name >>= checked, @@ -769,7 +775,7 @@ postConvWithRole u members name access arole timer role = postConvWithReceipt :: UserId -> [UserId] -> Maybe Text -> [Access] -> Maybe (Set AccessRole) -> 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 post $ g . path "/conversations" . zUser u . zConn "conn" . zType "access" . json conv postSelfConv :: UserId -> TestM ResponseLBS @@ -780,7 +786,7 @@ 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 post $ g . path "/conversations/one2one" . zUser u1 . zConn "conn" . zType "access" . json conv postConnectConv :: UserId -> UserId -> Text -> Text -> Maybe Text -> TestM ResponseLBS @@ -954,7 +960,7 @@ mkOtrPayload sender rec reportMissingBody ad = mkOtrMessage :: (UserId, ClientId, Text) -> (Text, HashMap.HashMap Text Text) mkOtrMessage (usr, clt, m) = (fn usr, HashMap.singleton (fn clt) m) where - fn :: (FromByteString a, ToByteString a) => a -> Text + fn :: ToByteString a => a -> Text fn = fromJust . fromByteString . toByteString' postProtoOtrMessage :: UserId -> ClientId -> ConvId -> OtrRecipients -> TestM ResponseLBS @@ -1051,10 +1057,8 @@ getConv u c = do . zType "access" getConvQualifiedV2 :: - ( Monad m, - MonadReader TestSetup m, - MonadHttp m, - MonadIO m + ( MonadReader TestSetup m, + MonadHttp m ) => UserId -> Qualified ConvId -> @@ -1129,7 +1133,7 @@ listRemoteConvs remoteDomain uid = do pure $ filter (\qcnv -> qDomain qcnv == remoteDomain) allConvs postQualifiedMembers :: - (MonadReader TestSetup m, MonadIO m, MonadHttp m) => + (MonadReader TestSetup m, MonadHttp m) => UserId -> NonEmpty (Qualified UserId) -> ConvId -> @@ -1255,7 +1259,7 @@ putOtherMember from to m c = do . json m putQualifiedConversationName :: - (HasCallStack, HasGalley m, MonadIO m, MonadHttp m, MonadMask m) => + (HasCallStack, HasGalley m, MonadIO m, MonadHttp m) => UserId -> Qualified ConvId -> Text -> @@ -1439,7 +1443,7 @@ deleteConvCode u c = do . zConn "conn" . zType "access" -deleteUser :: (MonadIO m, MonadCatch m, MonadHttp m, HasGalley m, HasCallStack) => UserId -> m ResponseLBS +deleteUser :: (MonadIO m, MonadHttp m, HasGalley m, HasCallStack) => UserId -> m ResponseLBS deleteUser u = do g <- viewGalley delete (g . path "/i/user" . zUser u) @@ -1510,7 +1514,7 @@ registerRemoteConv convId originUser name othMembers = do ccProtocol = ProtocolProteus } -getFeatureStatusMulti :: forall cfg. (IsFeatureConfig cfg, KnownSymbol (FeatureSymbol cfg)) => Multi.TeamFeatureNoConfigMultiRequest -> TestM ResponseLBS +getFeatureStatusMulti :: forall cfg. KnownSymbol (FeatureSymbol cfg) => Multi.TeamFeatureNoConfigMultiRequest -> TestM ResponseLBS getFeatureStatusMulti req = do g <- viewGalley post @@ -2452,7 +2456,7 @@ waitForMemberDeletion zusr tid uid = do res <- get (galley . paths ["teams", toByteString' tid, "members", toByteString' uid] . zUser zusr) case statusCode res of 404 -> pure () - _ -> loop + _ -> threadDelay 1000 >> loop deleteTeamMember :: (MonadIO m, MonadCatch m, MonadHttp m) => (Request -> Request) -> TeamId -> UserId -> UserId -> m () deleteTeamMember g tid owner deletee = @@ -2589,7 +2593,7 @@ withTempMockFederator' resp action = do -- Starts a servant Application in Network.Wai.Test session and runs the -- FederatedRequest against it. makeFedRequestToServant :: - forall (api :: *). + forall (api :: Type). HasServer api '[] => Domain -> Server api -> @@ -2740,7 +2744,7 @@ checkConvMemberLeaveEvent cid usr w = WS.assertMatch_ checkTimeout w $ \notif -> other -> assertFailure $ "Unexpected event data: " <> show other checkTimeout :: WS.Timeout -checkTimeout = 3 # Second +checkTimeout = 4 # Second -- | The function is used in conjuction with 'withTempMockFederator' to mock -- responses by Brig on the mocked side of federation. diff --git a/services/galley/test/integration/API/Util/TeamFeature.hs b/services/galley/test/integration/API/Util/TeamFeature.hs index b29008f857..402393de88 100644 --- a/services/galley/test/integration/API/Util/TeamFeature.hs +++ b/services/galley/test/integration/API/Util/TeamFeature.hs @@ -1,3 +1,6 @@ +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -33,17 +36,16 @@ import Galley.Options (optSettings, setFeatureFlags) import Galley.Types.Teams import Imports import TestSetup -import Wire.API.Team.Feature (IsFeatureConfig) import qualified Wire.API.Team.Feature as Public withCustomSearchFeature :: FeatureTeamSearchVisibilityAvailability -> TestM () -> TestM () withCustomSearchFeature flag action = do Util.withSettingsOverrides (\opts -> opts & optSettings . setFeatureFlags . flagTeamSearchVisibility .~ flag) action -getTeamSearchVisibilityAvailable :: HasCallStack => (Request -> Request) -> UserId -> TeamId -> (MonadIO m, MonadHttp m) => m ResponseLBS +getTeamSearchVisibilityAvailable :: HasCallStack => (Request -> Request) -> UserId -> TeamId -> MonadHttp m => m ResponseLBS getTeamSearchVisibilityAvailable = getTeamFeatureFlagWithGalley @Public.SearchVisibilityAvailableConfig -getTeamSearchVisibilityAvailableInternal :: HasCallStack => (Request -> Request) -> TeamId -> (MonadIO m, MonadHttp m) => m ResponseLBS +getTeamSearchVisibilityAvailableInternal :: HasCallStack => (Request -> Request) -> TeamId -> MonadHttp m => m ResponseLBS getTeamSearchVisibilityAvailableInternal = getTeamFeatureFlagInternalWithGalley @Public.SearchVisibilityAvailableConfig @@ -64,14 +66,14 @@ putTeamSearchVisibilityAvailableInternal g tid statusValue = getTeamFeatureFlagInternal :: forall cfg m. - (HasGalley m, MonadIO m, MonadHttp m, IsFeatureConfig cfg, KnownSymbol (Public.FeatureSymbol cfg)) => + (HasGalley m, MonadIO m, MonadHttp m, KnownSymbol (Public.FeatureSymbol cfg)) => TeamId -> m ResponseLBS getTeamFeatureFlagInternal tid = do g <- viewGalley getTeamFeatureFlagInternalWithGalley @cfg g tid -getTeamFeatureFlagInternalWithGalley :: forall cfg m. (MonadIO m, MonadHttp m, HasCallStack, IsFeatureConfig cfg, KnownSymbol (Public.FeatureSymbol cfg)) => (Request -> Request) -> TeamId -> m ResponseLBS +getTeamFeatureFlagInternalWithGalley :: forall cfg m. (MonadHttp m, HasCallStack, KnownSymbol (Public.FeatureSymbol cfg)) => (Request -> Request) -> TeamId -> m ResponseLBS getTeamFeatureFlagInternalWithGalley g tid = do get $ g @@ -79,7 +81,7 @@ getTeamFeatureFlagInternalWithGalley g tid = do getTeamFeatureFlag :: forall cfg m. - (HasGalley m, MonadIO m, MonadHttp m, HasCallStack, IsFeatureConfig cfg, KnownSymbol (Public.FeatureSymbol cfg)) => + (HasGalley m, MonadIO m, MonadHttp m, HasCallStack, KnownSymbol (Public.FeatureSymbol cfg)) => UserId -> TeamId -> m ResponseLBS @@ -110,14 +112,14 @@ getAllTeamFeaturesPersonal uid = do . paths ["feature-configs"] . zUser uid -getTeamFeatureFlagWithGalley :: forall cfg m. (MonadIO m, MonadHttp m, HasCallStack, IsFeatureConfig cfg, KnownSymbol (Public.FeatureSymbol cfg)) => (Request -> Request) -> UserId -> TeamId -> m ResponseLBS +getTeamFeatureFlagWithGalley :: forall cfg m. (MonadHttp m, HasCallStack, KnownSymbol (Public.FeatureSymbol cfg)) => (Request -> Request) -> UserId -> TeamId -> m ResponseLBS getTeamFeatureFlagWithGalley galley uid tid = do get $ galley . paths ["teams", toByteString' tid, "features", Public.featureNameBS @cfg] . zUser uid -getFeatureConfig :: forall cfg m. (HasCallStack, MonadThrow m, HasGalley m, MonadIO m, MonadHttp m, IsFeatureConfig cfg, KnownSymbol (Public.FeatureSymbol cfg), FromJSON (Public.WithStatus cfg)) => UserId -> m (Public.WithStatus cfg) +getFeatureConfig :: forall cfg m. (HasCallStack, MonadThrow m, HasGalley m, MonadHttp m, KnownSymbol (Public.FeatureSymbol cfg), FromJSON (Public.WithStatus cfg)) => UserId -> m (Public.WithStatus cfg) getFeatureConfig uid = do galley <- viewGalley response :: Value <- responseJsonError =<< getAllFeatureConfigsWithGalley galley uid @@ -133,7 +135,7 @@ getAllFeatureConfigs uid = do g <- viewGalley getAllFeatureConfigsWithGalley g uid -getAllFeatureConfigsWithGalley :: (MonadIO m, MonadHttp m, HasCallStack) => (Request -> Request) -> UserId -> m ResponseLBS +getAllFeatureConfigsWithGalley :: (MonadHttp m, HasCallStack) => (Request -> Request) -> UserId -> m ResponseLBS getAllFeatureConfigsWithGalley galley uid = do get $ galley @@ -143,7 +145,6 @@ getAllFeatureConfigsWithGalley galley uid = do putTeamFeatureFlagWithGalley :: forall cfg. ( HasCallStack, - Public.IsFeatureConfig cfg, KnownSymbol (Public.FeatureSymbol cfg), ToJSON (Public.WithStatusNoLock cfg) ) => @@ -162,7 +163,6 @@ putTeamFeatureFlagWithGalley galley uid tid status = putTeamFeatureFlagInternalTTL :: forall cfg. ( HasCallStack, - Typeable cfg, Public.IsFeatureConfig cfg, KnownSymbol (Public.FeatureSymbol cfg), ToSchema cfg @@ -178,7 +178,6 @@ putTeamFeatureFlagInternalTTL reqmod tid status = do putTeamFeatureFlagInternal :: forall cfg. ( HasCallStack, - Public.IsFeatureConfig cfg, KnownSymbol (Public.FeatureSymbol cfg), ToJSON (Public.WithStatusNoLock cfg) ) => @@ -192,10 +191,8 @@ putTeamFeatureFlagInternal reqmod tid status = do putTeamFeatureFlagInternalWithGalleyAndMod :: forall cfg m. - ( MonadIO m, - MonadHttp m, + ( MonadHttp m, HasCallStack, - Public.IsFeatureConfig cfg, KnownSymbol (Public.FeatureSymbol cfg), ToJSON (Public.WithStatusNoLock cfg) ) => @@ -214,9 +211,7 @@ putTeamFeatureFlagInternalWithGalleyAndMod galley reqmod tid status = setLockStatusInternal :: forall cfg. ( HasCallStack, - Public.IsFeatureConfig cfg, - KnownSymbol (Public.FeatureSymbol cfg), - ToJSON Public.LockStatus + KnownSymbol (Public.FeatureSymbol cfg) ) => (Request -> Request) -> TeamId -> @@ -232,9 +227,7 @@ setLockStatusInternal reqmod tid lockStatus = do getFeatureStatusInternal :: forall cfg. ( HasCallStack, - Public.IsFeatureConfig cfg, - KnownSymbol (Public.FeatureSymbol cfg), - ToJSON Public.LockStatus + KnownSymbol (Public.FeatureSymbol cfg) ) => TeamId -> TestM ResponseLBS @@ -247,9 +240,7 @@ getFeatureStatusInternal tid = do patchFeatureStatusInternal :: forall cfg. ( HasCallStack, - Public.IsFeatureConfig cfg, KnownSymbol (Public.FeatureSymbol cfg), - ToJSON Public.LockStatus, ToSchema cfg ) => TeamId -> @@ -265,9 +256,7 @@ patchFeatureStatusInternal tid reqBody = do patchFeatureStatusInternalWithMod :: forall cfg. ( HasCallStack, - Public.IsFeatureConfig cfg, KnownSymbol (Public.FeatureSymbol cfg), - ToJSON Public.LockStatus, ToSchema cfg ) => (Request -> Request) -> diff --git a/services/galley/test/integration/Main.hs b/services/galley/test/integration/Main.hs index 6410cc9fb8..c67d355dfa 100644 --- a/services/galley/test/integration/Main.hs +++ b/services/galley/test/integration/Main.hs @@ -35,6 +35,7 @@ import Data.Text (pack) import Data.Text.Encoding (encodeUtf8) import Data.Yaml (decodeFileEither) import Galley.API (sitemap) +import qualified Galley.Aws as Aws import Galley.Options import Imports hiding (local) import Network.HTTP.Client (responseTimeoutMicro) @@ -50,6 +51,7 @@ import TestSetup import Util.Options import Util.Options.Common import Util.Test +import qualified Util.Test.SQS as SQS newtype ServiceConfigFile = ServiceConfigFile String deriving (Eq, Ord, Typeable) @@ -111,14 +113,14 @@ main = withOpenSSL $ runTests go e <- join <$> optOrEnvSafe endpoint gConf (fromByteString . BS.pack) "GALLEY_SQS_ENDPOINT" convMaxSize <- optOrEnv maxSize gConf read "CONV_MAX_SIZE" awsEnv <- initAwsEnv e q - SQS.ensureQueueEmptyIO awsEnv -- Initialize cassandra let ch = fromJust gConf ^. optCassandra . casEndpoint . epHost let cp = fromJust gConf ^. optCassandra . casEndpoint . epPort let ck = fromJust gConf ^. optCassandra . casKeyspace lg <- Logger.new Logger.defSettings db <- defInitCassandra ck ch cp lg - pure $ TestSetup (fromJust gConf) (fromJust iConf) m g b c awsEnv convMaxSize db (FedClient m galleyEndpoint) + teamEventWatcher <- sequence $ SQS.watchSQSQueue <$> ((^. Aws.awsEnv) <$> awsEnv) <*> q + pure $ TestSetup (fromJust gConf) (fromJust iConf) m g b c awsEnv convMaxSize db (FedClient m galleyEndpoint) teamEventWatcher queueName = fmap (view awsQueueName) . view optJournal endpoint = fmap (view awsEndpoint) . view optJournal maxSize = view (optSettings . setMaxConvSize) diff --git a/services/galley/test/integration/TestHelpers.hs b/services/galley/test/integration/TestHelpers.hs index 88b35c4e77..25def87f49 100644 --- a/services/galley/test/integration/TestHelpers.hs +++ b/services/galley/test/integration/TestHelpers.hs @@ -19,41 +19,24 @@ module TestHelpers where -import API.SQS import Control.Lens (view) import Control.Monad.Catch (MonadMask) import Control.Retry import Data.Domain (Domain) import Data.Qualified -import qualified Galley.Aws as Aws import Galley.Options (optSettings, setFederationDomain) import Imports import Test.Tasty (TestName, TestTree) -import Test.Tasty.HUnit (Assertion, assertBool, testCase) +import Test.Tasty.HUnit (Assertion, testCase) import TestSetup -import UnliftIO.Exception (finally) test :: IO TestSetup -> TestName -> TestM a -> TestTree test s n h = testCase n runTest where - assertClean :: TestM () - assertClean = do - awsEnv <- fromJust <$> view tsAwsEnv - msgs <- liftIO $ Aws.execute awsEnv readAllUntilEmpty - liftIO $ - assertBool - ( "Found " - <> show (length msgs) - <> " messages left on queue:\n" - <> show msgs - ) - (null msgs) runTest :: Assertion runTest = do setup <- s - -- this `finally` doesnt seem to help. if there is an exception in a test - -- the other tests still see remaining messages on the queue - void . flip runReaderT setup . runTestM $ (ensureQueueEmpty >> h) `finally` assertClean + void . flip runReaderT setup . runTestM $ h viewFederationDomain :: TestM Domain viewFederationDomain = view (tsGConf . optSettings . setFederationDomain) diff --git a/services/galley/test/integration/TestSetup.hs b/services/galley/test/integration/TestSetup.hs index 9714e98fc4..20b1527d75 100644 --- a/services/galley/test/integration/TestSetup.hs +++ b/services/galley/test/integration/TestSetup.hs @@ -1,5 +1,7 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE TemplateHaskell #-} +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} {-# OPTIONS_GHC -fprint-potential-instances #-} -- This file is part of the Wire Server implementation. @@ -32,6 +34,7 @@ module TestSetup tsMaxConvSize, tsCass, tsFedGalleyClient, + tsTeamEventWatcher, TestM (..), TestSetup (..), FedClient (..), @@ -56,10 +59,12 @@ import qualified Galley.Aws as Aws import Galley.Options (Opts) import Imports import qualified Network.HTTP.Client as HTTP +import Proto.TeamEvents (TeamEvent) import qualified Servant.Client as Servant import qualified Servant.Client.Core as Servant import Test.Tasty.HUnit import Util.Options +import qualified Util.Test.SQS as SQS import Wire.API.Federation.API import Wire.API.Federation.Domain @@ -85,8 +90,7 @@ data LegalHoldConfig = LegalHoldConfig { privateKey :: FilePath, publicKey :: FilePath, cert :: FilePath, - botHost :: Text, - botPort :: Int + botHost :: Text } deriving (Show, Generic) @@ -118,7 +122,8 @@ data TestSetup = TestSetup _tsAwsEnv :: Maybe Aws.Env, _tsMaxConvSize :: Word16, _tsCass :: Cql.ClientState, - _tsFedGalleyClient :: FedClient 'Galley + _tsFedGalleyClient :: FedClient 'Galley, + _tsTeamEventWatcher :: Maybe (SQS.SQSWatcher TeamEvent) } makeLenses ''TestSetup diff --git a/services/galley/test/unit/Test/Galley/Intra/User.hs b/services/galley/test/unit/Test/Galley/Intra/User.hs index c6bec86487..42320121c6 100644 --- a/services/galley/test/unit/Test/Galley/Intra/User.hs +++ b/services/galley/test/unit/Test/Galley/Intra/User.hs @@ -1,5 +1,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- diff --git a/services/gundeck/default.nix b/services/gundeck/default.nix index 917012e8fe..bb13675e03 100644 --- a/services/gundeck/default.nix +++ b/services/gundeck/default.nix @@ -62,7 +62,6 @@ , servant-swagger-ui , streaming-commons , string-conversions -, swagger , swagger2 , tagged , tasty @@ -140,7 +139,6 @@ mkDerivation { servant-server servant-swagger servant-swagger-ui - swagger swagger2 text time diff --git a/services/gundeck/src/Main.hs b/services/gundeck/exec/Main.hs similarity index 100% rename from services/gundeck/src/Main.hs rename to services/gundeck/exec/Main.hs diff --git a/services/gundeck/gundeck.cabal b/services/gundeck/gundeck.cabal index 1a079e716f..598ad7ad60 100644 --- a/services/gundeck/gundeck.cabal +++ b/services/gundeck/gundeck.cabal @@ -47,7 +47,6 @@ library Gundeck.Util Gundeck.Util.DelayQueue Gundeck.Util.Redis - Main other-modules: Paths_gundeck hs-source-dirs: src @@ -94,7 +93,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -fwarn-incomplete-uni-patterns + -Wredundant-constraints build-depends: aeson >=2.0.1.0 @@ -139,7 +138,6 @@ library , servant-server , servant-swagger , servant-swagger-ui - , swagger , swagger2 , text >=1.1 , time >=1.4 @@ -162,7 +160,7 @@ library default-language: Haskell2010 executable gundeck - main-is: src/Main.hs + main-is: exec/Main.hs other-modules: Paths_gundeck default-extensions: NoImplicitPrelude @@ -207,7 +205,7 @@ executable gundeck ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -rtsopts -with-rtsopts=-T + -threaded -rtsopts -with-rtsopts=-T -Wredundant-constraints build-depends: base @@ -279,7 +277,7 @@ executable gundeck-integration ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded + -threaded -Wredundant-constraints build-depends: aeson @@ -389,7 +387,7 @@ executable gundeck-schema ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded + -threaded -Wredundant-constraints build-depends: base @@ -464,7 +462,7 @@ test-suite gundeck-tests ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded + -threaded -Wredundant-constraints build-depends: aeson @@ -554,6 +552,7 @@ benchmark gundeck-bench ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: aeson diff --git a/services/gundeck/gundeck.integration.yaml b/services/gundeck/gundeck.integration.yaml index 5457469af0..1d56cfdf88 100644 --- a/services/gundeck/gundeck.integration.yaml +++ b/services/gundeck/gundeck.integration.yaml @@ -37,5 +37,5 @@ settings: hard: 30 # more than this number of threads will not be allowed soft: 10 # more than this number of threads will be warned about -logLevel: Info +logLevel: Warn logNetStrings: false diff --git a/services/gundeck/src/Gundeck/API.hs b/services/gundeck/src/Gundeck/API.hs index c831a15be5..21bf6c72c7 100644 --- a/services/gundeck/src/Gundeck/API.hs +++ b/services/gundeck/src/Gundeck/API.hs @@ -20,11 +20,10 @@ module Gundeck.API ) where -import qualified Data.Swagger.Build.Api as Doc import qualified Gundeck.API.Internal as Internal import Gundeck.Monad (Gundeck) import Network.Wai.Routing (Routes) -sitemap :: Routes Doc.ApiBuilder Gundeck () +sitemap :: Routes () Gundeck () sitemap = do Internal.sitemap diff --git a/services/gundeck/src/Gundeck/API/Public.hs b/services/gundeck/src/Gundeck/API/Public.hs index 0ee9b83d7f..b84fe880ee 100644 --- a/services/gundeck/src/Gundeck/API/Public.hs +++ b/services/gundeck/src/Gundeck/API/Public.hs @@ -81,7 +81,7 @@ servantSitemap = pushAPI :<|> notificationAPI -- We will return all the notifications that we have that happened after 'since' -- but return status code 404 to signal that 'since' itself was missing. -- --- (arianvp): I am not sure why it is convenient for clients to distinct +-- (arianvp): I am not sure why it is convenient for clients to distinguish -- between these two cases. paginateUntilV2 :: UserId -> diff --git a/services/gundeck/src/Gundeck/Aws.hs b/services/gundeck/src/Gundeck/Aws.hs index ab9f7e839d..b34f3bf749 100644 --- a/services/gundeck/src/Gundeck/Aws.hs +++ b/services/gundeck/src/Gundeck/Aws.hs @@ -66,10 +66,10 @@ import Control.Error hiding (err, isRight) import Control.Lens hiding ((.=)) import Control.Monad.Catch import Control.Monad.Trans.Resource -import Control.Retry (limitRetries, retrying) +import Control.Retry import Data.Aeson (decodeStrict) import Data.Attoparsec.Text -import Data.ByteString.Builder (toLazyByteString) +import Data.ByteString.Builder import qualified Data.HashMap.Strict as Map import Data.Id import qualified Data.Set as Set @@ -77,13 +77,13 @@ import qualified Data.Text as Text import qualified Data.Text.Encoding as Text import qualified Data.Text.Lazy as LT import Gundeck.Aws.Arn -import Gundeck.Aws.Sns (Event, evEndpoint, evType) +import Gundeck.Aws.Sns import Gundeck.Instances () import Gundeck.Options -import Gundeck.Types.Push (AppName (..), Token, Transport (..)) +import Gundeck.Types.Push hiding (token) import qualified Gundeck.Types.Push as Push import Imports -import Network.HTTP.Client (HttpException (..), HttpExceptionContent (..), Manager) +import Network.HTTP.Client import Network.HTTP.Types import qualified Network.TLS as TLS import qualified System.Logger as Logger @@ -236,7 +236,7 @@ updateEndpoint :: Set UserId -> Token -> EndpointArn -> Amazon () updateEndpoint us tk arn = do let req = over SNS.setEndpointAttributes_attributes fun (SNS.newSetEndpointAttributes (toText arn)) env <- ask - res <- retrying (limitRetries 1) (const isTimeout) (const (sendCatch (env ^. awsEnv) req)) + res <- retry 1 (const (sendCatch (env ^. awsEnv) req)) case res of Right _ -> pure () Left x@(AWS.ServiceError e) @@ -265,7 +265,7 @@ updateEndpoint us tk arn = do deleteEndpoint :: EndpointArn -> Amazon () deleteEndpoint arn = do e <- view awsEnv - res <- retrying (limitRetries 1) (const isTimeout) (const (sendCatch e req)) + res <- retry 1 (const (sendCatch e req)) either (throwM . GeneralError) (const (pure ())) res where req = SNS.newDeleteEndpoint (toText arn) @@ -273,7 +273,7 @@ deleteEndpoint arn = do lookupEndpoint :: EndpointArn -> Amazon (Maybe SNSEndpoint) lookupEndpoint arn = do e <- view awsEnv - res <- retrying (limitRetries 1) (const isTimeout) (const (sendCatch e req)) + res <- retry 1 (const (sendCatch e req)) let attrs = fromMaybe mempty . view SNS.getEndpointAttributesResponse_attributes <$> res case attrs of Right a -> Just <$> mkEndpoint a @@ -297,7 +297,7 @@ createEndpoint u tr arnEnv app token = do SNS.newCreatePlatformEndpoint (toText arn) tkn & set SNS.createPlatformEndpoint_customUserData (Just (toText u)) & set SNS.createPlatformEndpoint_attributes (Just $ Map.insert "Enabled" "true" Map.empty) - res <- retrying (limitRetries 2) (const isTimeout) (const (sendCatch (env ^. awsEnv) req)) + res <- retry 2 (const (sendCatch (env ^. awsEnv) req)) case res of Right r -> case view SNS.createPlatformEndpointResponse_endpointArn r of @@ -406,7 +406,7 @@ publish arn txt attrs = do & SNS.publish_messageStructure ?~ "json" & SNS.publish_messageAttributes ?~ appEndo (setAttributes attrs) Map.empty env <- ask - res <- retrying (limitRetries 3) (const isTimeout) (const (sendCatch (env ^. awsEnv) req)) + res <- retry 3 (const (sendCatch (env ^. awsEnv) req)) case res of Right _ -> pure (Right ()) Left x@(AWS.ServiceError e) @@ -492,8 +492,15 @@ is :: AWS.Abbrev -> Int -> AWS.Error -> Bool is srv s (AWS.ServiceError e) = srv == e ^. serviceError_abbrev && s == statusCode (e ^. serviceError_status) is _ _ _ = False -isTimeout :: MonadIO m => Either AWS.Error a -> m Bool -isTimeout (Right _) = pure False -isTimeout (Left e) = case e of - AWS.TransportError (HttpExceptionRequest _ ResponseTimeout) -> pure True - _ -> pure False +retry :: Int -> (RetryStatus -> Amazon (Either AWS.Error a)) -> Amazon (Either AWS.Error a) +retry n = + retrying + (exponentialBackoff 50000 <> limitRetries n) + (const $ \x -> pure $ isTimeout x || isRateLimited x) + where + isTimeout :: Either AWS.Error a -> Bool + isTimeout (Left (AWS.TransportError (HttpExceptionRequest _ ResponseTimeout))) = True + isTimeout _ = False + isRateLimited :: Either AWS.Error a -> Bool + isRateLimited (Left (AWS.TransportError (HttpExceptionRequest _ (StatusCodeException r _)))) = responseStatus r == status429 + isRateLimited _ = False diff --git a/services/gundeck/src/Gundeck/Options.hs b/services/gundeck/src/Gundeck/Options.hs index 95eb235f41..8e23457e9d 100644 --- a/services/gundeck/src/Gundeck/Options.hs +++ b/services/gundeck/src/Gundeck/Options.hs @@ -87,10 +87,6 @@ data MaxConcurrentNativePushes = MaxConcurrentNativePushes } deriving (Show, Generic) -deriveFromJSON toOptionFieldName ''Settings - -makeLenses ''Settings - deriveFromJSON toOptionFieldName ''MaxConcurrentNativePushes makeLenses ''MaxConcurrentNativePushes @@ -113,6 +109,10 @@ deriveFromJSON toOptionFieldName ''RedisEndpoint makeLenses ''RedisEndpoint +makeLenses ''Settings + +deriveFromJSON toOptionFieldName ''Settings + data Opts = Opts { -- | Hostname and port to bind to _optGundeck :: !Endpoint, diff --git a/services/gundeck/src/Gundeck/Presence/Data.hs b/services/gundeck/src/Gundeck/Presence/Data.hs index 2bd9aa2078..7b74482a8f 100644 --- a/services/gundeck/src/Gundeck/Presence/Data.hs +++ b/services/gundeck/src/Gundeck/Presence/Data.hs @@ -73,7 +73,7 @@ add p = do where maxIdleTime = 7 * 24 * 60 * 60 -- 7 days in seconds -deleteAll :: (MonadRedis m, MonadMask m, MonadThrow m, MonadIO m, RedisCtx m (Either Reply), MonadLogger m) => [Presence] -> m () +deleteAll :: (MonadMask m, MonadIO m, RedisCtx m (Either Reply), MonadLogger m) => [Presence] -> m () deleteAll [] = pure () deleteAll pp = for_ pp $ \p -> do let k = toKey (userId p) diff --git a/services/gundeck/src/Gundeck/Push.hs b/services/gundeck/src/Gundeck/Push.hs index f863ff57a0..225cd08934 100644 --- a/services/gundeck/src/Gundeck/Push.hs +++ b/services/gundeck/src/Gundeck/Push.hs @@ -299,18 +299,21 @@ compilePushResps notifIdMap (Map.fromList -> deliveries) = -- | Is 'PushTarget' the origin of the 'Push', or is missing in a non-empty whitelist? (Whitelists -- reside both in 'Push' itself and in each 'Recipient'). shouldActuallyPush :: Push -> Recipient -> Presence -> Bool -shouldActuallyPush psh rcp pres = not isOrigin && okByPushWhitelist && okByRecipientWhitelist +shouldActuallyPush psh rcp pres = not isOrigin && okByPushAllowlist && okByRecipientAllowlist where isOrigin = psh ^. pushOrigin == Just (userId pres) && psh ^. pushOriginConnection == Just (connId pres) - okByPushWhitelist = not whitelistExists || isWhitelisted + + okByPushAllowlist :: Bool + okByPushAllowlist = not allowlistExists || isAllowlisted where - whitelist = psh ^. pushConnections - whitelistExists = not $ Set.null whitelist - isWhitelisted = connId pres `Set.member` whitelist - okByRecipientWhitelist :: Bool - okByRecipientWhitelist = + allowlist = psh ^. pushConnections + allowlistExists = not $ Set.null allowlist + isAllowlisted = connId pres `Set.member` allowlist + + okByRecipientAllowlist :: Bool + okByRecipientAllowlist = case (rcp ^. recipientClients, clientId pres) of (RecipientClientsSome cs, Just c) -> c `elem` cs _ -> True diff --git a/services/gundeck/src/Gundeck/Push/Data.hs b/services/gundeck/src/Gundeck/Push/Data.hs index f8bc4b8c3c..6437e2466e 100644 --- a/services/gundeck/src/Gundeck/Push/Data.hs +++ b/services/gundeck/src/Gundeck/Push/Data.hs @@ -18,7 +18,7 @@ module Gundeck.Push.Data ( insert, delete, - Gundeck.Push.Data.lookup, + lookup, erase, Consistency (..), ) @@ -30,7 +30,7 @@ import Data.Id (ClientId, ConnId, UserId) import Gundeck.Instances () import Gundeck.Push.Native.Types import Gundeck.Types -import Imports +import Imports hiding (lookup) import System.Logger.Class (MonadLogger, field, msg, val, (~~)) import qualified System.Logger.Class as Log diff --git a/services/gundeck/src/Gundeck/Push/Native.hs b/services/gundeck/src/Gundeck/Push/Native.hs index 96b602c634..c48fabd469 100644 --- a/services/gundeck/src/Gundeck/Push/Native.hs +++ b/services/gundeck/src/Gundeck/Push/Native.hs @@ -119,7 +119,7 @@ publish m a = flip catches pushException $ do let ept = a ^. addrEndpoint uid = a ^. addrUser transp = a ^. addrTransport - txt <- liftIO $ serialise m uid transp + txt = serialise m uid transp Log.debug $ field "user" (toByteString (a ^. addrUser)) ~~ field "arn" (toText (a ^. addrEndpoint)) diff --git a/services/gundeck/src/Gundeck/Push/Native/Serialise.hs b/services/gundeck/src/Gundeck/Push/Native/Serialise.hs index 8e070222f5..38834651ad 100644 --- a/services/gundeck/src/Gundeck/Push/Native/Serialise.hs +++ b/services/gundeck/src/Gundeck/Push/Native/Serialise.hs @@ -1,3 +1,6 @@ +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -32,11 +35,11 @@ import Gundeck.Push.Native.Types import Gundeck.Types import Imports -serialise :: HasCallStack => NativePush -> UserId -> Transport -> IO (Either Failure LT.Text) +serialise :: HasCallStack => NativePush -> UserId -> Transport -> Either Failure LT.Text serialise (NativePush nid prio _aps) uid transport = do case renderText transport prio o of - Nothing -> pure $ Left PayloadTooLarge - Just txt -> pure $ Right txt + Nothing -> Left PayloadTooLarge + Just txt -> Right txt where o = object diff --git a/services/gundeck/src/Gundeck/Push/Websocket.hs b/services/gundeck/src/Gundeck/Push/Websocket.hs index 61b8ab3490..660ca2515f 100644 --- a/services/gundeck/src/Gundeck/Push/Websocket.hs +++ b/services/gundeck/src/Gundeck/Push/Websocket.hs @@ -28,7 +28,7 @@ import Bilge.Retry (rpcHandlers) import Control.Arrow ((&&&)) import Control.Exception (ErrorCall (ErrorCall)) import Control.Lens (view, (%~), (^.), _2) -import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow, catch, throwM, try) +import Control.Monad.Catch (MonadMask, MonadThrow, catch, throwM, try) import Control.Retry import Data.Aeson (eitherDecode, encode) import Data.ByteString.Conversion @@ -145,10 +145,7 @@ fanOut = bulkSend :: forall m. - ( MonadIO m, - MonadThrow m, - MonadCatch m, - MonadMask m, + ( MonadMask m, HasRequestId m, MonadHttp m, MonadUnliftIO m, @@ -162,12 +159,9 @@ bulkSend uri req = (uri,) <$> ((Right <$> bulkSend' uri req) `catch` (pure . Lef bulkSend' :: forall m. ( MonadIO m, - MonadThrow m, - MonadCatch m, MonadMask m, HasRequestId m, MonadHttp m, - MonadUnliftIO m, Log.MonadLogger m ) => URI -> @@ -201,7 +195,7 @@ bulkSend' uri bulkPushRequest = do let ex = StatusCodeException (rs {responseBody = ()}) mempty in throwM $ HttpExceptionRequest rq ex } - decodeBulkResp :: MonadThrow m => Maybe L.ByteString -> m BulkPushResponse + decodeBulkResp :: Maybe L.ByteString -> m BulkPushResponse decodeBulkResp Nothing = throwM $ ErrorCall "missing response body from cannon" decodeBulkResp (Just lbs) = either err pure $ eitherDecode lbs where @@ -282,7 +276,7 @@ bulkresource = URI . (\x -> x {URI.uriPath = "/i/bulkpush"}) . fromURI . resourc -- TODO: a Map-based implementation would be faster for sufficiently large inputs. do we want to -- take the time and benchmark the difference? move it to types-common? {-# INLINE groupAssoc #-} -groupAssoc :: (Eq a, Ord a) => [(a, b)] -> [(a, [b])] +groupAssoc :: Ord a => [(a, b)] -> [(a, [b])] groupAssoc = groupAssoc' compare -- TODO: Also should we give 'Notification' an 'Ord' instance? diff --git a/services/gundeck/src/Gundeck/Redis.hs b/services/gundeck/src/Gundeck/Redis.hs index 8994fb5e92..36298b0f67 100644 --- a/services/gundeck/src/Gundeck/Redis.hs +++ b/services/gundeck/src/Gundeck/Redis.hs @@ -104,7 +104,7 @@ connectRobust l retryStrategy connectLowLevel = do -- -- Blocks on connection errors as long as the connection is not reestablished. -- Without externally enforcing timeouts, this may lead to leaking threads. -runRobust :: (MonadIO m, MonadUnliftIO m, MonadLogger m) => RobustConnection -> Redis a -> m a +runRobust :: (MonadUnliftIO m, MonadLogger m) => RobustConnection -> Redis a -> m a runRobust mvar action = do robustConnection <- readMVar mvar catches diff --git a/services/gundeck/src/Gundeck/ThreadBudget/Internal.hs b/services/gundeck/src/Gundeck/ThreadBudget/Internal.hs index a0ecd813c0..b393651524 100644 --- a/services/gundeck/src/Gundeck/ThreadBudget/Internal.hs +++ b/services/gundeck/src/Gundeck/ThreadBudget/Internal.hs @@ -1,3 +1,6 @@ +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -108,7 +111,7 @@ unregister ref key = -- update the budget. runWithBudget :: forall m. - (MonadIO m, LC.MonadLogger m, MonadUnliftIO m) => + (LC.MonadLogger m, MonadUnliftIO m) => Metrics -> ThreadBudgetState -> Int -> @@ -182,7 +185,7 @@ watchThreadBudgetState metrics (ThreadBudgetState limits ref) freq = safeForever recordMetrics :: forall m. - (MonadIO m, LC.MonadLogger m, MonadCatch m) => + MonadIO m => Metrics -> MaxConcurrentNativePushes -> IORef BudgetMap -> @@ -208,7 +211,7 @@ staleTolerance = 3 -- then remove them from the budgetmap. removeStaleHandles :: forall m. - (MonadIO m, LC.MonadLogger m, MonadCatch m) => + (MonadIO m, LC.MonadLogger m) => IORef BudgetMap -> m () removeStaleHandles ref = do diff --git a/services/gundeck/src/Gundeck/Util/Redis.hs b/services/gundeck/src/Gundeck/Util/Redis.hs index 476c5916a5..82ac48b321 100644 --- a/services/gundeck/src/Gundeck/Util/Redis.hs +++ b/services/gundeck/src/Gundeck/Util/Redis.hs @@ -35,7 +35,7 @@ x1 = limitRetries 1 <> exponentialBackoff 100000 x3 :: RetryPolicy x3 = limitRetries 3 <> exponentialBackoff 100000 -handlers :: (MonadLogger m, Monad m) => [a -> Handler m Bool] +handlers :: MonadLogger m => [a -> Handler m Bool] handlers = [ const . Handler $ \case RedisSimpleError (Error err) -> pure $ "READONLY" `BS.isPrefixOf` err diff --git a/services/gundeck/test/bench/Main.hs b/services/gundeck/test/bench/Main.hs index b4df7ee234..f199ae87dc 100644 --- a/services/gundeck/test/bench/Main.hs +++ b/services/gundeck/test/bench/Main.hs @@ -63,7 +63,7 @@ notice = do let msg = NativePush i HighPriority Nothing uid = a ^. addrUser transp = a ^. addrTransport - Right txt <- serialise msg uid transp + Right txt <- pure $ serialise msg uid transp pure $! LT.toStrict txt bench_BudgetSpent' :: IORef BudgetMap -> IO () diff --git a/services/gundeck/test/integration/API.hs b/services/gundeck/test/integration/API.hs index 1e14a74354..9e336b176d 100644 --- a/services/gundeck/test/integration/API.hs +++ b/services/gundeck/test/integration/API.hs @@ -1,3 +1,5 @@ +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} {-# OPTIONS_GHC -fno-warn-incomplete-patterns #-} -- This file is part of the Wire Server implementation. @@ -114,8 +116,9 @@ tests s = ], testGroup "Websocket pingpong" - [ test s "pings produce pongs" testPingPong, - test s "non-pings are ignored" testNoPingNoPong + [ test s "data-level pings produce pongs" testDataPingPong, + test s "control pings with payload produce pongs with the same payload" testControlPingPongWithData, + test s "data non-pings are ignored" testNoPingNoPong ], testGroup "Redis migration" @@ -838,8 +841,8 @@ testUnregisterPushToken = do void $ retryWhileN 12 (not . null) (listPushTokens uid) unregisterPushToken uid (tkn ^. token) !!! const 404 === statusCode -testPingPong :: TestM () -testPingPong = do +testDataPingPong :: TestM () +testDataPingPong = do ca <- view tsCannon uid :: UserId <- randomId connid :: ConnId <- randomConnId @@ -850,6 +853,21 @@ testPingPong = do msg <- waitForMessage chread assertBool "no pong" $ msg == Just "pong" +testControlPingPongWithData :: TestM () +testControlPingPongWithData = do + ca <- view tsCannon + uid :: UserId <- randomId + connid :: ConnId <- randomConnId + [(_, [(chread, chPingWrite)] :: [(TChan WS.Message, TChan ByteString)])] <- + connectUsersAndDevicesWithSendingClientsRaw ca [(uid, [connid])] + liftIO $ do + let pingPayload = "pi 3e4ac0590d55a24af7298b po" + atomically $ writeTChan chPingWrite pingPayload + _msg <- waitForMessageRaw chread -- this is a server-sent ping; we'll ignore this + msg <- waitForMessageRaw chread + let expected = Just (WS.ControlMessage $ WS.Pong $ fromStrict pingPayload) + assertBool "no pong with the same payload" $ msg == expected + testNoPingNoPong :: TestM () testNoPingNoPong = do ca <- view tsCannon @@ -995,15 +1013,39 @@ connectUsersAndDevicesWithSendingClients :: [(UserId, [ConnId])] -> TestM [(UserId, [(TChan ByteString, TChan ByteString)])] connectUsersAndDevicesWithSendingClients ca uidsAndConnIds = do - chs <- forM uidsAndConnIds $ \(uid, conns) -> - (uid,) <$> do - forM conns $ \conn -> do - chread <- liftIO $ atomically newTChan - chwrite <- liftIO $ atomically newTChan - _ <- wsRun ca uid conn (wsReaderWriter chread chwrite) - pure (chread, chwrite) - (\(uid, conns) -> wsAssertPresences uid (length conns)) `mapM_` uidsAndConnIds - pure chs + forM uidsAndConnIds $ \(uid, conns) -> do + chs <- + (uid,) <$> do + forM conns $ \conn -> do + chread <- liftIO $ atomically newTChan + chwrite <- liftIO $ atomically newTChan + _ <- wsRun ca uid conn (wsReaderWriter chread chwrite) + pure (chread, chwrite) + assertPresences (uid, conns) + pure chs + +-- similar to the function above, but hooks +-- in a Ping Writer and gives access to 'WS.Message's +-- this can be used to test Ping/Pong behaviour on the control channel +connectUsersAndDevicesWithSendingClientsRaw :: + HasCallStack => + CannonR -> + [(UserId, [ConnId])] -> + TestM [(UserId, [(TChan WS.Message, TChan ByteString)])] +connectUsersAndDevicesWithSendingClientsRaw ca uidsAndConnIds = do + forM uidsAndConnIds $ \(uid, conns) -> do + chs <- + (uid,) <$> do + forM conns $ \conn -> do + chread <- liftIO $ atomically newTChan + chwrite <- liftIO $ atomically newTChan + _ <- wsRun ca uid conn (wsReaderWriterPing chread chwrite) + pure (chread, chwrite) + assertPresences (uid, conns) + pure chs + +assertPresences :: (UserId, [ConnId]) -> TestM () +assertPresences (uid, conns) = wsAssertPresences uid (length conns) -- | Sort 'PushToken's based on the actual 'token' values. sortPushTokens :: [PushToken] -> [PushToken] @@ -1035,6 +1077,12 @@ wsReaderWriter chread chwrite conn = (forever $ WS.receiveData conn >>= atomically . writeTChan chread) (forever $ WS.sendTextData conn =<< atomically (readTChan chwrite)) +wsReaderWriterPing :: TChan WS.Message -> TChan ByteString -> WS.ClientApp () +wsReaderWriterPing chread chwrite conn = + concurrently_ + (forever $ WS.receive conn >>= atomically . writeTChan chread) + (forever $ WS.sendPing conn =<< atomically (readTChan chwrite)) + retryWhile :: (MonadIO m) => (a -> Bool) -> m a -> m a retryWhile = retryWhileN 10 @@ -1045,10 +1093,13 @@ retryWhileN n f m = (const (pure . f)) (const m) -waitForMessage :: TChan ByteString -> IO (Maybe ByteString) +waitForMessageRaw :: TChan WS.Message -> IO (Maybe WS.Message) +waitForMessageRaw = System.Timeout.timeout 3000000 . liftIO . atomically . readTChan + +waitForMessage :: ToByteString a => TChan a -> IO (Maybe a) waitForMessage = waitForMessage' 1000000 -waitForMessage' :: Int -> TChan ByteString -> IO (Maybe ByteString) +waitForMessage' :: ToByteString a => Int -> TChan a -> IO (Maybe a) waitForMessage' musecs = System.Timeout.timeout musecs . liftIO . atomically . readTChan unregisterClient :: GundeckR -> UserId -> ClientId -> TestM (Response (Maybe BL.ByteString)) diff --git a/services/gundeck/test/unit/MockGundeck.hs b/services/gundeck/test/unit/MockGundeck.hs index 11c85fefac..5b7ef2999d 100644 --- a/services/gundeck/test/unit/MockGundeck.hs +++ b/services/gundeck/test/unit/MockGundeck.hs @@ -3,6 +3,8 @@ {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeSynonymInstances #-} {-# OPTIONS_GHC -Wno-orphans #-} +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- diff --git a/services/gundeck/test/unit/Native.hs b/services/gundeck/test/unit/Native.hs index e8715a44ba..7dc84d1ad9 100644 --- a/services/gundeck/test/unit/Native.hs +++ b/services/gundeck/test/unit/Native.hs @@ -49,7 +49,7 @@ serialiseOkProp t = ioProperty $ do a <- mkAddress t n <- randNotif (0, 1280) m <- randMessage n - r <- serialise m (a ^. addrUser) (a ^. addrTransport) + let r = serialise m (a ^. addrUser) (a ^. addrTransport) let sn = either (const Nothing) Just r >>= decode' . LT.encodeUtf8 let equalTransport = fmap snsNotifTransport sn == Just t equalNotif <- case snsNotifBundle <$> sn of diff --git a/services/gundeck/test/unit/ThreadBudget.hs b/services/gundeck/test/unit/ThreadBudget.hs index 76180875f1..444d978fa5 100644 --- a/services/gundeck/test/unit/ThreadBudget.hs +++ b/services/gundeck/test/unit/ThreadBudget.hs @@ -3,6 +3,8 @@ {-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} {-# OPTIONS_GHC -Wno-orphans #-} +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- diff --git a/services/integration.sh b/services/integration.sh deleted file mode 100755 index 648a60aaba..0000000000 --- a/services/integration.sh +++ /dev/null @@ -1,171 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail - -USAGE="$0 [args...]" -EXE=${1:?$USAGE} -TOP_LEVEL="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" -DIR="${TOP_LEVEL}/services" -PARENT_PID=$$ -rm -f /tmp/integration.* # remove previous temp files, if any -EXIT_STATUS_LOCATION=$(mktemp "/tmp/integration.XXXXXXXXXXX") -echo 1 >"${EXIT_STATUS_LOCATION}" - -function kill_all() { - # kill the process tree of the PARENT_PID - kill -9 -${PARENT_PID} &> /dev/null -} - -function list_descendants () { - local children - children="$(pgrep -P "$1")" - for pid in $children - do - list_descendants "$pid" - done - echo "$children" -} - -function kill_gracefully() { - pkill "gundeck|brig|galley|cargohold|cannon|spar|nginz|stern" - sleep 1 - kill $(list_descendants "$PARENT_PID") &> /dev/null -} - -trap "kill_gracefully; kill_all" INT TERM ERR - -function check_prerequisites() { - if ! ( nc -z 127.0.0.1 9042 \ - && nc -z 127.0.0.1 9200 \ - && nc -z 127.0.0.1 6379 ); then - echo "Databases not up. Maybe run 'deploy/dockerephemeral/run.sh' in a separate terminal first?"; exit 1; - fi - if [ ! -f "${TOP_LEVEL}/dist/brig" ] \ - && [ ! -f "${TOP_LEVEL}/dist/galley" ] \ - && [ ! -f "${TOP_LEVEL}/dist/cannon" ] \ - && [ ! -f "${TOP_LEVEL}/dist/gundeck" ] \ - && [ ! -f "${TOP_LEVEL}/dist/cargohold" ] \ - && [ ! -f "${TOP_LEVEL}/dist/stern" ] \ - && [ ! -f "${TOP_LEVEL}/dist/spar" ]; then - echo "Not all services are compiled. How about you run 'cd ${TOP_LEVEL} && make' first?"; exit 1; - fi -} - -blue=6 -green=10 -orange=3 -yellow=11 -purpleish=13 - -if [[ $INTEGRATION_USE_REAL_AWS -eq 1 ]]; then - echo 'Attempting to run integration tests using real AWS services!' - [ -z "$AWS_REGION" ] && echo "Need to set AWS_REGION in your environment" && exit 1; - [ -z "$AWS_ACCESS_KEY_ID" ] && echo "Need to set AWS_ACCESS_KEY_ID in your environment" && exit 1; - [ -z "$AWS_SECRET_ACCESS_KEY" ] && echo "Need to set AWS_SECRET_ACCESS_KEY in your environment" && exit 1; - "${TOP_LEVEL}"/services/gen-aws-conf.sh - integration_file_extension='-aws.yaml' -elif [[ $INTEGRATION_CARGOHOLD_ONLY_COMPAT -eq 1 ]]; then - echo "Running tests using specific S3 buckets for cargohold using folder $CARGOHOLD_COMPAT_CONFIG_FOLDER" - if [ ! -f "${CARGOHOLD_COMPAT_CONFIG_FOLDER}/env.sh" ] \ - && [ ! -f "${CARGOHOLD_COMPAT_CONFIG_FOLDER}/cargohold.integration.yaml" ]; then - echo 'expecting a CARGOHOLD_COMPAT_CONFIG_FOLDER/cargohold.integration.yaml and' - echo 'expecting a CARGOHOLD_COMPAT_CONFIG_FOLDER/env.sh' - exit 1; - fi -else - # brig,gundeck,galley use the amazonka library's 'Discover', which expects AWS credentials - # even if those are not used/can be dummy values with the fake sqs/ses/etc containers used - # (see deploy/dockerephemeral/docker-compose.yaml ) - echo 'Running tests using mocked AWS services' - export AWS_REGION=eu-west-1 - export AWS_ACCESS_KEY_ID=dummykey - export AWS_SECRET_ACCESS_KEY=dummysecret - integration_file_extension='.yaml' -fi - -function run() { - service=$1 - instance=$2 - colour=$3 - configfile=${4:-"${service}${instance}.integration${integration_file_extension}"} - # Check if we're on a Mac - if [[ "$OSTYPE" == "darwin"* ]]; then - # Mac sed uses '-l' to set line-by-line buffering - UNBUFFERED=-l - # Test if sed supports buffer settings. GNU sed does, busybox does not. - elif sed -u '' /dev/null 2>&1; then - UNBUFFERED=-u - else - echo -e "\n\nWARNING: log output is buffered and may not show on your screen!\n\n" - UNBUFFERED='' - fi - ( ( cd "${DIR}/${service}" && "${TOP_LEVEL}/dist/${service}" -c "${configfile}" ) || kill_all) \ - | sed ${UNBUFFERED} -e "s/^/$(tput setaf ${colour})[${service}] /" -e "s/$/$(tput sgr0)/" & -} - - -if [[ $INTEGRATION_CARGOHOLD_ONLY_COMPAT -eq 1 ]]; then - source "${CARGOHOLD_COMPAT_CONFIG_FOLDER}/env.sh" - echo run cargohold "" ${purpleish} "${CARGOHOLD_COMPAT_CONFIG_FOLDER}/cargohold.integration.yaml" - run cargohold "" ${purpleish} "${CARGOHOLD_COMPAT_CONFIG_FOLDER}/cargohold.integration.yaml" -else - check_prerequisites - run brig "" ${green} - run galley "" ${yellow} - run gundeck "" ${blue} - run cannon "" ${orange} - run cannon "2" ${orange} - run cargohold "" ${purpleish} - run spar "" ${orange} - run federator "" ${blue} - run stern "" ${yellow} -fi - -function run_nginz() { - colour=$1 - - if [[ ! ${COMPILE_NGINX_USING_NIX:-1} -eq 0 ]]; then - # For nix we don't need LD_LIBRARY_PATH; we link against libzauth directly. - nginz=$(nix-build "${TOP_LEVEL}/nix" -A pkgs.nginz --no-out-link ) - (cd ${NGINZ_WORK_DIR} && ${nginz}/bin/nginx -p ${NGINZ_WORK_DIR} -c ${NGINZ_WORK_DIR}/conf/nginz/nginx.conf -g 'daemon off;' || kill_all) \ - | sed -e "s/^/$(tput setaf ${colour})[nginz] /" -e "s/$/$(tput sgr0)/" & - else - prefix=$([ -w /usr/local ] && echo /usr/local || echo "${HOME}/.wire-dev") - - (cd ${NGINZ_WORK_DIR} && LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${prefix}/lib/ ${TOP_LEVEL}/dist/nginx -p ${NGINZ_WORK_DIR} -c ${NGINZ_WORK_DIR}/conf/nginz/nginx.conf -g 'daemon off;' || kill_all) \ - | sed -e "s/^/$(tput setaf ${colour})[nginz] /" -e "s/$/$(tput sgr0)/" & - fi -} - -NGINZ_PORT="" - -if [[ ! ${INTEGRATION_USE_NGINZ:-1} -eq 0 ]]; then - NGINZ_PORT=8080 - # Note: for integration tests involving nginz, - # nginz and brig must share the same zauth public/private keys - export NGINZ_WORK_DIR="$TOP_LEVEL/services/nginz/integration-test" - - run_nginz ${purpleish} -fi - -# the ports are copied from ./integration.yaml -if [[ $INTEGRATION_CARGOHOLD_ONLY_COMPAT -eq 1 ]]; then - PORT_LIST="8084" -else - PORT_LIST="8082 8083 8084 8085 8086 8088 $NGINZ_PORT" -fi - -while [ "$all_services_are_up" == "" ]; do - export all_services_are_up="1" - for port in $PORT_LIST; do - ( curl --write-out '%{http_code}' --silent --output /dev/null http://localhost:"$port"/i/status \ - | grep -q '^20[04]' ) \ - || export all_services_are_up="" - done - sleep 1 -done -echo "all services are up!" - -( ${EXE} "${@:2}" && echo 0 > "${EXIT_STATUS_LOCATION}" && kill_gracefully ) || kill_gracefully & - -wait -exit $(<"${EXIT_STATUS_LOCATION}") diff --git a/services/integration.yaml b/services/integration.yaml index d8d0cba691..20e002ec88 100644 --- a/services/integration.yaml +++ b/services/integration.yaml @@ -64,11 +64,10 @@ provider: publicKey: test/resources/pubkey.pem cert: test/resources/cert.pem botHost: https://127.0.0.1 - botPort: 29631 # Used by spar integration tests # Keep this in sync with setTeamInvitationTimeout from brig -brigSettingsTeamInvitationTimeout: 5 +brigSettingsTeamInvitationTimeout: 10 # Used by brig-integration (Federation subfolder) backendTwo: @@ -89,7 +88,7 @@ backendTwo: port: 9084 cannon: host: 127.0.0.1 - port: 9086 + port: 9083 redis2: host: 127.0.0.1 diff --git a/services/nginz/integration-test/conf/nginz/integration.conf b/services/nginz/integration-test/conf/nginz/integration.conf new file mode 100644 index 0000000000..baae352c92 --- /dev/null +++ b/services/nginz/integration-test/conf/nginz/integration.conf @@ -0,0 +1,19 @@ +# plain TCP/http listening for integration tests only. +listen 8080; +listen 8081; + +# for nginx-without-tls, we need to use a separate port for http2 traffic, +# as nginx cannot handle unencrypted http1 and http2 trafic on the same +# port. +# This port is only used for trying out nginx http2 forwarding without TLS locally and should not +# be ported to any production nginz config. +listen 8090 http2; + +######## TLS/SSL block start ############## +# +# Most integration tests simply use the http ports 8080 and 8081 +# But to also test tls forwarding, this port can be used. +# This applies only locally, as for kubernetes (helm chart) based deployments, +# TLS is terminated at the ingress level, not at nginz level +listen 8443 ssl http2; +listen [::]:8443 ssl http2; diff --git a/services/nginz/integration-test/conf/nginz/nginx.conf b/services/nginz/integration-test/conf/nginz/nginx.conf index e1d5f3fb6c..f6db101baf 100644 --- a/services/nginz/integration-test/conf/nginz/nginx.conf +++ b/services/nginz/integration-test/conf/nginz/nginx.conf @@ -1,6 +1,6 @@ worker_processes 4; worker_rlimit_nofile 1024; -pid /tmp/nginz.pid; +include pid.conf; # for easy overriding # nb. start up errors (eg. misconfiguration) may still end up in /$(LOG_PATH)/error.log error_log stderr warn; @@ -106,25 +106,7 @@ http { # server { - # plain TCP/http listening for integration tests only. - listen 8080; - listen 8081; - - # for nginx-without-tls, we need to use a separate port for http2 traffic, - # as nginx cannot handle unencrypted http1 and http2 trafic on the same - # port. - # This port is only used for trying out nginx http2 forwarding without TLS locally and should not - # be ported to any production nginz config. - listen 8090 http2; - - ######## TLS/SSL block start ############## - # - # Most integration tests simply use the http ports 8080 and 8081 - # But to also test tls forwarding, this port can be used. - # This applies only locally, as for kubernetes (helm chart) based deployments, - # TLS is terminated at the ingress level, not at nginz level - listen 8443 ssl http2; - listen [::]:8443 ssl http2; + include integration.conf; # self-signed certificates generated using wire-server/hack/bin/selfsigned.sh ssl_certificate integration-leaf.pem; @@ -177,19 +159,22 @@ http { # ## brig unauthenticated endpoints - rewrite ^/api-docs/users /users/api-docs?base_url=http://127.0.0.1:8080/ break; + location ~* ^(/v[0-9]+)?/api/swagger-ui { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } - location /users/api-docs { - include common_response_no_zauth.conf; - proxy_pass http://brig; + location ~* ^(/v[0-9]+)?/api/swagger.json { + include common_response_no_zauth.conf; + proxy_pass http://brig; } - location /api/swagger-ui { + location ~* ^(/v[0-9]+)?/api-internal/swagger-ui { include common_response_no_zauth.conf; proxy_pass http://brig; } - location /api/swagger.json { + location ~* ^(/v[0-9]+)?/api-internal/swagger.json { include common_response_no_zauth.conf; proxy_pass http://brig; } @@ -293,14 +278,6 @@ http { # Cargohold Endpoints - rewrite ^/api-docs/assets /assets/api-docs?base_url=http://127.0.0.1:8080/ break; - - location /assets/api-docs { - include common_response_no_zauth.conf; - proxy_pass http://cargohold; - } - - location /assets { include common_response_with_zauth.conf; proxy_pass http://cargohold; @@ -318,13 +295,6 @@ http { # Galley Endpoints - rewrite ^/api-docs/conversations /conversations/api-docs?base_url=http://127.0.0.1:8080/ break; - - location /conversations/api-docs { - include common_response_no_zauth.conf; - proxy_pass http://galley; - } - location ~* ^(/v[0-9]+)?/legalhold/conversations/(.*)$ { include common_response_with_zauth.conf; proxy_pass http://galley; @@ -405,16 +375,6 @@ http { proxy_pass http://galley; } - location /galley-api/swagger-ui { - include common_response_no_zauth.conf; - proxy_pass http://galley; - } - - location /galley-api/swagger.json { - include common_response_no_zauth.conf; - proxy_pass http://galley; - } - location /mls/welcome { include common_response_with_zauth.conf; proxy_pass http://galley; @@ -437,13 +397,6 @@ http { # Gundeck Endpoints - rewrite ^/api-docs/push /push/api-docs?base_url=http://127.0.0.1:8080/ break; - - location /push/api-docs { - include common_response_no_zauth.conf; - proxy_pass http://gundeck; - } - location /push { include common_response_with_zauth.conf; proxy_pass http://gundeck; @@ -461,13 +414,6 @@ http { # Proxy Endpoints - rewrite ^/api-docs/proxy /proxy/api-docs?base_url=http://127.0.0.1:8080/ break; - - location /proxy/api-docs { - include common_response_no_zauth.conf; - proxy_pass http://proxy; - } - location /proxy { include common_response_with_zauth.conf; proxy_pass http://proxy; @@ -475,13 +421,6 @@ http { # Cannon Endpoints - rewrite ^/api-docs/await /await/api-docs?base_url=http://127.0.0.1:8080/ break; - - location /await/api-docs { - include common_response_no_zauth.conf; - proxy_pass http://cannon; - } - location /await { include common_response_with_zauth.conf; proxy_pass http://cannon; @@ -502,55 +441,5 @@ http { include common_response_with_zauth.conf; proxy_pass http://spar; } - - # - # Swagger Resource Listing - # - location /api-docs { - zauth off; - default_type application/json; - root conf/nginz/zwagger-ui; - index resources.json; - if ($request_method = 'OPTIONS') { - add_header 'Access-Control-Allow-Methods' "GET, POST, PUT, DELETE, OPTIONS"; - add_header 'Access-Control-Allow-Headers' "$http_access_control_request_headers, DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type"; - add_header 'Content-Type' 'text/plain; charset=UTF-8'; - add_header 'Content-Length' 0; - return 204; - } - more_set_headers 'Access-Control-Allow-Origin: $http_origin'; - } - - # - # Back Office Swagger Resource Listing - # - location /backoffice/api-docs { - zauth off; - default_type application/json; - root conf/nginz/zwagger-ui; - index resources.json; - if ($request_method = 'OPTIONS') { - add_header 'Access-Control-Allow-Methods' "GET, POST, PUT, DELETE, OPTIONS"; - add_header 'Access-Control-Allow-Headers' "$http_access_control_request_headers, DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type"; - add_header 'Content-Type' 'text/plain; charset=UTF-8'; - add_header 'Content-Length' 0; - return 204; - } - more_set_headers 'Access-Control-Allow-Origin: $http_origin'; - } - - # Swagger UI - - location /swagger-ui { - zauth off; - gzip off; - alias conf/nginz/zwagger-ui; - types { - application/javascript js; - text/css css; - text/html html; - image/png png; - } - } } } diff --git a/services/nginz/integration-test/conf/nginz/pid.conf b/services/nginz/integration-test/conf/nginz/pid.conf new file mode 100644 index 0000000000..e722aa5ae2 --- /dev/null +++ b/services/nginz/integration-test/conf/nginz/pid.conf @@ -0,0 +1 @@ +pid /tmp/nginz.pid; diff --git a/services/nginz/integration-test/conf/nginz/zwagger-ui/api-docs/resources.json b/services/nginz/integration-test/conf/nginz/zwagger-ui/api-docs/resources.json deleted file mode 100644 index 805dee81d8..0000000000 --- a/services/nginz/integration-test/conf/nginz/zwagger-ui/api-docs/resources.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "apiVersion": "1.0", - "swaggerVersion": "1.2", - "apis": [ - { - "path": "/users", - "description": "Users, Connections and Onboarding" - }, - { - "path": "/push", - "description": "Push Notifications" - }, - { - "path": "/conversations", - "description": "Conversations and Messaging" - }, - { - "path": "/assets", - "description": "Assets" - }, - { - "path": "/await", - "description": "Push Notifications" - } - ] -} diff --git a/services/nginz/integration-test/conf/nginz/zwagger-ui/index.html b/services/nginz/integration-test/conf/nginz/zwagger-ui/index.html deleted file mode 100644 index 2409c26180..0000000000 --- a/services/nginz/integration-test/conf/nginz/zwagger-ui/index.html +++ /dev/null @@ -1,82 +0,0 @@ - - -
- - - -
- - - -
- - -
- - - - - - - - - - - - diff --git a/services/nginz/integration-test/conf/nginz/zwagger-ui/tab.html b/services/nginz/integration-test/conf/nginz/zwagger-ui/tab.html deleted file mode 100644 index 9d514b1452..0000000000 --- a/services/nginz/integration-test/conf/nginz/zwagger-ui/tab.html +++ /dev/null @@ -1,218 +0,0 @@ - - - - - - Swagger UI - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -

- -
-
-
-
-
-
- -
- -
- -
- -
- - - -
 
-
- - - diff --git a/services/nginz/zwagger-ui/api-docs/resources.json b/services/nginz/zwagger-ui/api-docs/resources.json deleted file mode 100644 index 805dee81d8..0000000000 --- a/services/nginz/zwagger-ui/api-docs/resources.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "apiVersion": "1.0", - "swaggerVersion": "1.2", - "apis": [ - { - "path": "/users", - "description": "Users, Connections and Onboarding" - }, - { - "path": "/push", - "description": "Push Notifications" - }, - { - "path": "/conversations", - "description": "Conversations and Messaging" - }, - { - "path": "/assets", - "description": "Assets" - }, - { - "path": "/await", - "description": "Push Notifications" - } - ] -} diff --git a/services/nginz/zwagger-ui/index.html b/services/nginz/zwagger-ui/index.html deleted file mode 100644 index 2409c26180..0000000000 --- a/services/nginz/zwagger-ui/index.html +++ /dev/null @@ -1,82 +0,0 @@ - - -
- - - -
- - - -
- - -
- - - - - - - - - - - - diff --git a/services/nginz/zwagger-ui/tab.html b/services/nginz/zwagger-ui/tab.html deleted file mode 100644 index 9d514b1452..0000000000 --- a/services/nginz/zwagger-ui/tab.html +++ /dev/null @@ -1,218 +0,0 @@ - - - - - - Swagger UI - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -

- -
-
-
-
-
-
- -
- -
- -
- -
- - - -
 
-
- - - diff --git a/services/proxy/src/Main.hs b/services/proxy/exec/Main.hs similarity index 100% rename from services/proxy/src/Main.hs rename to services/proxy/exec/Main.hs diff --git a/services/proxy/proxy.cabal b/services/proxy/proxy.cabal index 9b850c2e56..784b5d854a 100644 --- a/services/proxy/proxy.cabal +++ b/services/proxy/proxy.cabal @@ -17,7 +17,6 @@ flag static library exposed-modules: - Main Proxy.API Proxy.API.Public Proxy.Env @@ -70,7 +69,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -funbox-strict-fields + -funbox-strict-fields -Wredundant-constraints build-depends: aeson >=2.0.1.0 @@ -104,7 +103,7 @@ library default-language: Haskell2010 executable proxy - main-is: src/Main.hs + main-is: exec/Main.hs other-modules: Paths_proxy default-extensions: NoImplicitPrelude @@ -149,7 +148,7 @@ executable proxy ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -rtsopts -with-rtsopts=-T + -threaded -rtsopts -with-rtsopts=-T -Wredundant-constraints build-depends: base diff --git a/services/proxy/src/Proxy/API/Public.hs b/services/proxy/src/Proxy/API/Public.hs index 6c12b314e4..28bc75e1cf 100644 --- a/services/proxy/src/Proxy/API/Public.hs +++ b/services/proxy/src/Proxy/API/Public.hs @@ -187,7 +187,7 @@ soundcloudStream url = do x2 :: RetryPolicy x2 = exponentialBackoff 5000 <> limitRetries 2 -handler :: (MonadIO m, MonadMask m) => RetryStatus -> Handler m Bool +handler :: MonadIO m => RetryStatus -> Handler m Bool handler = const . Handler $ \case Client.HttpExceptionRequest _ Client.NoResponseDataReceived -> pure True Client.HttpExceptionRequest _ Client.IncompleteHeaders -> pure True diff --git a/services/run-services b/services/run-services new file mode 100755 index 0000000000..9fdbf894e6 --- /dev/null +++ b/services/run-services @@ -0,0 +1,443 @@ +#!/usr/bin/env python3 + +from dataclasses import dataclass, replace +import logging +import os +import re +import select +import signal +import shutil +import socket +import subprocess +import yaml +import urllib.request +import urllib.error +import sys +import tempfile +import time +import traceback +import threading + +@dataclass +class SpawnFailException(Exception): + failed_instances: object + +class Colors: + GREEN = "\x1b[38;5;10m" + YELLOW = "\x1b[38;5;11m" + BLUE = "\x1b[38;5;6m" + PURPLEISH = "\x1b[38;5;13m" + ORANGE = "\x1b[38;5;3m" + RED = "\x1b[38;5;1m" + RESET = "\x1b[0m" + +@dataclass(frozen=True) +class Service: + name: str + color: str + _internal_name: str = None + check_status: bool = True + level: str = None + config: str = None + + def with_level(self, level=None): + if level is None: + level = os.environ.get("INTEGRATION_{self.name.capitalize()}_LEVEL") + return replace(self, level=level) + + @property + def internal_name(self): + if self._internal_name is None: + return self.name + else: + return self._internal_name + + def path(self): + return os.path.join(ROOT, "dist", self.name) + + def config_file(self): + if self.config is None: + base = self.name + else: + base = self.config + return os.path.join(ROOT, "services", self.name, + base + ".integration.yaml") + + + def spawn(self, config_file, environment): + return subprocess.Popen([self.path(), "-c", config_file], + encoding='utf-8', + cwd=os.path.join(ROOT, "services", self.name), + env=environment, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + def check_exists(self): + if not os.path.exists(self.path()): + raise Exception(f"{self.name} not found") + +@dataclass(frozen=True) +class Nginz: + color: str + level: str = None + + @property + def name(self): return "nginz" + + @property + def internal_name(self): return self.name + + @property + def check_status(self): return True + + def config_file(self): + return os.path.join(ROOT, "services", "nginz", "integration-test", + "conf", "nginz","nginx.conf") + + def spawn(self, config_file, environment): + cwd = os.path.join(ROOT, "services", "nginz", "integration-test") + return subprocess.Popen([shutil.which("nginx"), "-p", cwd, "-c", + config_file, + "-g", "daemon off;"], + encoding='utf-8', cwd=cwd, env=environment, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + def check_exists(self): + if shutil.which("nginx") is None: + raise Exception("nginx not found") + +@dataclass(frozen=True) +class Instance: + service: Service + port: int + thread: threading.Thread = None + process: subprocess.Popen = None + exception: Exception = None + + def check_status(self): + self.process.poll() + if self.process.returncode is not None: + raise Exception(f"{self.service.name} has terminated") + if not self.service.check_status: + return True + try: + with urllib.request.urlopen(f"http://localhost:{self.port}/i/status") as resp: + return resp.status in [200, 204] + except urllib.error.URLError: + return False + + def spawn(self, service_map, environment, suffix, domain, backend_name): + try: + config_file = self.modified_config_file(service_map, suffix, domain) + sub = self.service.spawn(config_file, environment) + t = threading.Thread(target=lambda: color_output(sub, self.service, backend_name)) + t.start() + return Instance(self.service, self.port, t, sub) + except Exception as e: + return Instance(self.service, self.port, exception=e) + + def modified_config_file(self, service_map, suffix, domain): + """Overwrite port configuration on this service using the provided + service_map. + + This works by creating an unnamed pipe, writing the modified config + file to it, and returning a path to the read end of the pipe (in + /proc).""" + + with open(self.service.config_file()) as f: + data = yaml.safe_load(f) + + # set ports of other services + for service in service_map: + if service.internal_name in data: + data[service.internal_name]['port'] = service_map[service] + + # set cassandra keyspace + if 'cassandra' in data: + data['cassandra']['keyspace'] = f"{self.service.name}_test{suffix}" + + # set elasticseach index + if 'elasticsearch' in data: + data['elasticsearch']['index'] = f"directory{suffix}_test" + + # set federation domain + if 'optSettings' in data: + data['optSettings']['setFederationDomain'] = domain + elif 'settings' in data: + data['settings']['federationDomain'] = domain + + # set log level + if self.service.level is not None: + if 'logLevel' in data: + data['logLevel'] = self.service.level + elif 'saml' in data: + data['saml']['logLevel'] = self.service.level + + self.set_own_port(data) + + # write modified config file to pipe + return make_pipe(yaml.dump(data).encode('utf-8')) + + def set_own_port(self, data): + # spar's own port is in a different place + if self.service.name == 'spar': + data['saml']['spPort'] = self.port + elif self.service.name in data: + data[self.service.name]['port'] = self.port + +class DummyInstance(Instance): + def spawn(self, service_map, environment, suffix, domain, backend_name): + return self + + def modified_config_file(self, service_map, suffix, domain): + return "" + + def check_status(self): + return True + +class FederatorInstance(Instance): + def __init__(self, internal_port, external_port): + self.external_port = external_port + super().__init__(FEDERATOR, internal_port) + + def set_own_port(self, data): + # set external port only, as the internal one is part of the service + # map and is set by the general config logic + data['federatorExternal']['port'] = self.external_port + +class NginzInstance(Instance): + def __init__(self, local_port, http2_port, ssl_port, fed_port): + self.http2_port = http2_port + self.ssl_port = ssl_port + self.fed_port = fed_port + super().__init__(NGINZ, local_port) + + def modified_config_file(self, service_map, suffix, domain): + # Create a whole temporary directory and copy all nginx's config files. + # This is necessary because nginx assumes local imports are relative to + # the location of the main configuration file. + self.tmpdir = tempfile.TemporaryDirectory() + shutil.copytree(os.path.dirname(self.service.config_file()), + self.tmpdir.name, + dirs_exist_ok=True) + + # override port configuration + with open(os.path.join(self.tmpdir.name, "integration.conf"), 'w') as f: + override = f""" + listen {self.port}; + listen {self.http2_port} http2; + listen {self.ssl_port} ssl http2; + listen [::]:{self.ssl_port} ssl http2;""" + print(override, file=f) + + # override upstreams + with open(os.path.join(self.tmpdir.name, "upstreams"), 'w') as f: + for service, port in service_map.items(): + print(f"upstream {service.internal_name} {{", file=f) + print(f" least_conn;", file=f) + print(f" keepalive 32;", file=f) + print(f" server 127.0.0.1:{port} max_fails=3 weight=1;", file=f) + print("}", file=f) + print("upstream federator_external {", file=f) + print(f" server 127.0.0.1:{self.fed_port} max_fails=3 weight=1;", file=f) + print("}", file=f) + + # override pid configuration + with open(os.path.join(self.tmpdir.name, "pid.conf"), 'w') as f: + pid = os.path.join(self.tmpdir.name, "nginz.pid") + print(f"pid {pid};", file=f) + + return os.path.join(self.tmpdir.name, "nginx.conf") + +def check_prerequisites(services): + try: + for port in (9042, 9200, 6379): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.connect(("127.0.0.1", port)) + except Exception as e: + logging.error(f"{Colors.RED}Databases not up. Try running 'deploy/dockerephemeral/run.sh'. {Colors.RESET}") + sys.exit(1) + + try: + for service in services: + service.check_exists() + except Exception as e: + logging.error(Colors.RED + str(e) + Colors.RESET) + sys.exit(1) + +def color_output(sub, service, backend_name): + if backend_name is not None: + backend_name = "@" + backend_name + try: + for line in sub.stdout: + logging.info(f"{service.color}[{service.name}{backend_name}] {line.rstrip()}{Colors.RESET}") + finally: + sub.terminate() + sub.wait() + +def find_root(base): + # find git repository + root = os.path.realpath(base) + while not os.path.exists(os.path.join(root, ".git")): + p = os.path.dirname(root) + if p == root: raise Exception("Could not find wire-server root") + root = p + return root + +def make_pipe(data): + (r, w) = os.pipe() + os.write(w, data) + os.close(w) + return f"/proc/{os.getpid()}/fd/{r}" + +def cleanup_instances(instances): + for instance in instances: + if instance.process is None: continue + instance.process.terminate() + + for instance in instances: + if instance.thread is None: continue + instance.thread.join(timeout=0.1) + # some services don't react promptly to SIGTERM, so we give them a + # nudge if they don't terminate within a few milliseconds + if instance.thread.is_alive(): + instance.process.terminate() + instance.thread.join(timeout=0.1) + if instance.thread.is_alive(): + print("force-killing", instance.service.name) + instance.process.send_signal(signal.SIGKILl) + instance.thread.join() + +def start_backend(services, suffix, domain, backend_name): + # build a service map by choosing an arbitrary instance of each service + service_map = dict((s.service, s.port) for s in services) + + instances = set() + for blueprint in services: + instances.add(blueprint.spawn(service_map, environment, suffix, domain, backend_name)) + + failed_instances = [instance for instance in instances + if instance.exception is not None] + + # check instances + to_be_checked = [instance for instance in instances + if instance.exception is None] + start_time = time.time() + while to_be_checked: + if time.time() - start_time >= 5: + print(f"{Colors.RED}Timeout while spawing services{Colors.RESET}") + failed_instances.extend(to_be_checked) + break + + to_be_checked_again = set() + for instance in to_be_checked: + try: + if not instance.check_status(): + to_be_checked_again.add(instance) + except Exception as e: + failed_instances.append(replace(instance, exception=e)) + + to_be_checked = to_be_checked_again + time.sleep(0.05) + + # TODO: elapse timeout so that the timeout thread doesn't hold up the + # process + + if failed_instances: + cleanup_instances(instances) + raise SpawnFailException(failed_instances) + + return instances + +ENABLE_FEDERATION = os.environ.get("INTEGRATION_FEDERATION_TESTS") == "1" +LEVEL = os.environ.get("INTEGRATION_LEVEL") +BRIG = Service("brig", Colors.GREEN).with_level(LEVEL) +GALLEY = Service("galley", Colors.YELLOW).with_level(LEVEL) +GUNDECK = Service("gundeck", Colors.BLUE).with_level(LEVEL) +CANNON = Service("cannon", Colors.ORANGE).with_level(LEVEL) +CANNON2 = Service("cannon", Colors.ORANGE, + "cannon2", config="cannon2").with_level(LEVEL) +CARGOHOLD = Service("cargohold", Colors.PURPLEISH).with_level(LEVEL) +SPAR = Service("spar", Colors.ORANGE).with_level(LEVEL) +FEDERATOR = Service("federator", Colors.BLUE, + "federatorInternal", + check_status=False).with_level(LEVEL) +STERN = Service("stern", Colors.YELLOW).with_level(LEVEL) +PROXY = Service("proxy", Colors.RED).with_level(LEVEL) +NGINZ = Nginz(Colors.PURPLEISH) + +if __name__ == '__main__': + logging.basicConfig(encoding='utf-8', level=logging.INFO, + format='%(message)s') + ROOT = find_root(os.getcwd()) + if ROOT is None: + error("This script needs to be run within the wire-server direnv") + + environment = { + 'AWS_REGION': "eu-west-1", + 'AWS_ACCESS_KEY_ID': "dummykey", + 'AWS_SECRET_ACCESS_KEY': "dummysecret" + } + + backend_a = [ + Instance(BRIG, 8082), + Instance(GALLEY, 8085), + Instance(GUNDECK, 8086), + Instance(CANNON, 8083), + Instance(CANNON2, 8183), + Instance(CARGOHOLD, 8084), + Instance(SPAR, 8088), + DummyInstance(PROXY, 8087), + FederatorInstance(8097, 8098), + NginzInstance( + local_port=8080, + http2_port=8090, + ssl_port=8443, + fed_port=8098) + ] + + backend_b = [ + Instance(BRIG, 9082), + Instance(GALLEY, 9085), + Instance(GUNDECK, 9086), + Instance(CANNON, 9083), + Instance(CANNON2, 9183), + Instance(CARGOHOLD, 9084), + Instance(SPAR, 9088), + DummyInstance(PROXY, 9087), + FederatorInstance(9097, 9098), + NginzInstance( + local_port=9080, + http2_port=9090, + ssl_port=9443, + fed_port=9098) + ] + + check_prerequisites(set(s.service for s in backend_a)) + + try: + instances = set() + instances |= start_backend(backend_a, "", "example.com", "A") + if ENABLE_FEDERATION: + instances |= start_backend(backend_b, "2", "b.example.com", "B") + + # run main script or just wait forever + if len(sys.argv) == 1: + print("(This will hang, Control+C to close.)") + print("Now you can manually curl them or start an integration test executable manually with e.g. \n(first cd to a service dir for correct working directory)\n cd services/brig && ../../dist/brig-integration -s brig.integration.yaml -i ../integration.yaml") + signal.pause() + else: + ret = subprocess.run(sys.argv[1:], + env=dict(list(os.environ.items()) + + list(environment.items()))) + sys.exit(ret.returncode) + except KeyboardInterrupt: + pass + except SpawnFailException as e: + print(f"{Colors.RED}The following services failed to start:{Colors.RESET}") + for instance in e.failed_instances: + print(f"{instance.service.name} at port {instance.port}" + + (f" ({instance.exception})" if instance.exception else "")) + finally: + cleanup_instances(instances) diff --git a/services/spar/spar.cabal b/services/spar/spar.cabal index 8a879ee934..c3e43ca61b 100644 --- a/services/spar/spar.cabal +++ b/services/spar/spar.cabal @@ -122,7 +122,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -j - -Wno-redundant-constraints -Werror + -Wno-redundant-constraints -Werror -Wredundant-constraints build-depends: aeson @@ -242,7 +242,7 @@ executable spar -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -j -Wno-redundant-constraints -Werror -threaded -rtsopts - -with-rtsopts=-N -with-rtsopts=-T + -with-rtsopts=-N -with-rtsopts=-T -Wredundant-constraints build-depends: aeson @@ -381,7 +381,7 @@ executable spar-integration -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -j -Wno-redundant-constraints -Werror -threaded -rtsopts - -with-rtsopts=-N + -with-rtsopts=-N -Wredundant-constraints build-tool-depends: hspec-discover:hspec-discover build-depends: @@ -521,7 +521,7 @@ executable spar-migrate-data -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -j -Wno-redundant-constraints -Werror -threaded -rtsopts - -with-rtsopts=-N + -with-rtsopts=-N -Wredundant-constraints build-depends: aeson @@ -665,7 +665,7 @@ executable spar-schema -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -j -Wno-redundant-constraints -Werror -threaded -rtsopts - -with-rtsopts=-N + -with-rtsopts=-N -Wredundant-constraints build-depends: aeson @@ -801,7 +801,7 @@ test-suite spec -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -j -Wno-redundant-constraints -Werror -threaded -rtsopts - -with-rtsopts=-N + -with-rtsopts=-N -Wredundant-constraints build-tool-depends: hspec-discover:hspec-discover build-depends: diff --git a/services/spar/spar.integration.yaml b/services/spar/spar.integration.yaml index 77792b4ee1..6a1eb2f398 100644 --- a/services/spar/spar.integration.yaml +++ b/services/spar/spar.integration.yaml @@ -1,6 +1,6 @@ saml: version: SAML2.0 - logLevel: Info + logLevel: Warn spHost: 0.0.0.0 spPort: 8088 diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index e0a6cd861b..2489bf38d8 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -1,9 +1,10 @@ {-# LANGUAGE RecordWildCards #-} +{-# HLINT ignore "Use $>" #-} +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} {-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} {-# OPTIONS_GHC -fplugin=Polysemy.Plugin #-} -{-# HLINT ignore "Use $>" #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -100,6 +101,7 @@ import Spar.Sem.VerdictFormatStore (VerdictFormatStore) import qualified Spar.Sem.VerdictFormatStore as VerdictFormatStore import System.Logger (Msg) import qualified URI.ByteString as URI +import Wire.API.Routes.Internal.Spar import Wire.API.Routes.Public.Spar import Wire.API.User import Wire.API.User.IdentityProvider @@ -116,32 +118,33 @@ app ctx = serve (Proxy @API) (hoistServer (Proxy @API) (runSparToHandler ctx) (api $ sparCtxOpts ctx) :: Server API) api :: - Members - '[ GalleyAccess, - BrigAccess, - Input Opts, - AssIDStore, - AReqIDStore, - VerdictFormatStore, - ScimExternalIdStore, - ScimUserTimesStore, - ScimTokenStore, - DefaultSsoCode, - IdPConfigStore, - IdPRawMetadataStore, - SAMLUserStore, - Random, - Error SparError, - SAML2, - Now, - SamlProtocolSettings, - Logger String, - Reporter, - -- TODO(sandy): Only necessary for 'fromExceptionSem' in 'apiScim' - Final IO, - Logger (Msg -> Msg) - ] - r => + ( Member GalleyAccess r, + Member BrigAccess r, + Member (Input Opts) r, + Member AssIDStore r, + Member AReqIDStore r, + Member VerdictFormatStore r, + Member ScimExternalIdStore r, + Member ScimUserTimesStore r, + Member ScimTokenStore r, + Member DefaultSsoCode r, + Member IdPConfigStore r, + Member IdPRawMetadataStore r, + Member SAMLUserStore r, + Member Random r, + Member (Error SparError) r, + Member SAML2 r, + Member Now r, + Member SamlProtocolSettings r, + Member (Logger String) r, + Member Reporter r, + Member + ( -- TODO(sandy): Only necessary for 'fromExceptionSem' in 'apiScim' + Final IO + ) + r, + Member (Logger (Msg -> Msg)) r + ) => Opts -> ServerT API (Sem r) api opts = @@ -151,25 +154,23 @@ api opts = :<|> apiINTERNAL apiSSO :: - Members - '[ GalleyAccess, - Logger String, - Input Opts, - BrigAccess, - AssIDStore, - VerdictFormatStore, - AReqIDStore, - ScimTokenStore, - DefaultSsoCode, - IdPConfigStore, - Random, - Error SparError, - SAML2, - SamlProtocolSettings, - Reporter, - SAMLUserStore - ] - r => + ( Member GalleyAccess r, + Member (Logger String) r, + Member (Input Opts) r, + Member BrigAccess r, + Member AssIDStore r, + Member VerdictFormatStore r, + Member AReqIDStore r, + Member ScimTokenStore r, + Member DefaultSsoCode r, + Member IdPConfigStore r, + Member Random r, + Member (Error SparError) r, + Member SAML2 r, + Member SamlProtocolSettings r, + Member Reporter r, + Member SAMLUserStore r + ) => Opts -> ServerT APISSO (Sem r) apiSSO opts = @@ -182,18 +183,16 @@ apiSSO opts = :<|> ssoSettings apiIDP :: - Members - '[ Random, - Logger String, - GalleyAccess, - BrigAccess, - ScimTokenStore, - IdPConfigStore, - IdPRawMetadataStore, - SAMLUserStore, - Error SparError - ] - r => + ( Member Random r, + Member (Logger String) r, + Member GalleyAccess r, + Member BrigAccess r, + Member ScimTokenStore r, + Member IdPConfigStore r, + Member IdPRawMetadataStore r, + Member SAMLUserStore r, + Member (Error SparError) r + ) => ServerT APIIDP (Sem r) apiIDP = idpGet @@ -204,16 +203,14 @@ apiIDP = :<|> idpDelete apiINTERNAL :: - Members - '[ ScimTokenStore, - DefaultSsoCode, - IdPConfigStore, - Error SparError, - SAMLUserStore, - ScimUserTimesStore - ] - r => - ServerT APIINTERNAL (Sem r) + ( Member ScimTokenStore r, + Member DefaultSsoCode r, + Member IdPConfigStore r, + Member (Error SparError) r, + Member SAMLUserStore r, + Member ScimUserTimesStore r + ) => + ServerT InternalAPI (Sem r) apiINTERNAL = internalStatus :<|> internalDeleteTeam @@ -227,11 +224,9 @@ appName = "spar" -- SSO API authreqPrecheck :: - Members - '[ IdPConfigStore, - Error SparError - ] - r => + ( Member IdPConfigStore r, + Member (Error SparError) r + ) => Maybe URI.URI -> Maybe URI.URI -> SAML.IdPId -> @@ -242,19 +237,17 @@ authreqPrecheck msucc merr idpid = $> NoContent authreq :: - Members - '[ Random, - Input Opts, - Logger String, - AssIDStore, - VerdictFormatStore, - AReqIDStore, - SAML2, - SamlProtocolSettings, - Error SparError, - IdPConfigStore - ] - r => + ( Member Random r, + Member (Input Opts) r, + Member (Logger String) r, + Member AssIDStore r, + Member VerdictFormatStore r, + Member AReqIDStore r, + Member SAML2 r, + Member SamlProtocolSettings r, + Member (Error SparError) r, + Member IdPConfigStore r + ) => NominalDiffTime -> Maybe URI.URI -> Maybe URI.URI -> @@ -292,24 +285,22 @@ validateRedirectURL uri = do authresp :: forall r. - Members - '[ Random, - Logger String, - Input Opts, - GalleyAccess, - BrigAccess, - AssIDStore, - VerdictFormatStore, - AReqIDStore, - ScimTokenStore, - IdPConfigStore, - SAML2, - SamlProtocolSettings, - Error SparError, - Reporter, - SAMLUserStore - ] - r => + ( Member Random r, + Member (Logger String) r, + Member (Input Opts) r, + Member GalleyAccess r, + Member BrigAccess r, + Member AssIDStore r, + Member VerdictFormatStore r, + Member AReqIDStore r, + Member ScimTokenStore r, + Member IdPConfigStore r, + Member SAML2 r, + Member SamlProtocolSettings r, + Member (Error SparError) r, + Member Reporter r, + Member SAMLUserStore r + ) => Maybe TeamId -> SAML.AuthnResponseBody -> Sem r Void @@ -337,15 +328,13 @@ ssoSettings = -- IdPConfigStore API idpGet :: - Members - '[ Random, - Logger String, - GalleyAccess, - BrigAccess, - IdPConfigStore, - Error SparError - ] - r => + ( Member Random r, + Member (Logger String) r, + Member GalleyAccess r, + Member BrigAccess r, + Member IdPConfigStore r, + Member (Error SparError) r + ) => Maybe UserId -> SAML.IdPId -> Sem r IdP @@ -355,14 +344,12 @@ idpGet zusr idpid = withDebugLog "idpGet" (Just . show . (^. SAML.idpId)) $ do pure idp idpGetRaw :: - Members - '[ GalleyAccess, - BrigAccess, - IdPConfigStore, - IdPRawMetadataStore, - Error SparError - ] - r => + ( Member GalleyAccess r, + Member BrigAccess r, + Member IdPConfigStore r, + Member IdPRawMetadataStore r, + Member (Error SparError) r + ) => Maybe UserId -> SAML.IdPId -> Sem r RawIdPMetadata @@ -374,15 +361,13 @@ idpGetRaw zusr idpid = do Nothing -> throwSparSem $ SparIdPNotFound (cs $ show idpid) idpGetAll :: - Members - '[ Random, - Logger String, - GalleyAccess, - BrigAccess, - IdPConfigStore, - Error SparError - ] - r => + ( Member Random r, + Member (Logger String) r, + Member GalleyAccess r, + Member BrigAccess r, + Member IdPConfigStore r, + Member (Error SparError) r + ) => Maybe UserId -> Sem r IdPList idpGetAll zusr = withDebugLog "idpGetAll" (const Nothing) $ do @@ -400,18 +385,16 @@ idpGetAll zusr = withDebugLog "idpGetAll" (const Nothing) $ do -- https://github.com/zinfra/backend-issues/issues/1314 idpDelete :: forall r. - Members - '[ Random, - Logger String, - GalleyAccess, - BrigAccess, - ScimTokenStore, - SAMLUserStore, - IdPConfigStore, - IdPRawMetadataStore, - Error SparError - ] - r => + ( Member Random r, + Member (Logger String) r, + Member GalleyAccess r, + Member BrigAccess r, + Member ScimTokenStore r, + Member SAMLUserStore r, + Member IdPConfigStore r, + Member IdPRawMetadataStore r, + Member (Error SparError) r + ) => Maybe UserId -> SAML.IdPId -> Maybe Bool -> @@ -477,17 +460,15 @@ idpDelete mbzusr idpid (fromMaybe False -> purge) = withDebugLog "idpDelete" (co -- | This handler only does the json parsing, and leaves all authorization checks and -- application logic to 'idpCreateXML'. idpCreate :: - Members - '[ Random, - Logger String, - GalleyAccess, - BrigAccess, - ScimTokenStore, - IdPRawMetadataStore, - IdPConfigStore, - Error SparError - ] - r => + ( Member Random r, + Member (Logger String) r, + Member GalleyAccess r, + Member BrigAccess r, + Member ScimTokenStore r, + Member IdPRawMetadataStore r, + Member IdPConfigStore r, + Member (Error SparError) r + ) => Maybe UserId -> IdPMetadataInfo -> Maybe SAML.IdPId -> @@ -498,17 +479,15 @@ idpCreate zusr (IdPMetadataValue raw xml) = idpCreateXML zusr raw xml -- | We generate a new UUID for each IdP used as IdPConfig's path, thereby ensuring uniqueness. idpCreateXML :: - Members - '[ Random, - Logger String, - GalleyAccess, - BrigAccess, - ScimTokenStore, - IdPConfigStore, - IdPRawMetadataStore, - Error SparError - ] - r => + ( Member Random r, + Member (Logger String) r, + Member GalleyAccess r, + Member BrigAccess r, + Member ScimTokenStore r, + Member IdPConfigStore r, + Member IdPRawMetadataStore r, + Member (Error SparError) r + ) => Maybe UserId -> Text -> SAML.IdPMetadata -> @@ -533,12 +512,10 @@ idpCreateXML zusr raw idpmeta mReplaces (fromMaybe defWireIdPAPIVersion -> apive -- credentials can be created. To fix this, we need to implement a way to associate scim -- tokens with IdPs. https://wearezeta.atlassian.net/browse/SQSERVICES-165 assertNoScimOrNoIdP :: - Members - '[ ScimTokenStore, - Error SparError, - IdPConfigStore - ] - r => + ( Member ScimTokenStore r, + Member (Error SparError) r, + Member IdPConfigStore r + ) => TeamId -> Sem r () assertNoScimOrNoIdP teamid = do @@ -572,13 +549,11 @@ assertNoScimOrNoIdP teamid = do validateNewIdP :: forall m r. (HasCallStack, m ~ Sem r) => - Members - '[ Random, - Logger String, - IdPConfigStore, - Error SparError - ] - r => + ( Member Random r, + Member (Logger String) r, + Member IdPConfigStore r, + Member (Error SparError) r + ) => WireIdPAPIVersion -> SAML.IdPMetadata -> TeamId -> @@ -617,16 +592,14 @@ validateNewIdP apiversion _idpMetadata teamId mReplaces handle = withDebugLog "v -- 'idpCreate', which is not a good reason. make this one function and pass around -- 'IdPMetadataInfo' directly where convenient. idpUpdate :: - Members - '[ Random, - Logger String, - GalleyAccess, - BrigAccess, - IdPConfigStore, - IdPRawMetadataStore, - Error SparError - ] - r => + ( Member Random r, + Member (Logger String) r, + Member GalleyAccess r, + Member BrigAccess r, + Member IdPConfigStore r, + Member IdPRawMetadataStore r, + Member (Error SparError) r + ) => Maybe UserId -> IdPMetadataInfo -> SAML.IdPId -> @@ -635,16 +608,14 @@ idpUpdate :: idpUpdate zusr (IdPMetadataValue raw xml) = idpUpdateXML zusr raw xml idpUpdateXML :: - Members - '[ Random, - Logger String, - GalleyAccess, - BrigAccess, - IdPConfigStore, - IdPRawMetadataStore, - Error SparError - ] - r => + ( Member Random r, + Member (Logger String) r, + Member GalleyAccess r, + Member BrigAccess r, + Member IdPConfigStore r, + Member IdPRawMetadataStore r, + Member (Error SparError) r + ) => Maybe UserId -> Text -> SAML.IdPMetadata -> @@ -677,15 +648,13 @@ idpUpdateXML zusr raw idpmeta idpid mHandle = withDebugLog "idpUpdateXML" (Just validateIdPUpdate :: forall m r. (HasCallStack, m ~ Sem r) => - Members - '[ Random, - Logger String, - GalleyAccess, - BrigAccess, - IdPConfigStore, - Error SparError - ] - r => + ( Member Random r, + Member (Logger String) r, + Member GalleyAccess r, + Member BrigAccess r, + Member IdPConfigStore r, + Member (Error SparError) r + ) => Maybe UserId -> SAML.IdPMetadata -> SAML.IdPId -> @@ -734,7 +703,12 @@ withDebugLog msg showval action = do pure val authorizeIdP :: - (HasCallStack, Members '[GalleyAccess, BrigAccess, Error SparError] r) => + ( HasCallStack, + ( Member GalleyAccess r, + Member BrigAccess r, + Member (Error SparError) r + ) + ) => Maybe UserId -> IdP -> Sem r (UserId, TeamId) @@ -757,18 +731,22 @@ internalStatus = pure NoContent -- | Cleanup handler that is called by Galley whenever a team is about to -- get deleted. -internalDeleteTeam :: Members '[ScimTokenStore, IdPConfigStore, SAMLUserStore] r => TeamId -> Sem r NoContent +internalDeleteTeam :: + ( Member ScimTokenStore r, + Member IdPConfigStore r, + Member SAMLUserStore r + ) => + TeamId -> + Sem r NoContent internalDeleteTeam team = do deleteTeam team pure NoContent internalPutSsoSettings :: - Members - '[ DefaultSsoCode, - Error SparError, - IdPConfigStore - ] - r => + ( Member DefaultSsoCode r, + Member (Error SparError) r, + Member IdPConfigStore r + ) => SsoSettings -> Sem r NoContent internalPutSsoSettings SsoSettings {defaultSsoCode = Nothing} = do @@ -782,7 +760,7 @@ internalPutSsoSettings SsoSettings {defaultSsoCode = Just code} = *> DefaultSsoCode.store code $> NoContent -internalGetScimUserInfo :: Members '[ScimUserTimesStore] r => UserSet -> Sem r ScimUserInfos +internalGetScimUserInfo :: Member ScimUserTimesStore r => UserSet -> Sem r ScimUserInfos internalGetScimUserInfo (UserSet uids) = do results <- ScimUserTimesStore.readMulti (Set.toList uids) let scimUserInfos = results <&> (\(uid, t, _) -> ScimUserInfo uid (Just t)) diff --git a/services/spar/src/Spar/App.hs b/services/spar/src/Spar/App.hs index e92f3f41a9..01b09bd099 100644 --- a/services/spar/src/Spar/App.hs +++ b/services/spar/src/Spar/App.hs @@ -1,4 +1,6 @@ {-# LANGUAGE RecordWildCards #-} +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} {-# OPTIONS_GHC -fplugin=Polysemy.Plugin #-} -- This file is part of the Wire Server implementation. @@ -127,12 +129,23 @@ data Env = Env -- https://github.com/wireapp/wire-server/pull/1418) -- -- FUTUREWORK: https://wearezeta.atlassian.net/browse/SQSERVICES-1655 -getUserByUrefUnsafe :: Members '[BrigAccess, SAMLUserStore] r => SAML.UserRef -> Sem r (Maybe User) +getUserByUrefUnsafe :: + ( Member BrigAccess r, + Member SAMLUserStore r + ) => + SAML.UserRef -> + Sem r (Maybe User) getUserByUrefUnsafe uref = do maybe (pure Nothing) (Intra.getBrigUser Intra.WithPendingInvitations) =<< SAMLUserStore.get uref -- FUTUREWORK: Remove and reinstatate getUser, in AuthID refactoring PR -getUserIdByScimExternalId :: Members '[BrigAccess, ScimExternalIdStore] r => TeamId -> Email -> Sem r (Maybe UserId) +getUserIdByScimExternalId :: + ( Member BrigAccess r, + Member ScimExternalIdStore r + ) => + TeamId -> + Email -> + Sem r (Maybe UserId) getUserIdByScimExternalId tid email = do muid <- ScimExternalIdStore.lookup tid email case muid of @@ -159,12 +172,10 @@ getUserIdByScimExternalId tid email = do -- users that have an sso id, unless the request comes from spar. then we can make users -- undeletable in the team admin page, and ask admins to go talk to their IdP system. createSamlUserWithId :: - Members - '[ Error SparError, - BrigAccess, - SAMLUserStore - ] - r => + ( Member (Error SparError) r, + Member BrigAccess r, + Member SAMLUserStore r + ) => TeamId -> UserId -> SAML.UserRef -> @@ -181,15 +192,13 @@ createSamlUserWithId teamid buid suid role = do -- https://wearezeta.atlassian.net/browse/SQSERVICES-1655) autoprovisionSamlUser :: forall r. - Members - '[ GalleyAccess, - BrigAccess, - ScimTokenStore, - IdPConfigStore, - Error SparError, - SAMLUserStore - ] - r => + ( Member GalleyAccess r, + Member BrigAccess r, + Member ScimTokenStore r, + Member IdPConfigStore r, + Member (Error SparError) r, + Member SAMLUserStore r + ) => IdP -> UserId -> SAML.UserRef -> @@ -215,14 +224,29 @@ autoprovisionSamlUser idp buid suid = do -- | If user's 'NameID' is an email address and the team has email validation for SSO enabled, -- make brig initiate the email validate procedure. -validateEmailIfExists :: forall r. Members '[GalleyAccess, BrigAccess] r => UserId -> SAML.UserRef -> Sem r () +validateEmailIfExists :: + forall r. + ( Member GalleyAccess r, + Member BrigAccess r + ) => + UserId -> + SAML.UserRef -> + Sem r () validateEmailIfExists uid = \case (SAML.UserRef _ (view SAML.nameID -> UNameIDEmail email)) -> do mbTid <- Intra.getBrigUserTeam Intra.NoPendingInvitations uid validateEmail mbTid uid . Intra.emailFromSAML . CI.original $ email _ -> pure () -validateEmail :: forall r. Members '[GalleyAccess, BrigAccess] r => Maybe TeamId -> UserId -> Email -> Sem r () +validateEmail :: + forall r. + ( Member GalleyAccess r, + Member BrigAccess r + ) => + Maybe TeamId -> + UserId -> + Email -> + Sem r () validateEmail mbTid uid email = do enabled <- maybe (pure False) GalleyAccess.isEmailValidationEnabledTeam mbTid when enabled $ do @@ -239,20 +263,18 @@ validateEmail mbTid uid email = do -- latter. verdictHandler :: HasCallStack => - Members - '[ Random, - Logger String, - GalleyAccess, - BrigAccess, - AReqIDStore, - VerdictFormatStore, - ScimTokenStore, - IdPConfigStore, - Error SparError, - Reporter, - SAMLUserStore - ] - r => + ( Member Random r, + Member (Logger String) r, + Member GalleyAccess r, + Member BrigAccess r, + Member AReqIDStore r, + Member VerdictFormatStore r, + Member ScimTokenStore r, + Member IdPConfigStore r, + Member (Error SparError) r, + Member Reporter r, + Member SAMLUserStore r + ) => SAML.AuthnResponse -> SAML.AccessVerdict -> IdP -> @@ -283,18 +305,16 @@ data VerdictHandlerResult verdictHandlerResult :: HasCallStack => - Members - '[ Random, - Logger String, - GalleyAccess, - BrigAccess, - ScimTokenStore, - IdPConfigStore, - Error SparError, - Reporter, - SAMLUserStore - ] - r => + ( Member Random r, + Member (Logger String) r, + Member GalleyAccess r, + Member BrigAccess r, + Member ScimTokenStore r, + Member IdPConfigStore r, + Member (Error SparError) r, + Member Reporter r, + Member SAMLUserStore r + ) => SAML.AccessVerdict -> IdP -> Sem r VerdictHandlerResult @@ -306,11 +326,9 @@ verdictHandlerResult verdict idp = do catchVerdictErrors :: forall r. - Members - '[ Reporter, - Error SparError - ] - r => + ( Member Reporter r, + Member (Error SparError) r + ) => Sem r VerdictHandlerResult -> Sem r VerdictHandlerResult catchVerdictErrors = (`catch` hndlr) @@ -329,13 +347,11 @@ catchVerdictErrors = (`catch` hndlr) -- FUTUREWORK: https://wearezeta.atlassian.net/browse/SQSERVICES-1655 getUserByUrefViaOldIssuerUnsafe :: forall r. - Members - '[ BrigAccess, - IdPConfigStore, - SAMLUserStore, - Error SparError - ] - r => + ( Member BrigAccess r, + Member IdPConfigStore r, + Member SAMLUserStore r, + Member (Error SparError) r + ) => IdP -> SAML.UserRef -> Sem r (Maybe (SAML.UserRef, User)) @@ -350,7 +366,14 @@ getUserByUrefViaOldIssuerUnsafe idp (SAML.UserRef _ subject) = do -- | After a user has been found using 'findUserWithOldIssuer', update it everywhere so that -- the old IdP is not needed any more next time. -moveUserToNewIssuer :: Members '[BrigAccess, SAMLUserStore] r => SAML.UserRef -> SAML.UserRef -> UserId -> Sem r () +moveUserToNewIssuer :: + ( Member BrigAccess r, + Member SAMLUserStore r + ) => + SAML.UserRef -> + SAML.UserRef -> + UserId -> + Sem r () moveUserToNewIssuer oldUserRef newUserRef uid = do SAMLUserStore.insert newUserRef uid BrigAccess.setVeid uid (UrefOnly newUserRef) @@ -358,17 +381,15 @@ moveUserToNewIssuer oldUserRef newUserRef uid = do verdictHandlerResultCore :: HasCallStack => - Members - '[ Random, - Logger String, - GalleyAccess, - BrigAccess, - ScimTokenStore, - IdPConfigStore, - Error SparError, - SAMLUserStore - ] - r => + ( Member Random r, + Member (Logger String) r, + Member GalleyAccess r, + Member BrigAccess r, + Member ScimTokenStore r, + Member IdPConfigStore r, + Member (Error SparError) r, + Member SAMLUserStore r + ) => IdP -> SAML.AccessVerdict -> Sem r VerdictHandlerResult @@ -544,7 +565,12 @@ errorPage err mpInputs = -- | Delete all tokens belonging to a team. deleteTeam :: - (HasCallStack, Members '[ScimTokenStore, SAMLUserStore, IdPConfigStore] r) => + ( HasCallStack, + ( Member ScimTokenStore r, + Member SAMLUserStore r, + Member IdPConfigStore r + ) + ) => TeamId -> Sem r () deleteTeam team = do diff --git a/services/spar/src/Spar/Intra/Brig.hs b/services/spar/src/Spar/Intra/Brig.hs index 5ca89e612b..cc431a1d49 100644 --- a/services/spar/src/Spar/Intra/Brig.hs +++ b/services/spar/src/Spar/Intra/Brig.hs @@ -1,3 +1,6 @@ +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -336,7 +339,7 @@ checkHandleAvailable hnd = do -- | Call brig to delete a user. -- If the user wasn't deleted completely before, another deletion attempt will be made. -deleteBrigUserInternal :: (HasCallStack, MonadSparToBrig m, MonadIO m) => UserId -> m DeleteUserResult +deleteBrigUserInternal :: (HasCallStack, MonadSparToBrig m) => UserId -> m DeleteUserResult deleteBrigUserInternal buid = do resp <- call $ diff --git a/services/spar/src/Spar/Intra/BrigApp.hs b/services/spar/src/Spar/Intra/BrigApp.hs index 6043970a05..ad3c53310d 100644 --- a/services/spar/src/Spar/Intra/BrigApp.hs +++ b/services/spar/src/Spar/Intra/BrigApp.hs @@ -1,3 +1,5 @@ +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} {-# OPTIONS_GHC -fplugin=Polysemy.Plugin #-} -- This file is part of the Wire Server implementation. @@ -124,7 +126,14 @@ getBrigUserTeam ifpend = fmap (userTeam =<<) . getBrigUser ifpend -- permission check fails or the user is not in status 'Active'. getZUsrCheckPerm :: forall r perm. - (HasCallStack, Members '[BrigAccess, GalleyAccess, Error SparError] r, IsPerm perm, Show perm) => + ( HasCallStack, + ( Member BrigAccess r, + Member GalleyAccess r, + Member (Error SparError) r + ), + IsPerm perm, + Show perm + ) => Maybe UserId -> perm -> Sem r TeamId @@ -137,7 +146,12 @@ getZUsrCheckPerm (Just uid) perm = do authorizeScimTokenManagement :: forall r. - (HasCallStack, Members '[BrigAccess, GalleyAccess, Error SparError] r) => + ( HasCallStack, + ( Member BrigAccess r, + Member GalleyAccess r, + Member (Error SparError) r + ) + ) => Maybe UserId -> Sem r TeamId authorizeScimTokenManagement Nothing = throw $ SAML.CustomError SparMissingZUsr diff --git a/services/spar/src/Spar/Intra/Galley.hs b/services/spar/src/Spar/Intra/Galley.hs index a09eef5476..5592544991 100644 --- a/services/spar/src/Spar/Intra/Galley.hs +++ b/services/spar/src/Spar/Intra/Galley.hs @@ -1,3 +1,6 @@ +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -113,7 +116,7 @@ isEmailValidationEnabledTeam tid = do -- | Update a team member. updateTeamMember :: - (MonadIO m, HasCallStack, MonadError SparError m, MonadSparToGalley m) => + (MonadIO m, HasCallStack, MonadSparToGalley m) => UserId -> TeamId -> Role -> diff --git a/services/spar/src/Spar/Scim.hs b/services/spar/src/Spar/Scim.hs index 72c3afa041..43fba22000 100644 --- a/services/spar/src/Spar/Scim.hs +++ b/services/spar/src/Spar/Scim.hs @@ -70,7 +70,6 @@ import Polysemy.Error (Error, fromExceptionSem, runError, throw, try) import Polysemy.Input (Input) import qualified SAML2.WebSSO as SAML import Servant -import Servant.API.Generic import Servant.Server.Generic (AsServerT) import Spar.App (sparToServerErrorWithLogging, throwSparSem) import Spar.Error @@ -111,25 +110,26 @@ configuration = Scim.Meta.empty apiScim :: forall r. - Members - '[ Random, - Input Opts, - Logger (Msg -> Msg), - Logger String, - Now, - Error SparError, - GalleyAccess, - BrigAccess, - ScimExternalIdStore, - ScimUserTimesStore, - ScimTokenStore, - Reporter, - IdPConfigStore, - -- TODO(sandy): Only necessary for 'fromExceptionSem'. But can these errors even happen? - Final IO, - SAMLUserStore - ] - r => + ( Member Random r, + Member (Input Opts) r, + Member (Logger (Msg -> Msg)) r, + Member (Logger String) r, + Member Now r, + Member (Error SparError) r, + Member GalleyAccess r, + Member BrigAccess r, + Member ScimExternalIdStore r, + Member ScimUserTimesStore r, + Member ScimTokenStore r, + Member Reporter r, + Member IdPConfigStore r, + Member + ( -- TODO(sandy): Only necessary for 'fromExceptionSem'. But can these errors even happen? + Final IO + ) + r, + Member SAMLUserStore r + ) => ServerT APIScim (Sem r) apiScim = hoistScim (toServant (server configuration)) diff --git a/services/spar/src/Spar/Scim/Auth.hs b/services/spar/src/Spar/Scim/Auth.hs index 12081181f9..31c71987de 100644 --- a/services/spar/src/Spar/Scim/Auth.hs +++ b/services/spar/src/Spar/Scim/Auth.hs @@ -88,17 +88,15 @@ instance Member ScimTokenStore r => Scim.Class.Auth.AuthDB SparTag (Sem r) where -- | API for manipulating SCIM tokens (protected by normal Wire authentication and available -- only to team owners). apiScimToken :: - Members - '[ Random, - Input Opts, - GalleyAccess, - BrigAccess, - ScimTokenStore, - Now, - IdPConfigStore, - Error E.SparError - ] - r => + ( Member Random r, + Member (Input Opts) r, + Member GalleyAccess r, + Member BrigAccess r, + Member ScimTokenStore r, + Member Now r, + Member IdPConfigStore r, + Member (Error E.SparError) r + ) => ServerT APIScimToken (Sem r) apiScimToken = createScimToken @@ -110,17 +108,15 @@ apiScimToken = -- Create a token for user's team. createScimToken :: forall r. - Members - '[ Random, - Input Opts, - GalleyAccess, - BrigAccess, - ScimTokenStore, - IdPConfigStore, - Now, - Error E.SparError - ] - r => + ( Member Random r, + Member (Input Opts) r, + Member GalleyAccess r, + Member BrigAccess r, + Member ScimTokenStore r, + Member IdPConfigStore r, + Member Now r, + Member (Error E.SparError) r + ) => -- | Who is trying to create a token Maybe UserId -> -- | Request body @@ -171,7 +167,11 @@ createScimToken zusr Api.CreateScimToken {..} = do -- -- Delete a token belonging to user's team. deleteScimToken :: - Members '[GalleyAccess, BrigAccess, ScimTokenStore, Error E.SparError] r => + ( Member GalleyAccess r, + Member BrigAccess r, + Member ScimTokenStore r, + Member (Error E.SparError) r + ) => -- | Who is trying to delete a token Maybe UserId -> ScimTokenId -> @@ -186,7 +186,11 @@ deleteScimToken zusr tokenid = do -- List all tokens belonging to user's team. Tokens themselves are not available, only -- metadata about them. listScimTokens :: - Members '[GalleyAccess, BrigAccess, ScimTokenStore, Error E.SparError] r => + ( Member GalleyAccess r, + Member BrigAccess r, + Member ScimTokenStore r, + Member (Error E.SparError) r + ) => -- | Who is trying to list tokens Maybe UserId -> Sem r ScimTokenList diff --git a/services/spar/src/Spar/Scim/User.hs b/services/spar/src/Spar/Scim/User.hs index 2215547ca4..e42b00d95c 100644 --- a/services/spar/src/Spar/Scim/User.hs +++ b/services/spar/src/Spar/Scim/User.hs @@ -116,20 +116,18 @@ import qualified Wire.Sem.Random as Random -- UserDB instance instance - Members - '[ Logger (Msg -> Msg), - Logger String, - Random, - Input Opts, - Now, - GalleyAccess, - BrigAccess, - ScimExternalIdStore, - ScimUserTimesStore, - IdPConfigStore, - SAMLUserStore - ] - r => + ( Member (Logger (Msg -> Msg)) r, + Member (Logger String) r, + Member Random r, + Member (Input Opts) r, + Member Now r, + Member GalleyAccess r, + Member BrigAccess r, + Member ScimExternalIdStore r, + Member ScimUserTimesStore r, + Member IdPConfigStore r, + Member SAMLUserStore r + ) => Scim.UserDB ST.SparTag (Sem r) where getUsers :: @@ -200,7 +198,9 @@ instance validateScimUser :: forall m r. (m ~ Scim.ScimHandler (Sem r)) => - Members '[Input Opts, IdPConfigStore] r => + ( Member (Input Opts) r, + Member IdPConfigStore r + ) => Text -> -- | Used to decide what IdP to assign the user to ScimTokenInfo -> @@ -427,19 +427,17 @@ veidEmail (ST.EmailOnly email) = Just email createValidScimUser :: forall m r. (m ~ Scim.ScimHandler (Sem r)) => - Members - '[ Random, - Now, - Input Opts, - Logger (Msg -> Msg), - Logger String, - GalleyAccess, - BrigAccess, - ScimExternalIdStore, - ScimUserTimesStore, - SAMLUserStore - ] - r => + ( Member Random r, + Member Now r, + Member (Input Opts) r, + Member (Logger (Msg -> Msg)) r, + Member (Logger String) r, + Member GalleyAccess r, + Member BrigAccess r, + Member ScimExternalIdStore r, + Member ScimUserTimesStore r, + Member SAMLUserStore r + ) => ScimTokenInfo -> ST.ValidScimUser -> m (Scim.StoredUser ST.SparTag) @@ -542,20 +540,18 @@ createValidScimUserSpar stiTeam uid storedUser veid = lift $ do -- TODO(arianvp): how do we get this safe w.r.t. race conditions / crashes? updateValidScimUser :: forall m r. - Members - '[ Random, - Input Opts, - Logger (Msg -> Msg), - Logger String, - Now, - GalleyAccess, - BrigAccess, - ScimExternalIdStore, - ScimUserTimesStore, - IdPConfigStore, - SAMLUserStore - ] - r => + ( Member Random r, + Member (Input Opts) r, + Member (Logger (Msg -> Msg)) r, + Member (Logger String) r, + Member Now r, + Member GalleyAccess r, + Member BrigAccess r, + Member ScimExternalIdStore r, + Member ScimUserTimesStore r, + Member IdPConfigStore r, + Member SAMLUserStore r + ) => (m ~ Scim.ScimHandler (Sem r)) => ScimTokenInfo -> UserId -> @@ -620,13 +616,11 @@ updateValidScimUser tokinfo@ScimTokenInfo {stiTeam} uid nvsu = Scim.getUser tokinfo uid updateVsuUref :: - Members - '[ GalleyAccess, - BrigAccess, - ScimExternalIdStore, - SAMLUserStore - ] - r => + ( Member GalleyAccess r, + Member BrigAccess r, + Member ScimExternalIdStore r, + Member SAMLUserStore r + ) => TeamId -> UserId -> ST.ValidExternalId -> @@ -697,15 +691,13 @@ updScimStoredUser' now usr (Scim.WithMeta meta (Scim.WithId scimuid _)) = } deleteScimUser :: - Members - '[ Logger (Msg -> Msg), - BrigAccess, - ScimExternalIdStore, - ScimUserTimesStore, - SAMLUserStore, - IdPConfigStore - ] - r => + ( Member (Logger (Msg -> Msg)) r, + Member BrigAccess r, + Member ScimExternalIdStore r, + Member ScimUserTimesStore r, + Member SAMLUserStore r, + Member IdPConfigStore r + ) => ScimTokenInfo -> UserId -> Scim.ScimHandler (Sem r) () @@ -759,13 +751,11 @@ deleteScimUser tokeninfo@ScimTokenInfo {stiTeam, stiIdP} uid = pure () where deleteUserInSpar :: - Members - '[ IdPConfigStore, - SAMLUserStore, - ScimExternalIdStore, - ScimUserTimesStore - ] - r => + ( Member IdPConfigStore r, + Member SAMLUserStore r, + Member ScimExternalIdStore r, + Member ScimUserTimesStore r + ) => User -> Scim.ScimHandler (Sem r) () deleteUserInSpar brigUser = do @@ -810,7 +800,10 @@ calculateVersion uid usr = Scim.Weak (Text.pack (show h)) -- ASSUMPTION: every scim user has a 'SAML.UserRef', and the `SAML.NameID` in it corresponds -- to a single `externalId`. assertExternalIdUnused :: - Members '[BrigAccess, ScimExternalIdStore, SAMLUserStore] r => + ( Member BrigAccess r, + Member ScimExternalIdStore r, + Member SAMLUserStore r + ) => TeamId -> ST.ValidExternalId -> Scim.ScimHandler (Sem r) () @@ -823,7 +816,15 @@ assertExternalIdUnused = -- -- ASSUMPTION: every scim user has a 'SAML.UserRef', and the `SAML.NameID` in it corresponds -- to a single `externalId`. -assertExternalIdNotUsedElsewhere :: Members '[BrigAccess, ScimExternalIdStore, SAMLUserStore] r => TeamId -> ST.ValidExternalId -> UserId -> Scim.ScimHandler (Sem r) () +assertExternalIdNotUsedElsewhere :: + ( Member BrigAccess r, + Member ScimExternalIdStore r, + Member SAMLUserStore r + ) => + TeamId -> + ST.ValidExternalId -> + UserId -> + Scim.ScimHandler (Sem r) () assertExternalIdNotUsedElsewhere tid veid wireUserId = assertExternalIdInAllowedValues [Nothing, Just wireUserId] @@ -831,7 +832,16 @@ assertExternalIdNotUsedElsewhere tid veid wireUserId = tid veid -assertExternalIdInAllowedValues :: Members '[BrigAccess, ScimExternalIdStore, SAMLUserStore] r => [Maybe UserId] -> Text -> TeamId -> ST.ValidExternalId -> Scim.ScimHandler (Sem r) () +assertExternalIdInAllowedValues :: + ( Member BrigAccess r, + Member ScimExternalIdStore r, + Member SAMLUserStore r + ) => + [Maybe UserId] -> + Text -> + TeamId -> + ST.ValidExternalId -> + Scim.ScimHandler (Sem r) () assertExternalIdInAllowedValues allowedValues errmsg tid veid = do isGood <- lift $ @@ -863,15 +873,13 @@ assertHandleNotUsedElsewhere uid hndl = do -- stamps. synthesizeStoredUser :: forall r. - Members - '[ Input Opts, - Now, - Logger (Msg -> Msg), - BrigAccess, - GalleyAccess, - ScimUserTimesStore - ] - r => + ( Member (Input Opts) r, + Member Now r, + Member (Logger (Msg -> Msg)) r, + Member BrigAccess r, + Member GalleyAccess r, + Member ScimUserTimesStore r + ) => UserAccount -> ST.ValidExternalId -> Scim.ScimHandler (Sem r) (Scim.StoredUser ST.SparTag) diff --git a/services/spar/src/Spar/Sem/AReqIDStore/Cassandra.hs b/services/spar/src/Spar/Sem/AReqIDStore/Cassandra.hs index 8c04ed8692..87a2ef9e55 100644 --- a/services/spar/src/Spar/Sem/AReqIDStore/Cassandra.hs +++ b/services/spar/src/Spar/Sem/AReqIDStore/Cassandra.hs @@ -1,3 +1,6 @@ +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -38,7 +41,14 @@ import qualified Wire.Sem.Now as Now aReqIDStoreToCassandra :: forall m r a. - (MonadClient m, Members '[Embed m, Now, Error TTLError, Embed IO, Input Opts] r) => + ( MonadClient m, + ( Member (Embed m) r, + Member Now r, + Member (Error TTLError) r, + Member (Embed IO) r, + Member (Input Opts) r + ) + ) => Sem (AReqIDStore ': r) a -> Sem r a aReqIDStoreToCassandra = interpret $ \case diff --git a/services/spar/src/Spar/Sem/AssIDStore/Cassandra.hs b/services/spar/src/Spar/Sem/AssIDStore/Cassandra.hs index 1465bf8aaf..877b2b3b6f 100644 --- a/services/spar/src/Spar/Sem/AssIDStore/Cassandra.hs +++ b/services/spar/src/Spar/Sem/AssIDStore/Cassandra.hs @@ -1,3 +1,6 @@ +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -38,7 +41,14 @@ import qualified Wire.Sem.Now as Now assIDStoreToCassandra :: forall m r a. - (MonadClient m, Members '[Embed m, Now, Error TTLError, Embed IO, Input Opts] r) => + ( MonadClient m, + ( Member (Embed m) r, + Member Now r, + Member (Error TTLError) r, + Member (Embed IO) r, + Member (Input Opts) r + ) + ) => Sem (AssIDStore ': r) a -> Sem r a assIDStoreToCassandra = diff --git a/services/spar/src/Spar/Sem/BrigAccess/Http.hs b/services/spar/src/Spar/Sem/BrigAccess/Http.hs index 0cd47ba97d..0331cca84d 100644 --- a/services/spar/src/Spar/Sem/BrigAccess/Http.hs +++ b/services/spar/src/Spar/Sem/BrigAccess/Http.hs @@ -32,7 +32,10 @@ import qualified System.Logger as TinyLog import Wire.Sem.Logger (Logger) brigAccessToHttp :: - Members '[Logger (TinyLog.Msg -> TinyLog.Msg), Error SparError, Embed IO] r => + ( Member (Logger (TinyLog.Msg -> TinyLog.Msg)) r, + Member (Error SparError) r, + Member (Embed IO) r + ) => Bilge.Manager -> Bilge.Request -> Sem (BrigAccess ': r) a -> diff --git a/services/spar/src/Spar/Sem/DefaultSsoCode/Cassandra.hs b/services/spar/src/Spar/Sem/DefaultSsoCode/Cassandra.hs index d501064368..5a4e845584 100644 --- a/services/spar/src/Spar/Sem/DefaultSsoCode/Cassandra.hs +++ b/services/spar/src/Spar/Sem/DefaultSsoCode/Cassandra.hs @@ -1,3 +1,5 @@ +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} {-# OPTIONS_GHC -fplugin=Polysemy.Plugin #-} -- This file is part of the Wire Server implementation. diff --git a/services/spar/src/Spar/Sem/GalleyAccess/Http.hs b/services/spar/src/Spar/Sem/GalleyAccess/Http.hs index f50e01bc25..793bac9c27 100644 --- a/services/spar/src/Spar/Sem/GalleyAccess/Http.hs +++ b/services/spar/src/Spar/Sem/GalleyAccess/Http.hs @@ -34,7 +34,10 @@ import qualified System.Logger as TinyLog import Wire.Sem.Logger (Logger) galleyAccessToHttp :: - Members '[Logger (TinyLog.Msg -> TinyLog.Msg), Error SparError, Embed IO] r => + ( Member (Logger (TinyLog.Msg -> TinyLog.Msg)) r, + Member (Error SparError) r, + Member (Embed IO) r + ) => Bilge.Manager -> Bilge.Request -> Sem (GalleyAccess ': r) a -> diff --git a/services/spar/src/Spar/Sem/IdPConfigStore/Cassandra.hs b/services/spar/src/Spar/Sem/IdPConfigStore/Cassandra.hs index d071cccf9f..2b3d347007 100644 --- a/services/spar/src/Spar/Sem/IdPConfigStore/Cassandra.hs +++ b/services/spar/src/Spar/Sem/IdPConfigStore/Cassandra.hs @@ -1,4 +1,5 @@ {-# LANGUAGE RecordWildCards #-} +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- @@ -306,7 +307,7 @@ getIdPConfigsByTeam team = getIdPIdsByTeam team >>= mapM getIdPConfig getIdPIdsByTeam :: - (HasCallStack, MonadClient m, MonadError IdpDbError m) => + (HasCallStack, MonadClient m) => TeamId -> m [SAML.IdPId] getIdPIdsByTeam team = do diff --git a/services/spar/src/Spar/Sem/IdPRawMetadataStore/Cassandra.hs b/services/spar/src/Spar/Sem/IdPRawMetadataStore/Cassandra.hs index e3e8111bc9..84a647baa2 100644 --- a/services/spar/src/Spar/Sem/IdPRawMetadataStore/Cassandra.hs +++ b/services/spar/src/Spar/Sem/IdPRawMetadataStore/Cassandra.hs @@ -1,3 +1,6 @@ +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH diff --git a/services/spar/src/Spar/Sem/Reporter/Wai.hs b/services/spar/src/Spar/Sem/Reporter/Wai.hs index a5ae93fe5c..e2d0b66ef6 100644 --- a/services/spar/src/Spar/Sem/Reporter/Wai.hs +++ b/services/spar/src/Spar/Sem/Reporter/Wai.hs @@ -27,7 +27,12 @@ import Polysemy.Input import Spar.Sem.Reporter import qualified System.Logger as TinyLog -reporterToTinyLogWai :: Members '[Embed IO, Input TinyLog.Logger] r => Sem (Reporter ': r) a -> Sem r a +reporterToTinyLogWai :: + ( Member (Embed IO) r, + Member (Input TinyLog.Logger) r + ) => + Sem (Reporter ': r) a -> + Sem r a reporterToTinyLogWai = interpret $ \case Report req err -> do logger <- input diff --git a/services/spar/src/Spar/Sem/SAML2/Library.hs b/services/spar/src/Spar/Sem/SAML2/Library.hs index 993b0fa407..3d2ac1a32a 100644 --- a/services/spar/src/Spar/Sem/SAML2/Library.hs +++ b/services/spar/src/Spar/Sem/SAML2/Library.hs @@ -46,7 +46,12 @@ import Wire.API.User.IdentityProvider (WireIdP) import Wire.Sem.Logger (Logger) import qualified Wire.Sem.Logger as Logger -wrapMonadClientSPImpl :: Members '[Error SparError, Final IO] r => Sem r a -> SPImpl r a +wrapMonadClientSPImpl :: + ( Member (Error SparError) r, + Member (Final IO) r + ) => + Sem r a -> + SPImpl r a wrapMonadClientSPImpl action = SPImpl action `Catch.catch` (SPImpl . throw . SAML.CustomError . SparCassandraError . cs . show @SomeException) @@ -68,7 +73,10 @@ newtype SPImpl r a = SPImpl {unSPImpl :: Sem r a} instance Member (Input Opts) r => HasConfig (SPImpl r) where getConfig = SPImpl $ inputs saml -instance Members '[Input Opts, Logger String] r => HasLogger (SPImpl r) where +instance + Member (Logger String) r => + HasLogger (SPImpl r) + where logger lvl = SPImpl . Logger.log (Logger.samlFromLevel lvl) instance Member (Embed IO) r => MonadIO (SPImpl r) where @@ -78,17 +86,35 @@ instance Member (Embed IO) r => HasCreateUUID (SPImpl r) instance Member (Embed IO) r => HasNow (SPImpl r) -instance Members '[Error SparError, Final IO, AReqIDStore] r => SPStoreID AuthnRequest (SPImpl r) where +instance + ( Member (Error SparError) r, + Member (Final IO) r, + Member AReqIDStore r + ) => + SPStoreID AuthnRequest (SPImpl r) + where storeID = (wrapMonadClientSPImpl .) . AReqIDStore.store unStoreID = wrapMonadClientSPImpl . AReqIDStore.unStore isAliveID = wrapMonadClientSPImpl . AReqIDStore.isAlive -instance Members '[Error SparError, Final IO, AssIDStore] r => SPStoreID Assertion (SPImpl r) where +instance + ( Member (Error SparError) r, + Member (Final IO) r, + Member AssIDStore r + ) => + SPStoreID Assertion (SPImpl r) + where storeID = (wrapMonadClientSPImpl .) . AssIDStore.store unStoreID = wrapMonadClientSPImpl . AssIDStore.unStore isAliveID = wrapMonadClientSPImpl . AssIDStore.isAlive -instance Members '[Error SparError, IdPConfigStore, Final IO] r => SPStoreIdP SparError (SPImpl r) where +instance + ( Member (Error SparError) r, + Member IdPConfigStore r, + Member (Final IO) r + ) => + SPStoreIdP SparError (SPImpl r) + where type IdPConfigExtra (SPImpl r) = WireIdP type IdPConfigSPId (SPImpl r) = TeamId @@ -107,17 +133,15 @@ instance Member (Error SparError) r => MonadError SparError (SPImpl r) where -- * https://reasonablypolymorphic.com/blog/tactics/ saml2ToSaml2WebSso :: forall r a. - Members - '[ AReqIDStore, - AssIDStore, - Error SparError, - IdPConfigStore, - Input Opts, - Logger String, - Embed IO, - Final IO - ] - r => + ( Member AReqIDStore r, + Member AssIDStore r, + Member (Error SparError) r, + Member IdPConfigStore r, + Member (Input Opts) r, + Member (Logger String) r, + Member (Embed IO) r, + Member (Final IO) r + ) => Sem (SAML2 ': r) a -> Sem r a saml2ToSaml2WebSso = @@ -147,17 +171,15 @@ saml2ToSaml2WebSso = liftT $ unSPImpl $ SAML.toggleCookie sbs mp inspectOrBomb :: - Members - '[ AReqIDStore, - AssIDStore, - Error SparError, - IdPConfigStore, - Logger String, - Input Opts, - Embed IO, - Final IO - ] - r => + ( Member AReqIDStore r, + Member AssIDStore r, + Member (Error SparError) r, + Member IdPConfigStore r, + Member (Logger String) r, + Member (Input Opts) r, + Member (Embed IO) r, + Member (Final IO) r + ) => Inspector f -> Sem (SAML2 : r) (f b) -> SPImpl r b diff --git a/services/spar/src/Spar/Sem/SAMLUserStore/Cassandra.hs b/services/spar/src/Spar/Sem/SAMLUserStore/Cassandra.hs index 9630c97e44..b724907427 100644 --- a/services/spar/src/Spar/Sem/SAMLUserStore/Cassandra.hs +++ b/services/spar/src/Spar/Sem/SAMLUserStore/Cassandra.hs @@ -1,3 +1,5 @@ +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} {-# OPTIONS_GHC -fplugin=Polysemy.Plugin #-} -- This file is part of the Wire Server implementation. diff --git a/services/spar/src/Spar/Sem/ScimExternalIdStore/Cassandra.hs b/services/spar/src/Spar/Sem/ScimExternalIdStore/Cassandra.hs index 2753520860..6dad02d5fa 100644 --- a/services/spar/src/Spar/Sem/ScimExternalIdStore/Cassandra.hs +++ b/services/spar/src/Spar/Sem/ScimExternalIdStore/Cassandra.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH diff --git a/services/spar/src/Spar/Sem/ScimExternalIdStore/Spec.hs b/services/spar/src/Spar/Sem/ScimExternalIdStore/Spec.hs index d8d9adb7a3..7593f11c7e 100644 --- a/services/spar/src/Spar/Sem/ScimExternalIdStore/Spec.hs +++ b/services/spar/src/Spar/Sem/ScimExternalIdStore/Spec.hs @@ -53,7 +53,7 @@ class PropConstraints r f instance - (Arbitrary UserId, CoArbitrary UserId, Functor f, Member E.ScimExternalIdStore r, forall z. Show z => Show (f z), forall z. Eq z => Eq (f z)) => + (CoArbitrary UserId, Functor f, Member E.ScimExternalIdStore r, forall z. Show z => Show (f z), forall z. Eq z => Eq (f z)) => PropConstraints r f prop_insertLookup :: diff --git a/services/spar/src/Spar/Sem/ScimTokenStore/Cassandra.hs b/services/spar/src/Spar/Sem/ScimTokenStore/Cassandra.hs index 54b94fe908..02480ac426 100644 --- a/services/spar/src/Spar/Sem/ScimTokenStore/Cassandra.hs +++ b/services/spar/src/Spar/Sem/ScimTokenStore/Cassandra.hs @@ -1,4 +1,6 @@ {-# LANGUAGE RecordWildCards #-} +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} {-# OPTIONS_GHC -fplugin=Polysemy.Plugin #-} -- This file is part of the Wire Server implementation. diff --git a/services/spar/src/Spar/Sem/ScimUserTimesStore/Cassandra.hs b/services/spar/src/Spar/Sem/ScimUserTimesStore/Cassandra.hs index 52dfbe11f2..bd3c45cbce 100644 --- a/services/spar/src/Spar/Sem/ScimUserTimesStore/Cassandra.hs +++ b/services/spar/src/Spar/Sem/ScimUserTimesStore/Cassandra.hs @@ -1,3 +1,6 @@ +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH diff --git a/services/spar/src/Spar/Sem/Utils.hs b/services/spar/src/Spar/Sem/Utils.hs index d6cb57d840..0f43096178 100644 --- a/services/spar/src/Spar/Sem/Utils.hs +++ b/services/spar/src/Spar/Sem/Utils.hs @@ -48,7 +48,9 @@ import qualified Wire.Sem.Logger as Logger -- | Run an embedded Cassandra 'Client' in @Final IO@. interpretClientToIO :: - Members '[Error SparError, Final IO] r => + ( Member (Error SparError) r, + Member (Final IO) r + ) => ClientState -> Sem (Embed Client ': r) a -> Sem r a @@ -89,7 +91,7 @@ semToRunHttp :: Sem r a -> RunHttp r a semToRunHttp = RunHttp . lift . lift . lift viaRunHttp :: - Members '[Error SparError, Embed IO] r => + Member (Error SparError) r => RunHttpEnv r -> RunHttp r a -> Sem r a @@ -102,12 +104,22 @@ viaRunHttp env m = do instance Member (Logger (TinyLog.Msg -> TinyLog.Msg)) r => TinyLog.MonadLogger (RunHttp r) where log lvl msg = semToRunHttp $ Logger.log (Logger.fromLevel lvl) msg -instance Members '[Logger (TinyLog.Msg -> TinyLog.Msg), Embed IO] r => MonadSparToGalley (RunHttp r) where +instance + ( Member (Logger (TinyLog.Msg -> TinyLog.Msg)) r, + Member (Embed IO) r + ) => + MonadSparToGalley (RunHttp r) + where call modreq = do req <- asks rheRequest httpLbs req modreq -instance Members '[Logger (TinyLog.Msg -> TinyLog.Msg), Embed IO] r => MonadSparToBrig (RunHttp r) where +instance + ( Member (Logger (TinyLog.Msg -> TinyLog.Msg)) r, + Member (Embed IO) r + ) => + MonadSparToBrig (RunHttp r) + where call modreq = do req <- asks rheRequest httpLbs req modreq diff --git a/services/spar/src/Spar/Sem/VerdictFormatStore/Cassandra.hs b/services/spar/src/Spar/Sem/VerdictFormatStore/Cassandra.hs index 84fde20e40..c396fb28fb 100644 --- a/services/spar/src/Spar/Sem/VerdictFormatStore/Cassandra.hs +++ b/services/spar/src/Spar/Sem/VerdictFormatStore/Cassandra.hs @@ -1,3 +1,6 @@ +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH diff --git a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs index 342fbbb5cc..7a7f9a9964 100644 --- a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs @@ -736,9 +736,7 @@ testCreateUserNoIdP = do -- | ES is only refreshed occasionally; we don't want to wait for that in tests. refreshIndex :: BrigReq -> TestSpar () refreshIndex brig = do - call $ void $ post (brig . path "/i/index/reindex" . expect2xx) - -- wait for async reindexing to complete (hopefully) - lift $ threadDelay 3_000_000 + call $ void $ post (brig . path "/i/index/refresh" . expect2xx) testCreateUserNoIdPNoEmail :: TestSpar () testCreateUserNoIdPNoEmail = do diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index b28898d87a..322d810286 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -1,5 +1,7 @@ {-# LANGUAGE RecordWildCards #-} {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} {-# OPTIONS_GHC -fplugin=Polysemy.Plugin #-} -- This file is part of the Wire Server implementation. @@ -386,7 +388,7 @@ createUserWithTeamDisableSSO brg gly = do pure () pure (uid, tid) -getSSOEnabledInternal :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyReq -> TeamId -> m ResponseLBS +getSSOEnabledInternal :: (HasCallStack, MonadHttp m) => GalleyReq -> TeamId -> m ResponseLBS getSSOEnabledInternal gly tid = do get $ gly @@ -441,7 +443,7 @@ inviteAndRegisterUser brig u tid inviteeEmail = do ] -- postInvitation :: - (MonadIO m, MonadHttp m, HasCallStack) => + (MonadHttp m, HasCallStack) => TeamId -> UserId -> TeamInvitation.InvitationRequest -> @@ -500,7 +502,7 @@ createTeamMember brigreq galleyreq teamid perms = do -- that builds up the internal structure from scratch, without too much thought. For -- instance, the team field in the user in brig won't be updated. addTeamMember :: - (HasCallStack, MonadCatch m, MonadIO m, MonadHttp m) => + (HasCallStack, MonadCatch m, MonadHttp m) => GalleyReq -> TeamId -> NewTeamMember -> @@ -517,7 +519,7 @@ addTeamMember galleyreq tid mem = -- | Delete a user from Brig and wait until it's gone. deleteUserOnBrig :: - (HasCallStack, MonadMask m, MonadCatch m, MonadIO m, MonadHttp m) => + (HasCallStack, MonadMask m, MonadIO m, MonadHttp m) => BrigReq -> UserId -> m () @@ -529,7 +531,7 @@ deleteUserOnBrig brigreq uid = do -- | Delete a user from Brig but don't wait. deleteUserNoWait :: - (HasCallStack, MonadCatch m, MonadIO m, MonadHttp m) => + (HasCallStack, MonadCatch m, MonadHttp m) => BrigReq -> UserId -> m () @@ -709,7 +711,7 @@ getActivationCode brig_ ep = do pure $ (,) <$> akey <*> acode activate :: - (HasCallStack, MonadIO m, MonadHttp m) => + (HasCallStack, MonadHttp m) => BrigReq -> ActivationPair -> m ResponseLBS @@ -1018,15 +1020,15 @@ parseAuthnReqResp (Just raw) = do >>= either (throwError . show) pure . SAML.decodeElem . cs pure (reqUri, reqBody) -safeHead :: forall n a. (MonadError String n, Show a) => String -> [a] -> n a +safeHead :: forall n a. (MonadError String n) => String -> [a] -> n a safeHead _ (a : _) = pure a safeHead msg [] = throwError $ msg <> ": []" -callAuthnReq' :: (MonadIO m, MonadHttp m) => SparReq -> SAML.IdPId -> m ResponseLBS +callAuthnReq' :: MonadHttp m => SparReq -> SAML.IdPId -> m ResponseLBS callAuthnReq' sparreq_ idpid = do get $ sparreq_ . path (cs $ "/sso/initiate-login/" -/ SAML.idPIdToST idpid) -callAuthnReqPrecheck' :: (MonadIO m, MonadHttp m) => SparReq -> SAML.IdPId -> m ResponseLBS +callAuthnReqPrecheck' :: MonadHttp m => SparReq -> SAML.IdPId -> m ResponseLBS callAuthnReqPrecheck' sparreq_ idpid = do head $ sparreq_ . path (cs $ "/sso/initiate-login/" -/ SAML.idPIdToST idpid) @@ -1036,7 +1038,7 @@ callIdpGet sparreq_ muid idpid = do either (liftIO . throwIO . ErrorCall . show) pure $ responseJsonEither @IdP resp -callIdpGet' :: (MonadIO m, MonadHttp m) => SparReq -> Maybe UserId -> SAML.IdPId -> m ResponseLBS +callIdpGet' :: MonadHttp m => SparReq -> Maybe UserId -> SAML.IdPId -> m ResponseLBS callIdpGet' sparreq_ muid idpid = do get $ sparreq_ . maybe id zUser muid . path (cs $ "/identity-providers/" -/ SAML.idPIdToST idpid) @@ -1045,7 +1047,7 @@ callIdpGetRaw sparreq_ muid idpid = do resp <- callIdpGetRaw' (sparreq_ . expect2xx) muid idpid maybe (liftIO . throwIO $ ErrorCall "Nothing") (pure . cs) (responseBody resp) -callIdpGetRaw' :: (MonadIO m, MonadHttp m) => SparReq -> Maybe UserId -> SAML.IdPId -> m ResponseLBS +callIdpGetRaw' :: MonadHttp m => SparReq -> Maybe UserId -> SAML.IdPId -> m ResponseLBS callIdpGetRaw' sparreq_ muid idpid = do get $ sparreq_ . maybe id zUser muid . path (cs $ "/identity-providers/" -/ SAML.idPIdToST idpid -/ "raw") @@ -1055,7 +1057,7 @@ callIdpGetAll sparreq_ muid = do either (liftIO . throwIO . ErrorCall . show) pure $ responseJsonEither resp -callIdpGetAll' :: (MonadIO m, MonadHttp m) => SparReq -> Maybe UserId -> m ResponseLBS +callIdpGetAll' :: MonadHttp m => SparReq -> Maybe UserId -> m ResponseLBS callIdpGetAll' sparreq_ muid = do get $ sparreq_ . maybe id zUser muid . path "/identity-providers" @@ -1088,7 +1090,7 @@ callIdpCreateRaw sparreq_ muid ctyp metadata = do either (liftIO . throwIO . ErrorCall . show) pure $ responseJsonEither @IdP resp -callIdpCreateRaw' :: (MonadIO m, MonadHttp m) => SparReq -> Maybe UserId -> SBS -> LBS -> m ResponseLBS +callIdpCreateRaw' :: MonadHttp m => SparReq -> Maybe UserId -> SBS -> LBS -> m ResponseLBS callIdpCreateRaw' sparreq_ muid ctyp metadata = do post $ sparreq_ @@ -1153,7 +1155,7 @@ callIdpCreateReplace' apiversion sparreq_ muid metadata idpid = do . body (RequestBodyLBS . cs $ SAML.encode metadata) . header "Content-Type" "application/xml" -callIdpUpdate :: (MonadIO m, MonadHttp m) => SparReq -> Maybe UserId -> IdPId -> IdPMetadataInfo -> m ResponseLBS +callIdpUpdate :: MonadHttp m => SparReq -> Maybe UserId -> IdPId -> IdPMetadataInfo -> m ResponseLBS callIdpUpdate sparreq_ muid idpid (IdPMetadataValue metadata _) = do put $ sparreq_ @@ -1162,7 +1164,7 @@ callIdpUpdate sparreq_ muid idpid (IdPMetadataValue metadata _) = do . body (RequestBodyLBS $ cs metadata) . header "Content-Type" "application/xml" -callIdpUpdateWithHandle :: (MonadIO m, MonadHttp m) => SparReq -> Maybe UserId -> IdPId -> IdPMetadataInfo -> IdPHandle -> m ResponseLBS +callIdpUpdateWithHandle :: MonadHttp m => SparReq -> Maybe UserId -> IdPId -> IdPMetadataInfo -> IdPHandle -> m ResponseLBS callIdpUpdateWithHandle sparreq_ muid idpid (IdPMetadataValue metadata _) idpHandle = do put $ sparreq_ @@ -1172,17 +1174,17 @@ callIdpUpdateWithHandle sparreq_ muid idpid (IdPMetadataValue metadata _) idpHan . body (RequestBodyLBS $ cs metadata) . header "Content-Type" "application/xml" -callIdpDelete :: (MonadIO m, MonadHttp m) => SparReq -> Maybe UserId -> SAML.IdPId -> m () +callIdpDelete :: (Functor m, MonadHttp m) => SparReq -> Maybe UserId -> SAML.IdPId -> m () callIdpDelete sparreq_ muid idpid = void $ callIdpDelete' (sparreq_ . expect2xx) muid idpid -callIdpDelete' :: (MonadIO m, MonadHttp m) => SparReq -> Maybe UserId -> SAML.IdPId -> m ResponseLBS +callIdpDelete' :: MonadHttp m => SparReq -> Maybe UserId -> SAML.IdPId -> m ResponseLBS callIdpDelete' sparreq_ muid idpid = do delete $ sparreq_ . maybe id zUser muid . path (cs $ "/identity-providers/" -/ SAML.idPIdToST idpid) -callIdpDeletePurge' :: (MonadIO m, MonadHttp m) => SparReq -> Maybe UserId -> SAML.IdPId -> m ResponseLBS +callIdpDeletePurge' :: MonadHttp m => SparReq -> Maybe UserId -> SAML.IdPId -> m ResponseLBS callIdpDeletePurge' sparreq_ muid idpid = do delete $ sparreq_ @@ -1190,13 +1192,13 @@ callIdpDeletePurge' sparreq_ muid idpid = do . path (cs $ "/identity-providers/" -/ SAML.idPIdToST idpid) . queryItem "purge" "true" -callGetDefaultSsoCode :: (MonadIO m, MonadHttp m) => SparReq -> m ResponseLBS +callGetDefaultSsoCode :: MonadHttp m => SparReq -> m ResponseLBS callGetDefaultSsoCode sparreq_ = do get $ sparreq_ . path "/sso/settings/" -callSetDefaultSsoCode :: (MonadIO m, MonadHttp m) => SparReq -> SAML.IdPId -> m ResponseLBS +callSetDefaultSsoCode :: MonadHttp m => SparReq -> SAML.IdPId -> m ResponseLBS callSetDefaultSsoCode sparreq_ ssoCode = do let settings = RequestBodyLBS . Aeson.encode $ @@ -1209,7 +1211,7 @@ callSetDefaultSsoCode sparreq_ ssoCode = do . body settings . header "Content-Type" "application/json" -callDeleteDefaultSsoCode :: (MonadIO m, MonadHttp m) => SparReq -> m ResponseLBS +callDeleteDefaultSsoCode :: MonadHttp m => SparReq -> m ResponseLBS callDeleteDefaultSsoCode sparreq_ = do let settings = RequestBodyLBS . Aeson.encode $ @@ -1298,7 +1300,7 @@ stdInvitationRequest' loc role email = TeamInvitation.InvitationRequest loc role Nothing email Nothing changeHandleBrig :: - (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => + (MonadHttp m, HasCallStack) => BrigReq -> UserId -> Text -> @@ -1314,7 +1316,7 @@ changeHandleBrig brig uid handlTxt = do ) updateProfileBrig :: - (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => + (MonadHttp m, HasCallStack) => BrigReq -> UserId -> UserUpdate -> diff --git a/services/spar/test-integration/Util/Email.hs b/services/spar/test-integration/Util/Email.hs index b41ff0951a..6e72c45d81 100644 --- a/services/spar/test-integration/Util/Email.hs +++ b/services/spar/test-integration/Util/Email.hs @@ -1,3 +1,6 @@ +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -60,7 +63,7 @@ changeEmailBrig brig usr newEmail = do Auth.PasswordLogin $ Auth.PasswordLoginData (Auth.LoginByEmail e) pw cl Nothing - login :: Auth.Login -> Auth.CookieType -> (MonadIO m, MonadHttp m) => m ResponseLBS + login :: Auth.Login -> Auth.CookieType -> MonadHttp m => m ResponseLBS login l t = post $ brig @@ -78,7 +81,7 @@ changeEmailBrig brig usr newEmail = do fromByteString (encodeUtf8 t) changeEmailBrigCreds :: - (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => + (MonadHttp m, HasCallStack) => BrigReq -> Cookie -> ZAuth.Token ZAuth.Access -> @@ -100,7 +103,7 @@ forceCookie :: Cookie -> Request -> Request forceCookie cky = header "Cookie" $ cookie_name cky <> "=" <> cookie_value cky activateEmail :: - (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => + (MonadCatch m, MonadIO m, HasCallStack) => BrigReq -> Email -> MonadHttp m => m () @@ -114,7 +117,7 @@ activateEmail brig email = do const (Just False) === fmap activatedFirst . responseJsonMaybe failActivatingEmail :: - (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => + (MonadCatch m, MonadIO m, HasCallStack) => BrigReq -> Email -> MonadHttp m => m () @@ -135,7 +138,7 @@ checkEmail uid expectedEmail = do const expectedEmail === (userEmail <=< responseJsonMaybe) activate :: - (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => + (MonadHttp m, HasCallStack) => BrigReq -> ActivationPair -> m ResponseLBS @@ -147,7 +150,7 @@ activate brig (k, c) = . queryItem "code" (toByteString' c) getActivationCode :: - (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => + (MonadCatch m, MonadHttp m, HasCallStack) => BrigReq -> Either Email Phone -> m (Maybe (ActivationKey, ActivationCode)) diff --git a/services/spar/test-integration/Util/Scim.hs b/services/spar/test-integration/Util/Scim.hs index 7c4e64c4ae..5c80081724 100644 --- a/services/spar/test-integration/Util/Scim.hs +++ b/services/spar/test-integration/Util/Scim.hs @@ -1,4 +1,6 @@ {-# LANGUAGE RecordWildCards #-} +-- Disabling to stop warnings on HasCallStack +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- @@ -118,7 +120,7 @@ randomScimUserWithSubject = do -- | See 'randomScimUser', 'randomScimUserWithSubject'. randomScimUserWithSubjectAndRichInfo :: - (HasCallStack, MonadRandom m, MonadIO m) => + (HasCallStack, MonadRandom m) => RichInfo -> m (Scim.User.User SparTag, SAML.UnqualifiedNameID) randomScimUserWithSubjectAndRichInfo richInfo = do diff --git a/services/spar/test/Arbitrary.hs b/services/spar/test/Arbitrary.hs index 3ca1b2ea68..937c483902 100644 --- a/services/spar/test/Arbitrary.hs +++ b/services/spar/test/Arbitrary.hs @@ -2,6 +2,7 @@ {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TypeSynonymInstances #-} {-# OPTIONS_GHC -Wno-orphans #-} +{-# OPTIONS_GHC -Wno-redundant-constraints #-} -- This file is part of the Wire Server implementation. -- diff --git a/services/spar/test/Test/Spar/Scim/UserSpec.hs b/services/spar/test/Test/Spar/Scim/UserSpec.hs index 271712d6ed..16dc0636a1 100644 --- a/services/spar/test/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test/Test/Spar/Scim/UserSpec.hs @@ -118,7 +118,7 @@ ignoringState f = fmap snd . f mockBrig :: forall (r :: EffectRow) a. - Members '[Embed IO] r => + Member (Embed IO) r => (UserId -> Maybe UserAccount) -> DeleteUserResult -> Sem (BrigAccess ': r) a -> diff --git a/services/start-services-only.sh b/services/start-services-only.sh index 9d7ea41c09..374e12f285 100755 --- a/services/start-services-only.sh +++ b/services/start-services-only.sh @@ -2,10 +2,9 @@ # Run all haskell services without immediately starting a test executable. # Can be useful for manually poking at the API. - set -eo pipefail SERVICES_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -# call integration.sh, show a message, then sleep (instead of executing a test executable) -"$SERVICES_DIR/integration.sh" bash -c 'printf "(This will hang, Control+C to close.)\nNow you can manually curl them or start an integration test executable manually with e.g. \n(first cd to a service dir for correct working directory)\n cd services/brig && ../../dist/brig-integration -s brig.integration.yaml -i ../integration.yaml\n" && sleep 1000000' +# call run-services, show a message, then sleep (instead of executing a test executable) +exec "$SERVICES_DIR/run-services" diff --git a/tools/api-simulations/api-simulations.cabal b/tools/api-simulations/api-simulations.cabal index e021f7e2ee..c21e404fb8 100644 --- a/tools/api-simulations/api-simulations.cabal +++ b/tools/api-simulations/api-simulations.cabal @@ -60,6 +60,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: api-bot @@ -127,7 +128,7 @@ executable api-loadtest ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -with-rtsopts=-N -with-rtsopts=-T + -threaded -with-rtsopts=-N -with-rtsopts=-T -Wredundant-constraints build-depends: api-bot @@ -203,7 +204,7 @@ executable api-smoketest ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -with-rtsopts=-N -with-rtsopts=-T + -threaded -with-rtsopts=-N -with-rtsopts=-T -Wredundant-constraints build-depends: api-bot diff --git a/tools/db/assets/assets.cabal b/tools/db/assets/assets.cabal index a1a84e595b..ec68240c60 100644 --- a/tools/db/assets/assets.cabal +++ b/tools/db/assets/assets.cabal @@ -18,6 +18,7 @@ library -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -funbox-strict-fields -threaded -with-rtsopts=-N + -Wredundant-constraints build-depends: , aeson @@ -101,3 +102,4 @@ executable assets -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -funbox-strict-fields -threaded -with-rtsopts=-N + -Wredundant-constraints diff --git a/tools/db/assets/src/Assets/Lib.hs b/tools/db/assets/src/Assets/Lib.hs index c4947b4d52..036b31ae6b 100644 --- a/tools/db/assets/src/Assets/Lib.hs +++ b/tools/db/assets/src/Assets/Lib.hs @@ -188,5 +188,4 @@ instance Semigroup Result where instance Monoid Result where mempty = Result 0 0 [] - mappend (Result n1 v1 i1) (Result n2 v2 i2) = - Result (n1 + n2) (v1 + v2) (i1 <> i2) + mappend = (<>) diff --git a/tools/db/auto-whitelist/auto-whitelist.cabal b/tools/db/auto-whitelist/auto-whitelist.cabal index 4cd900c307..39e886e058 100644 --- a/tools/db/auto-whitelist/auto-whitelist.cabal +++ b/tools/db/auto-whitelist/auto-whitelist.cabal @@ -61,7 +61,7 @@ executable auto-whitelist -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -funbox-strict-fields -threaded -with-rtsopts=-N -with-rtsopts=-T - -rtsopts + -rtsopts -Wredundant-constraints build-depends: base diff --git a/tools/db/billing-team-member-backfill/billing-team-member-backfill.cabal b/tools/db/billing-team-member-backfill/billing-team-member-backfill.cabal index 064a554986..ecb36e45b2 100644 --- a/tools/db/billing-team-member-backfill/billing-team-member-backfill.cabal +++ b/tools/db/billing-team-member-backfill/billing-team-member-backfill.cabal @@ -61,7 +61,7 @@ executable billing-team-member-backfill -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -funbox-strict-fields -threaded -with-rtsopts=-N -with-rtsopts=-T - -rtsopts + -rtsopts -Wredundant-constraints build-depends: base diff --git a/tools/db/find-undead/find-undead.cabal b/tools/db/find-undead/find-undead.cabal index 8c1469da84..ce86b5fb5b 100644 --- a/tools/db/find-undead/find-undead.cabal +++ b/tools/db/find-undead/find-undead.cabal @@ -61,7 +61,7 @@ executable find-undead -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -funbox-strict-fields -threaded -with-rtsopts=-N -with-rtsopts=-T - -rtsopts + -rtsopts -Wredundant-constraints build-depends: aeson diff --git a/tools/db/inconsistencies/default.nix b/tools/db/inconsistencies/default.nix index 6108d7797c..59820314dd 100644 --- a/tools/db/inconsistencies/default.nix +++ b/tools/db/inconsistencies/default.nix @@ -21,7 +21,6 @@ , imports , lens , lib -, multihash , optparse-applicative , saml2-web-sso , string-conversions @@ -57,7 +56,6 @@ mkDerivation { http-client imports lens - multihash optparse-applicative saml2-web-sso string-conversions diff --git a/tools/db/inconsistencies/inconsistencies.cabal b/tools/db/inconsistencies/inconsistencies.cabal index 54c3419c1b..152c074468 100644 --- a/tools/db/inconsistencies/inconsistencies.cabal +++ b/tools/db/inconsistencies/inconsistencies.cabal @@ -64,7 +64,7 @@ executable inconsistencies -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -funbox-strict-fields -threaded -with-rtsopts=-N -with-rtsopts=-T - -rtsopts + -rtsopts -Wredundant-constraints build-depends: aeson @@ -83,7 +83,6 @@ executable inconsistencies , http-client , imports , lens - , multihash , optparse-applicative , saml2-web-sso , string-conversions diff --git a/tools/db/migrate-sso-feature-flag/migrate-sso-feature-flag.cabal b/tools/db/migrate-sso-feature-flag/migrate-sso-feature-flag.cabal index 59f7e97431..dc1e178d0a 100644 --- a/tools/db/migrate-sso-feature-flag/migrate-sso-feature-flag.cabal +++ b/tools/db/migrate-sso-feature-flag/migrate-sso-feature-flag.cabal @@ -63,7 +63,7 @@ executable migrate-sso-feature-flag -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -funbox-strict-fields -threaded -with-rtsopts=-N -with-rtsopts=-T - -rtsopts + -rtsopts -Wredundant-constraints build-depends: base diff --git a/tools/db/move-team/move-team.cabal b/tools/db/move-team/move-team.cabal index 956ea2be44..f69508987a 100644 --- a/tools/db/move-team/move-team.cabal +++ b/tools/db/move-team/move-team.cabal @@ -64,7 +64,7 @@ library -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -funbox-strict-fields -threaded -with-rtsopts=-N -with-rtsopts=-T - -rtsopts + -rtsopts -Wredundant-constraints build-depends: aeson @@ -142,7 +142,7 @@ executable move-team -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -funbox-strict-fields -threaded -with-rtsopts=-N -with-rtsopts=-T - -rtsopts + -rtsopts -Wredundant-constraints build-depends: aeson @@ -221,7 +221,7 @@ executable move-team-generate -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -funbox-strict-fields -threaded -with-rtsopts=-N -with-rtsopts=-T - -rtsopts + -rtsopts -Wredundant-constraints build-depends: aeson diff --git a/tools/db/repair-handles/repair-handles.cabal b/tools/db/repair-handles/repair-handles.cabal index af0da94cb4..c0dc0a4e6f 100644 --- a/tools/db/repair-handles/repair-handles.cabal +++ b/tools/db/repair-handles/repair-handles.cabal @@ -61,6 +61,7 @@ executable repair-handles ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -Wredundant-constraints build-depends: base diff --git a/tools/db/service-backfill/service-backfill.cabal b/tools/db/service-backfill/service-backfill.cabal index 30aec325ca..7d76f7902e 100644 --- a/tools/db/service-backfill/service-backfill.cabal +++ b/tools/db/service-backfill/service-backfill.cabal @@ -61,7 +61,7 @@ executable service-backfill -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -funbox-strict-fields -threaded -with-rtsopts=-N -with-rtsopts=-T - -rtsopts + -rtsopts -Wredundant-constraints build-depends: base diff --git a/tools/fedcalls/fedcalls.cabal b/tools/fedcalls/fedcalls.cabal index 2e42d6f9bb..4d308d7564 100644 --- a/tools/fedcalls/fedcalls.cabal +++ b/tools/fedcalls/fedcalls.cabal @@ -58,7 +58,7 @@ executable fedcalls -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -funbox-strict-fields -threaded -with-rtsopts=-N -with-rtsopts=-T - -rtsopts + -rtsopts -Wredundant-constraints build-depends: aeson diff --git a/tools/rex/rex.cabal b/tools/rex/rex.cabal index 809c3db645..5724a9e41c 100644 --- a/tools/rex/rex.cabal +++ b/tools/rex/rex.cabal @@ -36,7 +36,8 @@ executable rex , wai , warp - ghc-options: -Wall -O1 -funbox-small-strict-fields + ghc-options: + -Wall -O1 -funbox-small-strict-fields -Wredundant-constraints if flag(static) ld-options: -static diff --git a/tools/stern/default.nix b/tools/stern/default.nix index 6eb54f2aa5..8c62c99a1a 100644 --- a/tools/stern/default.nix +++ b/tools/stern/default.nix @@ -24,6 +24,7 @@ , lib , metrics-wai , mtl +, retry , schema-profunctor , servant , servant-server @@ -31,7 +32,6 @@ , servant-swagger-ui , split , string-conversions -, swagger , swagger2 , text , tinylog @@ -75,6 +75,7 @@ mkDerivation { lens metrics-wai mtl + retry schema-profunctor servant servant-server @@ -82,7 +83,6 @@ mkDerivation { servant-swagger-ui split string-conversions - swagger swagger2 text tinylog diff --git a/tools/stern/src/Main.hs b/tools/stern/exec/Main.hs similarity index 100% rename from tools/stern/src/Main.hs rename to tools/stern/exec/Main.hs diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index a849241aeb..d743c45e18 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -44,7 +44,6 @@ import Data.String.Conversions (cs) import Data.Text (unpack) import qualified Data.Text as T import GHC.TypeLits (KnownSymbol) -import qualified Galley.Types.Teams.Intra as Team import Imports hiding (head) import Network.HTTP.Types import Network.Wai @@ -63,6 +62,7 @@ import Util.Options import Wire.API.Connection import Wire.API.Routes.Internal.Brig.Connection (ConnectionStatus) import qualified Wire.API.Routes.Internal.Brig.EJPD as EJPD +import qualified Wire.API.Routes.Internal.Galley.TeamsIntra as Team import Wire.API.Routes.Named (Named (Named)) import Wire.API.Team.Feature hiding (setStatus) import Wire.API.Team.SearchVisibility @@ -74,6 +74,7 @@ default (ByteString) start :: Opts -> IO () start o = do e <- newEnv o + runAppT e $ Intra.assertBackendApiVersion s <- Server.newSettings (server e) Server.runSettingsWithShutdown s (servantApp e) Nothing where @@ -156,6 +157,8 @@ sitemap' = :<|> Named @"put-route-mls-config" (mkFeaturePutRoute @MLSConfig) :<|> Named @"get-search-visibility" getSearchVisibility :<|> Named @"put-search-visibility" setSearchVisibility + :<|> Named @"get-route-outlook-cal-config" (mkFeatureGetRoute @OutlookCalIntegrationConfig) + :<|> Named @"put-route-outlook-cal-config" (mkFeaturePutRouteTrivialConfigNoTTL @OutlookCalIntegrationConfig) :<|> Named @"get-team-invoice" getTeamInvoice :<|> Named @"get-team-billing-info" getTeamBillingInfo :<|> Named @"put-team-billing-info" updateTeamBillingInfo @@ -207,7 +210,7 @@ searchOnBehalf :: UserId -> Maybe T.Text -> Maybe Int32 -> Handler (SearchResult searchOnBehalf uid (fromMaybe "" -> q) - (fromMaybe (unsafeRange 10) . checked @Int32 @1 @100 . fromMaybe 10 -> s) = + (fromMaybe (unsafeRange 10) . checked @1 @100 @Int32 . fromMaybe 10 -> s) = Intra.getContacts uid q (fromRange s) revokeIdentity :: Maybe Email -> Maybe Phone -> Handler NoContent @@ -296,8 +299,6 @@ mkFeatureGetRoute :: ( IsFeatureConfig cfg, ToSchema cfg, KnownSymbol (FeatureSymbol cfg), - FromJSON (WithStatusNoLock cfg), - ToJSON (WithStatusNoLock cfg), Typeable cfg ) => TeamId -> @@ -306,12 +307,8 @@ mkFeatureGetRoute = Intra.getTeamFeatureFlag @cfg mkFeaturePutRoute :: forall cfg. - ( IsFeatureConfig cfg, - ToSchema cfg, - KnownSymbol (FeatureSymbol cfg), - FromJSON (WithStatusNoLock cfg), - ToJSON (WithStatusNoLock cfg), - Typeable cfg + ( KnownSymbol (FeatureSymbol cfg), + ToJSON (WithStatusNoLock cfg) ) => TeamId -> WithStatusNoLock cfg -> @@ -402,15 +399,15 @@ getUserData uid = do convs <- Intra.getUserConversations uid clts <- Intra.getUserClients uid notfs <- Intra.getUserNotifications uid - consent <- Intra.getUserConsentValue uid - consentLog <- Intra.getUserConsentLog uid + consent <- (Intra.getUserConsentValue uid <&> toJSON @ConsentValue) `catchE` (pure . String . cs . show) + consentLog <- (Intra.getUserConsentLog uid <&> toJSON @ConsentLog) `catchE` (pure . String . cs . show) cookies <- Intra.getUserCookies uid properties <- Intra.getUserProperties uid -- Get all info from Marketo too let em = userEmail $ accountUser account marketo <- do let noEmail = MarketoResult $ KeyMap.singleton "results" emptyArray - maybe (pure noEmail) Intra.getMarketoResult em + maybe (pure $ toJSON noEmail) (\e -> (Intra.getMarketoResult e <&> toJSON) `catchE` (pure . String . cs . show)) em pure . UserMetaInfo . KeyMap.fromList $ [ "account" .= account, "cookies" .= cookies, diff --git a/tools/stern/src/Stern/API/Routes.hs b/tools/stern/src/Stern/API/Routes.hs index 5fabd0ffe0..525df34f9d 100644 --- a/tools/stern/src/Stern/API/Routes.hs +++ b/tools/stern/src/Stern/API/Routes.hs @@ -32,6 +32,7 @@ import Control.Monad.Trans.Except import qualified Data.Aeson as A import Data.Handle import Data.Id +import Data.Kind import qualified Data.Schema as Schema import qualified Data.Swagger as S import Imports hiding (head) @@ -317,6 +318,8 @@ type SternAPI = :> ReqBody '[JSON] TeamSearchVisibility :> Get '[JSON] NoContent ) + :<|> Named "get-route-outlook-cal-config" (MkFeatureGetRoute OutlookCalIntegrationConfig) + :<|> Named "put-route-outlook-cal-config" (MkFeaturePutRouteTrivialConfigNoTTL OutlookCalIntegrationConfig) :<|> Named "get-team-invoice" ( Summary "Get a specific invoice by Number" @@ -422,7 +425,7 @@ doubleMaybeToEither _ (Just a) Nothing = pure $ Left a doubleMaybeToEither _ Nothing (Just b) = pure $ Right b doubleMaybeToEither msg _ _ = throwE $ mkError status400 "either-params" ("Must use exactly one of two query params: " <> msg) -type MkFeatureGetRoute (feature :: *) = +type MkFeatureGetRoute (feature :: Type) = Summary "Shows whether a feature flag is enabled or not for a given team." :> "teams" :> Capture "tid" TeamId @@ -430,7 +433,7 @@ type MkFeatureGetRoute (feature :: *) = :> FeatureSymbol feature :> Get '[JSON] (WithStatus feature) -type MkFeaturePutRouteTrivialConfigNoTTL (feature :: *) = +type MkFeaturePutRouteTrivialConfigNoTTL (feature :: Type) = Summary "Disable / enable status for a given feature / team" :> "teams" :> Capture "tid" TeamId @@ -439,7 +442,7 @@ type MkFeaturePutRouteTrivialConfigNoTTL (feature :: *) = :> QueryParam' [Required, Strict] "status" FeatureStatus :> Put '[JSON] NoContent -type MkFeaturePutRouteTrivialConfigWithTTL (feature :: *) = +type MkFeaturePutRouteTrivialConfigWithTTL (feature :: Type) = Summary "Disable / enable status for a given feature / team" :> Description "team feature time to live, given in days, or 'unlimited' (default). only available on *some* features!" :> "teams" @@ -450,7 +453,7 @@ type MkFeaturePutRouteTrivialConfigWithTTL (feature :: *) = :> QueryParam' [Required, Strict, Description "team feature time to live, given in days, or 'unlimited' (default)."] "ttl" FeatureTTLDays :> Put '[JSON] NoContent -type MkFeaturePutRoute (feature :: *) = +type MkFeaturePutRoute (feature :: Type) = Summary "Disable / enable feature flag for a given team" :> "teams" :> Capture "tid" TeamId diff --git a/tools/stern/src/Stern/App.hs b/tools/stern/src/Stern/App.hs index 17bc07c726..8554f1288a 100644 --- a/tools/stern/src/Stern/App.hs +++ b/tools/stern/src/Stern/App.hs @@ -93,7 +93,7 @@ deriving instance MonadUnliftIO App type App = AppT IO -instance (Functor m, MonadIO m) => MonadLogger (AppT m) where +instance MonadIO m => MonadLogger (AppT m) where log l m = do g <- view applog r <- view requestId diff --git a/tools/stern/src/Stern/Intra.hs b/tools/stern/src/Stern/Intra.hs index 704892ba32..f4900e62fa 100644 --- a/tools/stern/src/Stern/Intra.hs +++ b/tools/stern/src/Stern/Intra.hs @@ -22,7 +22,8 @@ -- with this program. If not, see . module Stern.Intra - ( putUser, + ( assertBackendApiVersion, + putUser, putUserStatus, getContacts, getUserConnections, @@ -61,10 +62,12 @@ module Stern.Intra ) where -import Bilge hiding (head, options, requestId) +import Bilge hiding (head, options, path, paths, requestId) +import qualified Bilge import Bilge.RPC import Brig.Types.Intra import Control.Error +import Control.Exception (ErrorCall (ErrorCall)) import Control.Lens (view, (^.)) import Control.Monad.Reader import Data.Aeson hiding (Error) @@ -83,8 +86,6 @@ import Data.Text (strip) import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Data.Text.Lazy (pack) import GHC.TypeLits (KnownSymbol) -import Galley.Types.Teams.Intra -import qualified Galley.Types.Teams.Intra as Team import Imports import Network.HTTP.Types.Method import Network.HTTP.Types.Status hiding (statusCode) @@ -94,12 +95,17 @@ import Stern.Types import System.Logger.Class hiding (Error, name, (.=)) import qualified System.Logger.Class as Log import UnliftIO.Exception hiding (Handler) +import UnliftIO.Retry (constantDelay, limitRetries, recoverAll) import Wire.API.Connection import Wire.API.Conversation import Wire.API.Internal.Notification import Wire.API.Properties import Wire.API.Routes.Internal.Brig.Connection import qualified Wire.API.Routes.Internal.Brig.EJPD as EJPD +import Wire.API.Routes.Internal.Galley.TeamsIntra +import qualified Wire.API.Routes.Internal.Galley.TeamsIntra as Team +import Wire.API.Routes.Version +import Wire.API.Routes.Versioned import Wire.API.Team import Wire.API.Team.Feature import qualified Wire.API.Team.Feature as Public @@ -112,6 +118,28 @@ import Wire.API.User.Search ------------------------------------------------------------------------------- +backendApiVersion :: Version +backendApiVersion = V2 + +-- | Make sure the backend supports `backendApiVersion`. Crash if it doesn't. (This is called +-- in `Stern.API` so problems make `./services/run-service` crash.) +assertBackendApiVersion :: App () +assertBackendApiVersion = recoverAll (constantDelay 1000000 <> limitRetries 5) $ \_retryStatus -> do + b <- view brig + vinfo :: VersionInfo <- + responseJsonError + =<< rpc' "brig" b (method GET . Bilge.path "/api-version" . contentJson . expect2xx) + unless (maximum (vinfoSupported vinfo) == backendApiVersion) $ do + throwIO . ErrorCall $ "newest supported backend api version must be " <> show backendApiVersion + +path :: ByteString -> Request -> Request +path = Bilge.path . ((toPathComponent backendApiVersion <> "/") <>) + +paths :: [ByteString] -> Request -> Request +paths = Bilge.paths . (toPathComponent backendApiVersion :) + +------------------------------------------------------------------------------- + putUser :: UserId -> UserUpdate -> Handler () putUser uid upd = do info $ userMsg uid . msg "Changing user state" @@ -495,7 +523,6 @@ getTeamFeatureFlag :: forall cfg. ( Typeable (Public.WithStatus cfg), FromJSON (Public.WithStatus cfg), - Public.IsFeatureConfig cfg, KnownSymbol (Public.FeatureSymbol cfg) ) => TeamId -> @@ -515,7 +542,6 @@ getTeamFeatureFlag tid = do setTeamFeatureFlag :: forall cfg. ( ToJSON (Public.WithStatusNoLock cfg), - Public.IsFeatureConfig cfg, KnownSymbol (Public.FeatureSymbol cfg) ) => TeamId -> @@ -744,12 +770,12 @@ getUserConversations uid = do b ( method GET . header "Z-User" (toByteString' uid) - . path "/conversations" + . path "conversations" . queryItem "size" (toByteString' batchSize) . maybe id (queryItem "start" . toByteString') start . expect2xx ) - parseResponse (mkError status502 "bad-upstream") r + unVersioned @'V2 <$> parseResponse (mkError status502 "bad-upstream") r batchSize = 100 :: Int getUserClients :: UserId -> Handler [Client] diff --git a/tools/stern/src/Stern/Types.hs b/tools/stern/src/Stern/Types.hs index b4300b1889..0135fd1466 100644 --- a/tools/stern/src/Stern/Types.hs +++ b/tools/stern/src/Stern/Types.hs @@ -35,10 +35,10 @@ import Data.Range import qualified Data.Schema as S import qualified Data.Swagger as Swagger import Galley.Types.Teams -import Galley.Types.Teams.Intra (TeamData) import Imports import Servant.API import Wire.API.Properties +import Wire.API.Routes.Internal.Galley.TeamsIntra (TeamData) import Wire.API.Team.Member import Wire.API.Team.Permission diff --git a/tools/stern/stern.cabal b/tools/stern/stern.cabal index 7101c73a9e..3039c33d92 100644 --- a/tools/stern/stern.cabal +++ b/tools/stern/stern.cabal @@ -16,7 +16,6 @@ flag static library exposed-modules: - Main Stern.API Stern.API.Predicates Stern.API.Routes @@ -70,7 +69,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -funbox-strict-fields + -funbox-strict-fields -Wredundant-constraints build-depends: aeson >=2.0.1.0 && <2.2 @@ -92,6 +91,7 @@ library , lens >=4.4 , metrics-wai >=0.3 , mtl >=2.1 + , retry , schema-profunctor , servant , servant-server @@ -99,7 +99,6 @@ library , servant-swagger-ui , split >=0.2 , string-conversions - , swagger , swagger2 , text >=1.1 , tinylog >=0.10 @@ -120,7 +119,7 @@ library default-language: Haskell2010 executable stern - main-is: src/Main.hs + main-is: exec/Main.hs other-modules: Paths_stern default-extensions: NoImplicitPrelude @@ -165,7 +164,7 @@ executable stern ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -rtsopts -with-rtsopts=-T + -threaded -rtsopts -with-rtsopts=-T -Wredundant-constraints build-depends: base diff --git a/treefmt.toml b/treefmt.toml index 2984c8f027..6dd52aa914 100644 --- a/treefmt.toml +++ b/treefmt.toml @@ -33,7 +33,7 @@ excludes = [ "services/spar/test-scim-suite/runsuite.sh", "services/spar/test-scim-suite/run.sh", "services/brig/federation-tests.sh", - "services/integration.sh", + "services/run-services", "hack/bin/create_test_team_members.sh", "hack/bin/create_test_team_scim.sh", "hack/bin/create_test_user.sh",