diff --git a/.envrc b/.envrc index 675fb0829f7..ccf83c95b43 100644 --- a/.envrc +++ b/.envrc @@ -15,7 +15,7 @@ store_paths=$(echo "$nix_files" ./services/nginz/third_party/nginx-zauth-module/ layout_dir=$(direnv_layout_dir) env_dir=./.env -export NIX_CONFIG='extra-experimental-features = nix-command' +export NIX_CONFIG='extra-experimental-features = nix-command flakes' [[ -d "$layout_dir" ]] || mkdir -p "$layout_dir" @@ -27,7 +27,7 @@ if [[ ! -d "$env_dir" || ! -f "$layout_dir/nix-rebuild" || "$store_paths" != $(< fi fi echo "🔧 Building environment" - $bcmd build -f nix wireServer.devEnv -Lv --out-link ./.env --fallback + $bcmd build '.#wireServer.devEnv' -Lv --out-link ./.env --fallback echo "$store_paths" >"$layout_dir/nix-rebuild" fi diff --git a/CHANGELOG.md b/CHANGELOG.md index fe56aa54cce..dbfc9d778c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,131 @@ +# [2026-01-13] (Chart Release 5.25.0) + +## Release notes + + +* Operators: if you override `galley.settings.featureFlags.cells` in your Helm values, update your override to include the newly required cells config fields (channels/groups/one2one/users/collabora/publicLinks/storage/metadata); if you use the chart defaults, no action is needed. (#4903) + + +## API changes + + +* Create new API version V15 and finalize API version V14 (#4942) + +* The `PUT /teams/:tid/features/cells` endpoint has changed in API version V14 and requires additional config values. (#4903) + +* Add new fields to apps: category, description, creator (#4879) + +* Add "get app" endpoint to Brig (`GET /teams/:tid/apps/:id`) (#4879) + +* Add [pagination to SCIM groups](https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2.4) in Spar /scim/v2/Groups + + +## Features + + +* Add `meetingsPremium` feature flag to distinguish premium teams from trial teams. Meetings created by premium team members are marked as non-trial. Public endpoints: GET/PUT /teams/:tid/features/meetingsPremium. Internal endpoints: GET/PUT/PATCH /i/teams/:tid/features/meetingsPremium and lock status management. + + Add `meetings` feature flag to control access to the meetings API. When disabled, all meetings endpoints return 403 Forbidden. The feature is enabled and unlocked by default. Public endpoints: GET/PUT /teams/:tid/features/meetings. Internal endpoints: GET/PUT/PATCH /i/teams/:tid/features/meetings and lock status management. (#4915) + +* New team feature config `cellsInternal` (#4889, #4907, #4940) + +* The `cells` feature flag now contains a set of additional configuration values (#4903) + +* nginx-ingress-services chart: Add support for cert-manager Certificate + privateKey rotation policy configuration. This allows preserving private + keys across certificate renewals for client key pinning scenarios. + + Configuration options: + - `tls.privateKey.rotationPolicy` - for ingress certificates + - `federator.tls.privateKey.rotationPolicy` - for federator certificate + + Setting rotationPolicy to "Never" preserves the private key, enabling + scenarios where clients pin the server's public key rather than the + certificate itself. (#4945) + +* Allow configuring page size and parallelism for conversation migration to + PostgreSQL. This can be configured like this: + + ```yaml + background-worker: + config: + migrateConversationsOptions: + pageSize: 10000 + parallelism: 2 + ``` + (#4904) + +* Introduce new metrics for better tracking of conversation migration to postgresql: + 1. `wire_local_convs_migration_failed` + 2. `wire_user_remote_convs_migration_failed` + + If any of these become `1`, it means the migration has failed. The logs would + contain the error. In order to restart the migration, the background-worker must + be restarted. (#4891) + +* Commits with a broken group info are now let through if the group was already broken (#4883) + +* When a SAML IdP is created on a multi-ingress domain (implying that + multi-ingress domains are configured in Spar) the domain is added as `domain` + field to that IdP's `extraInfo` (`WireIdP` type in Haskell.) To avoid confusion + in later lookups, at most one IdP can be configured per multi-ingress domain. + If multi-ingress is not configured or it's not configured for the specific + domain, no `domain` field gets added to the IdP. This guards against creating + multiple IdPs and then assigning them to multi-ingress domains. Thus, users who + don't use multi-ingress don't observe any change. This feature only opens the + door to later provide an IdP for a multi-ingress domain. (#4778) + + +## Bug fixes and other updates + + +* Fixed notification endpoint returning an empty page with `hasMore=true` (#4871) + +* Fix SCIM groups endpoint to only return SCIM-managed groups, not wire-managed groups (#4906) + +* Fixed: change user idp, external_id or emails via scim (scim user update / patch failed to update parts of `ValidScimId`). (#4887) + +* Add `` to SAML/XML output. (#4898) + +* Make Swagger schema instances for `GET /search/results` and `GET /teams/{tid}/search` distinct (#4921) + +* Fix swagger docs for `GET` and `POST` on `/conversations/{cnv}/code` to show + that the response will always include the `uri` field. (#4911) + +* Reduce gc_grace_period for all conversation related tables to 1 day. This will + help restart the postgresql migration after a day, if it fails mid way. Lowering + it too much runs the risk of offline nodes resurrecting deleted data. (#4899) + +* Make underlying users for apps findable from `GET /search/contacts` (#4920) + +* Reject messages in MLS groups while in epoch 0. (#4811) + +* Optimize Postgresql queries for getting conversation members (#4896, #4896) + +* Since 5.23.23 (5866babe26f6b49511320dedb5b58a289ddcdbd4) RabbitMQ settings are + mandatory for Brig in both, federated and non-federated setups. Unfortunately, + this wasn't reflected in Brig's Helm chart. So, non-federated deployments were + failing. (#4886) + + +## Internal changes + + +* Upgrade nixpkgs and dependencies (icluding GHC from 9.8 to 9.10) (#4909) + +* Upgrade ormolu to match GHC 9.10. (#4923) + +* Fix postgres migrations on CI test runs (#4931) + +* Add `mls-users` tool to list all active users that don't support MLS. (#4888) + +* Add a golden test for `IdP` (de-) serialization to ensure the format doesn't change due to future developments. (#4927) + +* Explain MultiIngressSSO test helper functions a bit better. (#4882) + +* Use nix flakes instead of niv and manually pinned git dependencies (#4933) + + # [2025-11-26] (Chart Release 5.24.0) ## Release notes diff --git a/Makefile b/Makefile index ecf42013455..af09b6ce006 100644 --- a/Makefile +++ b/Makefile @@ -69,8 +69,13 @@ full-clean: clean .PHONY: clean clean: +ifeq ("$(package)", "all") cabal clean - -rm -rf dist +else + -if ( test -e dist || test -e dist-newstyle ); then find dist* -type d -name '$(package)-*' -exec rm -rf {}; fi +endif + # `/dist` and `.ghc.environment` shouldn't be created or used by anybody any more, we're just making sure here. + -rm -rf dist .ghc.environment -rm -f "bill-of-materials.$(HELM_SEMVER).json" .PHONY: clean-hint @@ -81,16 +86,15 @@ clean-hint: @echo -e ">>> to never have to remember submodules again, try 'git config --global submodule.recurse true'" @echo -e "\n\n\n" -.PHONY: cabal.project.local cabal.project.local: - cp ./hack/bin/cabal.project.local.template ./cabal.project.local + cp ./hack/cabal.project.local.template ./cabal.project.local # Usage: make c package=brig test=1 .PHONY: c c: treefmt c-fast .PHONY: c -c-fast: +c-fast: cabal.project.local cabal build $(WIRE_CABAL_BUILD_OPTIONS) $(package) || ( make clean-hint; false ) ifeq ($(test), 1) ./hack/bin/cabal-run-tests.sh $(package) $(testargs) @@ -298,7 +302,7 @@ treefmt-check: .PHONY: build-image-% build-image-%: - nix-build ./nix -A wireServer.imagesNoDocs.$(*) && \ + nix build '.#wireServer.imagesNoDocs.$(*)' && \ ./result | docker load | tee /tmp/imageName-$(*) && \ imageName=$$(grep quay.io /tmp/imageName-$(*) | awk '{print $$3}') && \ echo 'You can run your image locally using' && \ @@ -314,8 +318,11 @@ upload-images: upload-images-dev: ./hack/bin/upload-images.sh imagesUnoptimizedNoDocs +HOOGLE_IMAGE_DIR := $(shell mktemp -d -t wire-server-hoogle-image.XXXXXX) + upload-hoogle-image: - ./hack/bin/upload-image.sh wireServer.hoogleImage + nix -v --show-trace -L build ".#wireServer.hoogleImage" --out-link $(HOOGLE_IMAGE_DIR)/image --fallback + ./hack/bin/upload-image.sh $(HOOGLE_IMAGE_DIR)/image ################################# ## cassandra / postgres management @@ -660,7 +667,7 @@ helm-template-%: clean-charts charts-integration ./hack/bin/helm-template.sh $(*) sbom.json: - nix -Lv build -f nix wireServer.bomDependencies && \ + nix -Lv build '.#wireServer.bomDependencies' && \ nix run 'github:wireapp/tom-bombadil#create-sbom' -- --root-package-name "wire-server" # Ask the security team for the `DEPENDENCY_TRACK_API_KEY` (if you need it) diff --git a/cabal.project b/cabal.project index e4434bfad3a..f35b3b95050 100644 --- a/cabal.project +++ b/cabal.project @@ -49,6 +49,7 @@ packages: , tools/db/inconsistencies/ , tools/db/migrate-sso-feature-flag/ , tools/db/migrate-features/ + , tools/db/mls-users/ , tools/db/move-team/ , tools/db/phone-users/ , tools/db/repair-handles/ @@ -68,14 +69,32 @@ benchmarks: True program-options ghc-options: -Werror --- NOTE: --- - these packages are not provided by nix, reason being, that --- there is a bug in the nixpkgs haskell compatibility which --- makes it such that they cannot be installed by the nixpkgs code --- - these packages have bounds that are justified with their current --- dependency set, however, we have updated their dependencies, such --- that they work with newer base and ghc (api) versions -allow-newer: - , proto-lens-protoc:base - , proto-lens-protoc:ghc - , proto-lens-setup:Cabal +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +package polysemy-wire-zoo + flags: +nix-dev-env +package dns-util + flags: +nix-dev-env +package wire-subsystems + flags: +nix-dev-env +package wai-utilities + flags: +nix-dev-env +package wire-api-federation + flags: +nix-dev-env +package http2-manager + flags: +nix-dev-env +package hscim + flags: +nix-dev-env +package extended + flags: +nix-dev-env +package metrics-wai + flags: +nix-dev-env +package wire-server-enterprise + flags: +nix-dev-env +package spar + flags: +nix-dev-env +package wire-message-proto-lens + flags: +nix-dev-env +package types-common-journal + flags: +nix-dev-env diff --git a/cassandra-schema.cql b/cassandra-schema.cql index 7c4756158c0..c069ebc9f98 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -1114,7 +1114,7 @@ CREATE TABLE galley_test.team_conv ( AND crc_check_chance = 1.0 AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 + AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 @@ -1295,7 +1295,7 @@ CREATE TABLE galley_test.member ( AND crc_check_chance = 1.0 AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 + AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 @@ -1381,7 +1381,7 @@ CREATE TABLE galley_test.member_remote_user ( AND crc_check_chance = 1.0 AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 + AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 @@ -1508,7 +1508,7 @@ CREATE TABLE galley_test.user ( AND crc_check_chance = 1.0 AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 + AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 @@ -1600,7 +1600,7 @@ CREATE TABLE galley_test.mls_group_member_client ( AND crc_check_chance = 1.0 AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 + AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 @@ -1654,7 +1654,7 @@ CREATE TABLE galley_test.conversation ( AND crc_check_chance = 1.0 AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 + AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 @@ -1698,7 +1698,7 @@ CREATE TABLE galley_test.subconversation ( AND crc_check_chance = 1.0 AND dclocal_read_repair_chance = 0.1 AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 + AND gc_grace_seconds = 86400 AND max_index_interval = 2048 AND memtable_flush_period_in_ms = 0 AND min_index_interval = 128 @@ -2050,6 +2050,7 @@ CREATE TABLE spar_test.issuer_idp ( CREATE TABLE spar_test.idp ( idp uuid PRIMARY KEY, api_version int, + domain text, extra_public_keys list, handle text, issuer text, diff --git a/charts/background-worker/templates/configmap.yaml b/charts/background-worker/templates/configmap.yaml index a9a1f88e4bc..81e78f5ca77 100644 --- a/charts/background-worker/templates/configmap.yaml +++ b/charts/background-worker/templates/configmap.yaml @@ -90,6 +90,8 @@ data: {{- end }} migrateConversations: {{ .migrateConversations }} + migrateConversationsOptions: +{{toYaml .migrateConversationsOptions | indent 6 }} backendNotificationPusher: {{toYaml .backendNotificationPusher | indent 6 }} diff --git a/charts/background-worker/values.yaml b/charts/background-worker/values.yaml index 736e4983eb7..57f3ce00706 100644 --- a/charts/background-worker/values.yaml +++ b/charts/background-worker/values.yaml @@ -66,6 +66,9 @@ config: # `settings.postgresMigration.conversation` with `migration-to-postgresql` # before setting this to `true`. migrateConversations: false + migrateConversationsOptions: + pageSize: 10000 + parallelism: 2 backendNotificationPusher: pushBackoffMinWait: 10000 # in microseconds, so 10ms diff --git a/charts/brig/templates/configmap.yaml b/charts/brig/templates/configmap.yaml index 7ea485cb94f..41c0e03f9dc 100644 --- a/charts/brig/templates/configmap.yaml +++ b/charts/brig/templates/configmap.yaml @@ -96,6 +96,7 @@ data: host: wire-server-enterprise port: 8080 {{- end }} + {{- end }} {{- with .rabbitmq }} rabbitmq: @@ -108,7 +109,6 @@ data: caCert: /etc/wire/brig/rabbitmq-ca/{{ .tlsCaSecretRef.key }} {{- end }} {{- end }} - {{- end }} {{- with .aws }} aws: diff --git a/charts/brig/templates/deployment.yaml b/charts/brig/templates/deployment.yaml index dd6cc972340..02ab76b8874 100644 --- a/charts/brig/templates/deployment.yaml +++ b/charts/brig/templates/deployment.yaml @@ -146,7 +146,6 @@ spec: value: {{ join "," .noProxyList | quote }} {{- end }} {{- end }} - {{- if .Values.config.enableFederation }} - name: RABBITMQ_USERNAME valueFrom: secretKeyRef: @@ -157,7 +156,6 @@ spec: secretKeyRef: name: brig key: rabbitmqPassword - {{- end }} ports: - containerPort: {{ .Values.service.internalPort }} startupProbe: diff --git a/charts/brig/templates/secret.yaml b/charts/brig/templates/secret.yaml index 400abdb4fdd..57543a97ab8 100644 --- a/charts/brig/templates/secret.yaml +++ b/charts/brig/templates/secret.yaml @@ -32,10 +32,8 @@ data: {{- if .oauthJwkKeyPair }} oauth_ed25519.jwk: {{ .oauthJwkKeyPair | b64enc | quote }} {{- end }} - {{- if $.Values.config.enableFederation }} rabbitmqUsername: {{ .rabbitmq.username | b64enc | quote }} rabbitmqPassword: {{ .rabbitmq.password | b64enc | quote }} - {{- end }} {{- if .elasticsearch }} elasticsearch-credentials.yaml: {{ .elasticsearch | toYaml | b64enc }} {{- end }} diff --git a/charts/brig/templates/tests/brig-integration.yaml b/charts/brig/templates/tests/brig-integration.yaml index 15996698ba8..433ded2177a 100644 --- a/charts/brig/templates/tests/brig-integration.yaml +++ b/charts/brig/templates/tests/brig-integration.yaml @@ -137,12 +137,10 @@ spec: value: "dummy" - name: AWS_REGION value: "eu-west-1" - {{- if .Values.config.enableFederation }} - name: RABBITMQ_USERNAME value: "guest" - name: RABBITMQ_PASSWORD value: "guest" - {{- end }} - name: TEST_XML value: /tmp/result.xml {{- if .Values.tests.config.uploadXml }} diff --git a/charts/brig/values.yaml b/charts/brig/values.yaml index efd57f73efa..63d65a96103 100644 --- a/charts/brig/values.yaml +++ b/charts/brig/values.yaml @@ -68,7 +68,6 @@ config: multiSFT: enabled: false # keep multiSFT default in sync with sft chart's multiSFT.enabled enableFederation: false # keep in sync with background-worker, cargohold and galley charts' config.enableFederation as well as wire-server chart's tags.federation - # Not used if enableFederation is false rabbitmq: host: rabbitmq port: 5672 diff --git a/charts/galley/templates/configmap.yaml b/charts/galley/templates/configmap.yaml index 52ba50b6181..57dc603a61c 100644 --- a/charts/galley/templates/configmap.yaml +++ b/charts/galley/templates/configmap.yaml @@ -186,6 +186,10 @@ data: cells: {{- toYaml .settings.featureFlags.cells | nindent 10 }} {{- end }} + {{- if .settings.featureFlags.cellsInternal }} + cellsInternal: + {{- toYaml .settings.featureFlags.cellsInternal | nindent 10 }} + {{- end }} {{- if .settings.featureFlags.allowedGlobalOperations }} allowedGlobalOperations: {{- toYaml .settings.featureFlags.allowedGlobalOperations | nindent 10 }} @@ -214,5 +218,13 @@ data: stealthUsers: {{- toYaml .settings.featureFlags.stealthUsers | nindent 10 }} {{- end }} + {{- if .settings.featureFlags.meetings }} + meetings: + {{- toYaml .settings.featureFlags.meetings | nindent 10 }} + {{- end }} + {{- if .settings.featureFlags.meetingsPremium }} + meetingsPremium: + {{- toYaml .settings.featureFlags.meetingsPremium | nindent 10 }} + {{- end }} {{- end }} {{- end }} diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index af7d16e04e1..a8ac5e2b3b9 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -215,6 +215,53 @@ config: allowed_to_create_channels: team-members allowed_to_open_channels: team-members lockStatus: locked + cells: + defaults: + status: disabled + lockStatus: locked + config: + channels: + enabled: true + default: enabled + groups: + enabled: true + default: enabled + one2one: + enabled: true + default: enabled + users: + externals: true + guests: false + collabora: + enabled: false + publicLinks: + enableFiles: true + enableFolders: true + enforcePassword: false + enforceExpirationMax: 0 + enforceExpirationDefault: 0 + storage: + perFileQuotaBytes: "100000000" + recycle: + autoPurgeDays: 30 + disable: false + allowSkip: false + metadata: + namespaces: + usermetaTags: + defaultValues: [] + allowFreeValues: true + cellsInternal: + defaults: + status: enabled + lockStatus: unlocked + config: + backend: + url: https://cells-beta.wire.com + collabora: + edition: COOL + storage: + perUserQuotaBytes: "1000000000000" allowedGlobalOperations: status: enabled config: @@ -241,6 +288,14 @@ config: defaults: status: disabled lockStatus: locked + meetings: + defaults: + status: enabled + lockStatus: unlocked + meetingsPremium: + defaults: + status: disabled + lockStatus: locked aws: region: "eu-west-1" proxy: {} diff --git a/charts/nginx-ingress-services/templates/certificate.yaml b/charts/nginx-ingress-services/templates/certificate.yaml index 833fcec3265..8e88813fbdd 100644 --- a/charts/nginx-ingress-services/templates/certificate.yaml +++ b/charts/nginx-ingress-services/templates/certificate.yaml @@ -20,10 +20,10 @@ spec: secretName: {{ include "nginx-ingress-services.getCertificateSecretName" . | quote }} privateKey: - algorithm: ECDSA - size: 384 # 521 is not supported by Letsencrypt + algorithm: {{ .Values.tls.privateKey.algorithm }} + size: {{ .Values.tls.privateKey.size }} encoding: PKCS1 - rotationPolicy: Always + rotationPolicy: {{ .Values.tls.privateKey.rotationPolicy }} dnsNames: - {{ .Values.config.dns.https }} diff --git a/charts/nginx-ingress-services/templates/certificate_federator.yaml b/charts/nginx-ingress-services/templates/certificate_federator.yaml index 0ac26b6b2f1..1361ea386b5 100644 --- a/charts/nginx-ingress-services/templates/certificate_federator.yaml +++ b/charts/nginx-ingress-services/templates/certificate_federator.yaml @@ -29,7 +29,7 @@ spec: algorithm: ECDSA size: 256 # hs-tls only supports p256 encoding: PKCS1 - rotationPolicy: Always + rotationPolicy: {{ .Values.federator.tls.privateKey.rotationPolicy }} dnsNames: - "{{ or .Values.config.dns.certificateDomain .Values.config.dns.federator }}" {{- end -}} diff --git a/charts/nginx-ingress-services/values.yaml b/charts/nginx-ingress-services/values.yaml index d254733505f..8dc8608bb5e 100644 --- a/charts/nginx-ingress-services/values.yaml +++ b/charts/nginx-ingress-services/values.yaml @@ -15,6 +15,11 @@ fakeS3: federator: enabled: false integrationTestHelper: false + tls: + privateKey: + # rotationPolicy: Always (default) regenerates key on each renewal + # rotationPolicy: Never preserves key across renewals (for key pinning) + rotationPolicy: Always # If you want to use TLS termination on the ingress, # then set this variable to true and ensure that there # is a valid wildcard TLS certificate @@ -38,6 +43,13 @@ tls: # the validation depth between a federator client certificate and tlsClientCA verify_depth: 1 createIssuer: true + # Private key settings for cert-manager Certificate resources + privateKey: + # rotationPolicy: Always (default) regenerates key on each renewal + # rotationPolicy: Never preserves key across renewals (for key pinning) + rotationPolicy: Always + algorithm: ECDSA + size: 384 # 521 is not supported by Let's Encrypt issuer: # In a multi-domain backend (multi-ingress) setup, this name will be # augmented with the 'ingressName' (e.g. 'letsencrypt-http01-') diff --git a/charts/nginz/values.yaml b/charts/nginz/values.yaml index d902a367a5b..d7f3b9c482f 100644 --- a/charts/nginz/values.yaml +++ b/charts/nginz/values.yaml @@ -438,6 +438,9 @@ nginx_conf: - path: /teams/([^/]*)/apps$ envs: - all + - path: /teams/([^/]*)/apps/([^/]*)$ + envs: + - all - path: /teams/([^/]*)/apps/([^/]*)/cookies$ envs: - all diff --git a/docs/src/developer/developer/building.md b/docs/src/developer/developer/building.md index dd9ecc2ee84..6b754e073bb 100644 --- a/docs/src/developer/developer/building.md +++ b/docs/src/developer/developer/building.md @@ -99,24 +99,24 @@ you may build each individual service by running ```bash nix build -Lv \ - --experimental-features 'nix-command' \ - -f ./nix wireServer. + --experimental-features 'nix-command flakes' \ + '.#wireServer.' ``` you may build all the libraries that exist locally or are in the closure of `wire-server` by running ```bash nix build -Lv \ - --experimental-features 'nix-command' \ - -f ./nix wireServer.haskellPackages. + --experimental-features 'nix-command flakes' \ + '.#wireServer.haskellPackages.' ``` you may build all the images that would be deployed by running ```bash nix build -Lv \ - --experimental-features 'nix-command' \ - -f ./nix wireServer.allImages + --experimental-features 'nix-command flakes' \ + '.#wireServer.allImages' ``` > ℹ️ Info diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index 7379ad20ac5..d72b8158faa 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -234,6 +234,36 @@ The `conferenceCalling` section is optional in `featureFlags`. If it is omitted See also: conference falling for personal accounts (below). +### Meetings + +The `meetings` feature flag controls whether a user can initiate a meetings. It is enabled and unlocked by default. If you want a different configuration, use the following syntax: + +```yaml +meetings: + defaults: + status: disabled|enabled + lockStatus: locked|unlocked +``` + +The lock status for individual teams can be changed via the internal API (`PUT /i/teams/:tid/features/meetings/(un)?locked`). + +The feature status for individual teams can be changed via the public API (if the feature is unlocked). + +### Meetings Premium + +The `meetingsPremium` feature flag controls whether a team has premium meetings features. When enabled, meetings created by team members are not marked as trial. When disabled, meetings are trial and limited to 25 minutes. It is enabled and unlocked by default. If you want a different configuration, use the following syntax: + +```yaml +meetingsPremium: + defaults: + status: disabled|enabled + lockStatus: locked|unlocked +``` + +The lock status for individual teams can be changed via the internal API (`PUT /i/teams/:tid/features/meetingsPremium/(un)?locked`). + +The feature status for individual teams can be changed via the public API (if the feature is unlocked). + ### File Sharing File sharing is enabled and unlocked by default. If you want a different configuration, use the following syntax: @@ -579,6 +609,60 @@ cells: defaults: status: disabled lockStatus: locked + config: + channels: + enabled: true + default: enabled + groups: + enabled: true + default: enabled + one2one: + enabled: true + default: enabled + users: + externals: true + guests: false + collabora: + enabled: false + publicLinks: + enableFiles: true + enableFolders: true + enforcePassword: false + enforceExpirationMax: 0 + enforceExpirationDefault: 0 + storage: + perFileQuotaBytes: "100000000" + recycle: + autoPurgeDays: 30 + disable: false + allowSkip: false + metadata: + namespaces: + usermetaTags: + defaultValues: [] + allowFreeValues: true +``` + +### Cells Internal + +Cells configuration is intentionally split: `cells` is controlled by the team admin, while `cellsInternal` is set by the site operator/customer support via the internal API only. For `cellsInternal`, the `status` and `lockStatus` fields are *required* to be set to `enabled` and `unlocked` respectively, as enforced by validation logic. Failure to set these values will result in a configuration error. This block holds the backend URL, Collabora edition, and a storage quota. The quota must be provided as a positive decimal string. + +```yaml +# galley.yaml +config: + settings: + featureFlags: + cellsInternal: + defaults: + status: enabled + lockStatus: unlocked + config: + backend: + url: https://cells-beta.wire.com + collabora: + edition: COOL + storage: + perUserQuotaBytes: "1000000000000" # 1 TB ``` ### Allowed Global Operations @@ -1303,6 +1387,40 @@ clear as there are multiple `ssoUri`s defined. So, the SCIM base URI needs to be set explicitly in `scimBaseUri`. In spar's YAML config file `scimBaseUri` is always required. +#### SAML IdPs + +The multi-ingress configuration also affects SAML IdPs: The multi-ingress +domain (as specified by the internal `Z-Host` header; according to the domain +the `/identity-providers` endpoints are accessed on) is stored in the IdP's +configuration data. It can be observed as field `domain` in responses of +`/identity-providers`. + +For example: +```json +{ + "extraInfo": { + "apiVersion": "WireIdPAPIV2", + "domain": "nginz-https.ernie.example.com", + "handle": "IdP 1", + "oldIssuers": [ + "https://issuer.net/_c4590f08-14da-446b-89d0-fcb46ac8ccf9" + ], + "replacedBy": null, + "team": "ce2c2ade-8b93-4db3-b1d3-44ce1d987ca6" + }, + "id": "ba6afb01-3edf-416c-8561-42e7ecc9b00a", + "metadata": { + "certAuthnResponse": [ + "MIIBOTCBxKADAgECAg4TIFmNatMeqaAE8BWQBTANBgkqhkiG9w0BAQsFADAAMB4XDTI1MDkxODE2MjY1NVoXDTQ1MDkxMzE2MjY1NVowADB6MA0GCSqGSIb3DQEBAQUAA2kAMGYCYQC/KgI1kw9+dXc/XUQ8Q6no9GsT9gX1g3sekVEI7UuxrcHd+Tapzi1T99TdnBDedXCAxGTW6Rwhu3F20j0iAi0neWzi5xv+1KWxK0djzJ0Kxk5AcdDx/Tz+t1Uzd4VXkhECAREwDQYJKoZIhvcNAQELBQADYQAsFrbuDmGZphl9d9VdHyh8a9lIFh3oO5et+tPqFTTRPbbfEewqvtwFWvP9Gf1qgjk0qwKX3GDsFejQf4h94qU1Zf0IE8J/WyIiwEWRvZgAQ9UmqKljmbHKssyNwsl6tTY=" + ], + "issuer": "https://issuer.net/_2ab62f21-44c0-4c60-a115-d05b5129141d", + "requestURI": "https://requri.net/22169147-7d84-4991-9652-d7434986b7d8" + } +} +``` + +There can be at most one IdP per multi-ingress domain. Creating more returns an +error. Though, IdPs can be reconfigured as long as this invariant holds. ### Webapp diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000000..68686e15d11 --- /dev/null +++ b/flake.lock @@ -0,0 +1,387 @@ +{ + "nodes": { + "amazonka": { + "flake": false, + "locked": { + "lastModified": 1759730860, + "narHash": "sha256-cCRhHH/IgM7tPy8rXHTSRec1zxohO8NWxSVZEG1OjQw=", + "owner": "brendanhay", + "repo": "amazonka", + "rev": "a7d699be1076e2aad05a1930ca3937ffea954ad8", + "type": "github" + }, + "original": { + "owner": "brendanhay", + "repo": "amazonka", + "rev": "a7d699be1076e2aad05a1930ca3937ffea954ad8", + "type": "github" + } + }, + "bloodhound": { + "flake": false, + "locked": { + "lastModified": 1739958389, + "narHash": "sha256-E3co9FGZP135T3RocX4vbUELbbgGbYddD8CcVNUzHu8=", + "owner": "wireapp", + "repo": "bloodhound", + "rev": "dac0f1384b335ce35dc026bf8154e574b1a15d62", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "wire-fork", + "repo": "bloodhound", + "type": "github" + } + }, + "cql": { + "flake": false, + "locked": { + "lastModified": 1693567589, + "narHash": "sha256-2MYwZKiTdwgjJdLNvECi7gtcIo+3H4z1nYzen5x0lgU=", + "owner": "wireapp", + "repo": "cql", + "rev": "abbd2739969d17a909800f282d10d42a254c4e3b", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "develop", + "repo": "cql", + "type": "github" + } + }, + "cql-io": { + "flake": false, + "locked": { + "lastModified": 1661159563, + "narHash": "sha256-DMRWUq4yorG5QFw2ZyF/DWnRjfnzGupx0njTiOyLzPI=", + "owner": "wireapp", + "repo": "cql-io", + "rev": "c2b6aa995b5817ed7c78c53f72d5aa586ef87c36", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "control-conn", + "repo": "cql-io", + "type": "github" + } + }, + "cryptobox-haskell": { + "flake": false, + "locked": { + "lastModified": 1728557781, + "narHash": "sha256-LROqEzzvKiJ7YoF8SdKUkEgGXKBRW6Wdtd4EBY3LYOk=", + "owner": "wireapp", + "repo": "cryptobox-haskell", + "rev": "05560b2cfae13aac54414952638dadd62204f361", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "master", + "repo": "cryptobox-haskell", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "hedis": { + "flake": false, + "locked": { + "lastModified": 1748594228, + "narHash": "sha256-BwcqQZf2GaEn2i6o9bVl+jiu/CjShYlHCmO81bYfc8Y=", + "owner": "wireapp", + "repo": "hedis", + "rev": "00d7fbf5f19b812b9e64e12be8860c4741be8558", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "wire-changes", + "repo": "hedis", + "type": "github" + } + }, + "hsaml2": { + "flake": false, + "locked": { + "lastModified": 1717163391, + "narHash": "sha256-gufEAC7fFqafG8dXkGIOSfAcVv+ZWkawmBgUV+Ics2s=", + "owner": "dylex", + "repo": "hsaml2", + "rev": "874627ad22e69afe4d9a797e39633ffb30697c78", + "type": "github" + }, + "original": { + "owner": "dylex", + "ref": "main", + "repo": "hsaml2", + "type": "github" + } + }, + "hspec-wai": { + "flake": false, + "locked": { + "lastModified": 1699866697, + "narHash": "sha256-Nc5POjA+mJt7Vi3drczEivGsv9PXeVOCSwp21lLmz58=", + "owner": "wireapp", + "repo": "hspec-wai", + "rev": "08176f07fa893922e2e78dcaf996c33d79d23ce2", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "body-contains", + "repo": "hspec-wai", + "type": "github" + } + }, + "http-client": { + "flake": false, + "locked": { + "lastModified": 1706706086, + "narHash": "sha256-z47GlT+tHsSlRX4ApSGQIpOpaZiBeqr72/tWuvzw8tc=", + "owner": "wireapp", + "repo": "http-client", + "rev": "37494bb9a89dd52f97a8dc582746c6ff52943934", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "master", + "repo": "http-client", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1765772535, + "narHash": "sha256-aq+dQoaPONOSjtFIBnAXseDm9TUhIbe215TPmkfMYww=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "09b8fda8959d761445f12b55f380d90375a1d6bb", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "rev": "09b8fda8959d761445f12b55f380d90375a1d6bb", + "type": "github" + } + }, + "nixpkgs_24_11": { + "locked": { + "lastModified": 1751274312, + "narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-24.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "postie": { + "flake": false, + "locked": { + "lastModified": 1755365380, + "narHash": "sha256-gSWoV2EuqxTiVJgG5DBvpR2GmccAD/tRdGVxoNw8+Rw=", + "owner": "alexbiehl", + "repo": "postie", + "rev": "769dde424327c6b83079d79130a3d476967a9790", + "type": "github" + }, + "original": { + "owner": "alexbiehl", + "ref": "master", + "repo": "postie", + "type": "github" + } + }, + "root": { + "inputs": { + "amazonka": "amazonka", + "bloodhound": "bloodhound", + "cql": "cql", + "cql-io": "cql-io", + "cryptobox-haskell": "cryptobox-haskell", + "flake-utils": "flake-utils", + "hedis": "hedis", + "hsaml2": "hsaml2", + "hspec-wai": "hspec-wai", + "http-client": "http-client", + "nixpkgs": "nixpkgs", + "nixpkgs_24_11": "nixpkgs_24_11", + "postie": "postie", + "servant-openapi3": "servant-openapi3", + "tasty": "tasty", + "tasty-ant-xml": "tasty-ant-xml", + "text-icu-translit": "text-icu-translit", + "tinylog": "tinylog", + "tom-bombadil": "tom-bombadil", + "wai-predicates": "wai-predicates" + } + }, + "servant-openapi3": { + "flake": false, + "locked": { + "lastModified": 1716983629, + "narHash": "sha256-iKMWd+qm8hHhKepa13VWXDPCpTMXxoOwWyoCk4lLlIY=", + "owner": "wireapp", + "repo": "servant-openapi3", + "rev": "0db0095040df2c469a48f5b8724595f82afbad0c", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "required-request-bodies", + "repo": "servant-openapi3", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "tasty": { + "flake": false, + "locked": { + "lastModified": 1705586441, + "narHash": "sha256-oACehxazeKgRr993gASRbQMf74heh5g0B+70ceAg17I=", + "owner": "wireapp", + "repo": "tasty", + "rev": "97df5c1db305b626ffa0b80055361b7b28e69cec", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "mangoiv/full-stacktrace-rebased", + "repo": "tasty", + "type": "github" + } + }, + "tasty-ant-xml": { + "flake": false, + "locked": { + "lastModified": 1746711397, + "narHash": "sha256-Aj/iTVECsCGq4f+32FXWyYj/iLH5e4Gm4hYRmewnJJM=", + "owner": "wireapp", + "repo": "tasty-ant-xml", + "rev": "11c53e976e2e941f25a33e8768669eb576d19ea8", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "drop-console-formatting_rebased", + "repo": "tasty-ant-xml", + "type": "github" + } + }, + "text-icu-translit": { + "flake": false, + "locked": { + "lastModified": 1732177438, + "narHash": "sha256-wOZMz0yv29WgQyUuJ8fDejR11GopAUWkeh3nV0zlrow=", + "owner": "wireapp", + "repo": "text-icu-translit", + "rev": "2392d8d1500cd16e12aede1e0a3863ad3c1a7e37", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "master", + "repo": "text-icu-translit", + "type": "github" + } + }, + "tinylog": { + "flake": false, + "locked": { + "lastModified": 1674551828, + "narHash": "sha256-htEIJY+LmIMACVZrflU60+X42/g14NxUyFM7VJs4E6w=", + "owner": "wireapp", + "repo": "tinylog", + "rev": "9609104263e8cd2a631417c1c3ef23e090de0d09", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "wire-fork", + "repo": "tinylog", + "type": "github" + } + }, + "tom-bombadil": { + "inputs": { + "flake-utils": [ + "flake-utils" + ], + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1767870783, + "narHash": "sha256-0QStp+uH05bnGltPnOJM2FdeTJdgVIWkVM5wSFYVceM=", + "path": "/home/axeman/workspace/tom-bombadil", + "type": "path" + }, + "original": { + "path": "/home/axeman/workspace/tom-bombadil", + "type": "path" + } + }, + "wai-predicates": { + "flake": false, + "locked": { + "lastModified": 1732803463, + "narHash": "sha256-+v3nGZhW/pIki2/ax4sMLeR2F6Ikh7V1/JbGJnZC3Pc=", + "owner": "wireapp", + "repo": "wai-predicates", + "rev": "35b0ac568b5e197b21acc12699ed09ee89c1d994", + "type": "github" + }, + "original": { + "owner": "wireapp", + "ref": "develop", + "repo": "wai-predicates", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000000..698c5b23d25 --- /dev/null +++ b/flake.nix @@ -0,0 +1,119 @@ +{ + description = "A very basic flake"; + + inputs = { + self.submodules = true; + nixpkgs.url = "github:nixos/nixpkgs?rev=09b8fda8959d761445f12b55f380d90375a1d6bb"; + nixpkgs_24_11.url = "github:nixos/nixpkgs?ref=nixos-24.11"; + flake-utils.url = "github:numtide/flake-utils"; + tom-bombadil = { + url = "path:/home/axeman/workspace/tom-bombadil"; + inputs.nixpkgs.follows = "nixpkgs"; + inputs.flake-utils.follows = "flake-utils"; + }; + + cryptobox-haskell = { + url = "github:wireapp/cryptobox-haskell?ref=master"; + flake = false; + }; + bloodhound = { + url = "github:wireapp/bloodhound?ref=wire-fork"; + flake = false; + }; + hsaml2 = { + url = "github:dylex/hsaml2?ref=main"; + flake = false; + }; + hedis = { + url = "github:wireapp/hedis?ref=wire-changes"; + flake = false; + }; + + http-client = { + url = "github:wireapp/http-client?ref=master"; + flake = false; + }; + + hspec-wai = { + url = "github:wireapp/hspec-wai?ref=body-contains"; + flake = false; + }; + + cql = { + url = "github:wireapp/cql?ref=develop"; + flake = false; + }; + + cql-io = { + url = "github:wireapp/cql-io?ref=control-conn"; + flake = false; + }; + + wai-predicates = { + url = "github:wireapp/wai-predicates?ref=develop"; + flake = false; + }; + + tasty = { + url = "github:wireapp/tasty?ref=mangoiv/full-stacktrace-rebased"; + flake = false; + }; + + servant-openapi3 = { + url = "github:wireapp/servant-openapi3?ref=required-request-bodies"; + flake = false; + }; + + postie = { + url = "github:alexbiehl/postie?ref=master"; + flake = false; + }; + + tinylog = { + url = "github:wireapp/tinylog?ref=wire-fork"; + flake = false; + }; + + tasty-ant-xml = { + url = "github:wireapp/tasty-ant-xml?ref=drop-console-formatting_rebased"; + flake = false; + }; + + text-icu-translit = { + url = "github:wireapp/text-icu-translit?ref=master"; + flake = false; + }; + + amazonka = { + url = "github:brendanhay/amazonka?rev=a7d699be1076e2aad05a1930ca3937ffea954ad8"; + flake = false; + }; + }; + + outputs = inputs@{ nixpkgs, nixpkgs_24_11, flake-utils, tom-bombadil, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ + (import ./nix/overlay.nix) + (import ./nix/overlay-docs.nix) + ]; + }; + pkgs_24_11 = import nixpkgs_24_11 { + inherit system; + }; + bomDependenciesDrv = tom-bombadil.lib.${system}.bomDependenciesDrv; + wireServerPkgs = import ./nix { inherit pkgs pkgs_24_11 inputs bomDependenciesDrv; }; + in + { + # profileEnv wireServer docs docsEnv mls-test-cli nginz; + packages = { + inherit (wireServerPkgs) pkgs profileEnv wireServer docs docsEnv mls-test-cli nginz; + }; + devShells = { + default = wireServerPkgs.wireServer.devEnv; + }; + } + ); +} diff --git a/hack/bin/cabal.project.local.template b/hack/bin/cabal.project.local.template deleted file mode 100644 index 9264d3a48f4..00000000000 --- a/hack/bin/cabal.project.local.template +++ /dev/null @@ -1,6 +0,0 @@ -test-show-details: direct -profiling: False -profiling-detail: late -optimization: False -program-options - ghc-options: -O0 diff --git a/hack/bin/generate-local-nix-packages.sh b/hack/bin/generate-local-nix-packages.sh index 178f5515e65..cb12bd1ce19 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 "$ROOT_DIR" -type f -name '*.cabal' \ | grep -v dist-newstyle | sort) warningFile=$(mktemp) diff --git a/hack/bin/integration-cleanup.sh b/hack/bin/integration-cleanup.sh index 87eb8fddaac..de5d24a1471 100755 --- a/hack/bin/integration-cleanup.sh +++ b/hack/bin/integration-cleanup.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Script to delete any helm releases prefixed with `test-` older than 2 hours deemed inactive +# Script to delete any integration namespaces prefixed with `test-` older than 2 hours. # Also deletes leftover nginx ingress classes. # # Motivation: cleanup of old test clusters that were not deleted (e.g. by the CI system, because it broke) @@ -9,18 +9,27 @@ set -euo pipefail DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -releases=$(helm list -A -f '^test-' -o json | - jq -r -f "$DIR/filter-old-releases.jq") +NOW=$(date +%s) +namespaces=$(kubectl get namespaces -o json | jq -r --argjson now "$NOW" ' + .items[] + | .metadata as $meta + | $meta.name as $name + | select($name | startswith("test-")) + | select($name | contains("-fed2") | not) + | ($meta.creationTimestamp | fromdateiso8601) as $created + | select(($now - $created) > (2 * 60 * 60)) + | $name +') -if [ -n "$releases" ]; then - while read -r line; do - name=$(awk '{print $1}' <<<"$line") - namespace=$(awk '{print $2}' <<<"$line") - echo "test release '$name' older than 2 hours; deleting..." - helm delete -n "$namespace" "$name" - done <<<"$releases" -else +if [ -z "$namespaces" ]; then echo "Nothing to clean up." +else + while read -r namespace; do + echo "Test namespace '$namespace' older than 2 hours; tearing down..." + if ! NAMESPACE="$namespace" "${DIR}/integration-teardown-federation.sh"; then + echo "Failed to tear down namespace '$namespace'; continuing..." + fi + done <<<"$namespaces" fi -"${DIR}"/integration-teardown-ingress-classes.sh +"${DIR}/integration-teardown-ingress-classes.sh" diff --git a/hack/bin/integration-teardown-federation.sh b/hack/bin/integration-teardown-federation.sh index a0074f0842d..9b97eed327e 100755 --- a/hack/bin/integration-teardown-federation.sh +++ b/hack/bin/integration-teardown-federation.sh @@ -22,4 +22,4 @@ export INGRESS_CHART="ingress-nginx-controller" . "$DIR/helm_overrides.sh" helmfile --environment "$HELMFILE_ENV" --file "${TOP_LEVEL}/hack/helmfile.yaml.gotmpl" destroy --skip-deps --skip-charts --concurrency 0 || echo "Failed to delete helm deployments, ignoring this failure as next steps will the destroy namespaces anyway." -kubectl delete namespace "$NAMESPACE_1" "$NAMESPACE_2" +kubectl delete namespace "$NAMESPACE_1" "$NAMESPACE_2" --wait=false diff --git a/hack/bin/kind-upload-image.sh b/hack/bin/kind-upload-image.sh index 61b24c7937f..d376765f0c9 100755 --- a/hack/bin/kind-upload-image.sh +++ b/hack/bin/kind-upload-image.sh @@ -1,20 +1,15 @@ #!/usr/bin/env bash -# This script builds all the images in wireServer.images attribute of -# $ROOT_DIR/nix/default.nix and uploads them to the docker registry using the -# repository name specified in the image derivation and tag specified by -# environment variable "$DOCKER_TAG". -# -# If $DOCKER_USER and $DOCKER_PASSWORD are provided, the script will use them to -# upload the images. -# -# This script is intended to be run by CI/CD pipelines. +# This script builds all the images in wireServer.images attribute of the flake +# and loads them into the docker daemon of kind using the repository name +# specified in the image derivation and tag specified by environment variable +# "$DOCKER_TAG". set -euo pipefail set -x -# nix attribute under wireServer from "$ROOT_DIR/nix" containing all the images +# nix attribute under wireServer containing all the images readonly IMAGE_ATTR=${1:?$usage} SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) @@ -24,7 +19,7 @@ readonly SCRIPT_DIR ROOT_DIR tmp_link_store=$(mktemp -d) image_stream_file="$tmp_link_store/image-stream" -nix -v --show-trace -L build -f "$ROOT_DIR/nix" "$IMAGE_ATTR" -o "$image_stream_file" +nix -v --show-trace -L build "$ROOT_DIR#$IMAGE_ATTR" -o "$image_stream_file" image_file="$tmp_link_store/image" image_file_tagged="$tmp_link_store/image-tagged" "$image_stream_file" > "$image_file" diff --git a/hack/bin/kind-upload-images.sh b/hack/bin/kind-upload-images.sh index b1fea5cf980..cf97a44b839 100755 --- a/hack/bin/kind-upload-images.sh +++ b/hack/bin/kind-upload-images.sh @@ -1,20 +1,15 @@ #!/usr/bin/env bash -# This script builds all the images in wireServer.images attribute of -# $ROOT_DIR/nix/default.nix and uploads them to the docker registry using the -# repository name specified in the image derivation and tag specified by -# environment variable "$DOCKER_TAG". -# -# If $DOCKER_USER and $DOCKER_PASSWORD are provided, the script will use them to -# upload the images. -# -# This script is intended to be run by CI/CD pipelines. +# This script builds all the images in wireServer.images attribute of the flake +# and loads into the docker daemon of kind using the repository name specified +# in the image derivation and tag specified by environment variable +# "$DOCKER_TAG". set -euo pipefail set -x -# nix attribute under wireServer from "$ROOT_DIR/nix" containing all the images +# nix attribute under wireServer containing all the images readonly IMAGES_ATTR="imagesUnoptimizedNoDocs" SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) @@ -23,7 +18,7 @@ readonly SCRIPT_DIR ROOT_DIR tmp_link_store=$(mktemp -d) image_list_file="$tmp_link_store/image-list" -nix -v --show-trace -L build -f "$ROOT_DIR/nix" wireServer.imagesList -o "$image_list_file" +nix -v --show-trace -L build "$ROOT_DIR#wireServer.imagesList" -o "$image_list_file" xargs -I {} -P 10 "$SCRIPT_DIR/kind-upload-image.sh" "wireServer.$IMAGES_ATTR.{}" < "$image_list_file" diff --git a/hack/bin/nix-hls.sh b/hack/bin/nix-hls.sh index 5b66546ee50..827ad240b2f 100755 --- a/hack/bin/nix-hls.sh +++ b/hack/bin/nix-hls.sh @@ -5,7 +5,7 @@ set -euo pipefail DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" TOP_LEVEL="$(cd "$DIR/../.." && pwd)" -direnv="$(nix-build --no-out-link "$TOP_LEVEL/nix" -A pkgs.direnv)/bin/direnv" +direnv="$(nix build --no-link --print-out-paths "$TOP_LEVEL#pkgs.direnv")/bin/direnv" # shellcheck disable=SC2016 maxMemory=$("$direnv" exec "$TOP_LEVEL" bash -c 'echo "$HLS_MAX_MEMORY"') diff --git a/hack/bin/postgres_dump_schema b/hack/bin/postgres_dump_schema index be9c3220f50..b7435b41fc9 100755 --- a/hack/bin/postgres_dump_schema +++ b/hack/bin/postgres_dump_schema @@ -4,6 +4,7 @@ import sys import subprocess from subprocess import PIPE import re +import hashlib def run_psql(container, expr, dbname="postgres", user="wire-server"): p = ( @@ -50,6 +51,17 @@ def list_databases(container, user="wire-server"): dbs.append(match.group(1)) return dbs +def normalize_rls_hashes(dump_output, dbname): + """Replace random RLS policy hashes with deterministic ones based on database name.""" + # Create a deterministic hash based on the database name + det_hash = hashlib.sha256(dbname.encode()).hexdigest()[:63] + + # Replace both \restrict and \unrestrict hashes + output = re.sub(r'\\restrict [A-Za-z0-9]+', f'\\\\restrict {det_hash}', dump_output) + output = re.sub(r'\\unrestrict [A-Za-z0-9]+', f'\\\\unrestrict {det_hash}', output) + + return output + def main(): container = get_container_id() print("-- automatically generated with `make postgres-schema`") @@ -65,7 +77,8 @@ def main(): for db in databases: print(f"\n------------------------------------------------------------------------------------------") print(f"-- Database: {db}\n") - print(run_pg_dump(container, db, user="wire-server")) + dump = run_pg_dump(container, db, user="wire-server") + print(normalize_rls_hashes(dump, db)) if __name__ == "__main__": main() diff --git a/hack/bin/set-helm-chart-version.sh b/hack/bin/set-helm-chart-version.sh index b53b1857308..4a96a7ae9aa 100755 --- a/hack/bin/set-helm-chart-version.sh +++ b/hack/bin/set-helm-chart-version.sh @@ -22,7 +22,7 @@ function write_versions() { update_chart Chart.yaml # update all dependencies, if any - if [ -a requirements.yaml ]; then + if [ -e requirements.yaml ]; then sed -e "s/ version: \".*\"/ version: \"$target_version\"/g" requirements.yaml > "$tempfile" && mv "$tempfile" requirements.yaml for dep in $(helm dependency list | grep -v NAME | awk '{print $1}'); do if [ -d "$CHARTS_DIR/$dep" ] && [ "$chart" != "$dep" ]; then diff --git a/hack/bin/upload-image.sh b/hack/bin/upload-image.sh index a070b8661bb..080c18d8dcc 100755 --- a/hack/bin/upload-image.sh +++ b/hack/bin/upload-image.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash # This script builds an from the attribute provided at $1, which must be present -# in $ROOT_DIR/nix/default.nix, and uploads it to the docker registry using the -# repository name specified in the image derivation and tag specified by -# environment variable "$DOCKER_TAG". +# in the flake, and uploads it to the docker registry using the repository name +# specified in the image derivation and tag specified by environment variable +# "$DOCKER_TAG". # # If $DOCKER_USER and $DOCKER_PASSWORD are provided, the script will use them to # upload the images. @@ -14,12 +14,8 @@ set -euo pipefail readonly DOCKER_TAG=${DOCKER_TAG:?"Please set the DOCKER_TAG env variable"} -readonly usage="USAGE: $0 " -readonly IMAGE_ATTR=${1:?$usage} - -SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) -ROOT_DIR=$(cd -- "$SCRIPT_DIR/../../" &>/dev/null && pwd) -readonly SCRIPT_DIR ROOT_DIR +readonly usage="USAGE: $0 " +readonly IMAGE_STREAM_FILE=${1:?$usage} credsArgs="" if [[ "${DOCKER_USER+x}" != "" ]]; then @@ -63,10 +59,8 @@ tmp_link_store=$(mktemp -d) # product of other store paths which should already be cached and a lot of our # images should have a few common layers. More information: # https://nixos.org/manual/nixpkgs/unstable/#ssec-pkgs-dockerTools-streamLayeredImage -image_stream_file="$tmp_link_store/image_stream" -nix -v --show-trace -L build -f "$ROOT_DIR/nix" "$IMAGE_ATTR" -o "$image_stream_file" image_file="$tmp_link_store/image" -"$image_stream_file" >"$image_file" +"$IMAGE_STREAM_FILE" >"$image_file" repo=$(skopeo list-tags "docker-archive://$image_file" | jq -r '.Tags[0] | split(":") | .[0]') printf "*** Uploading $image_file to %s:%s\n" "$repo" "$DOCKER_TAG" # shellcheck disable=SC2086 diff --git a/hack/bin/upload-images.sh b/hack/bin/upload-images.sh index 89c0b721c72..9ccf1ad1874 100755 --- a/hack/bin/upload-images.sh +++ b/hack/bin/upload-images.sh @@ -1,9 +1,8 @@ #!/usr/bin/env bash -# This script builds all the images in wireServer.images attribute of -# $ROOT_DIR/nix/default.nix and uploads them to the docker registry using the -# repository name specified in the image derivation and tag specified by -# environment variable "$DOCKER_TAG". +# This script builds all the images in wireServer.images attribute of the flake +# and uploads them to the docker registry using the repository name specified in +# the image derivation and tag specified by environment variable "$DOCKER_TAG". # # If $DOCKER_USER and $DOCKER_PASSWORD are provided, the script will use them to # upload the images. @@ -14,21 +13,20 @@ set -euo pipefail readonly usage="USAGE: $0 " -# nix attribute under wireServer from "$ROOT_DIR/nix" containing all the images +# nix attribute under wireServer containing all the images readonly IMAGES_ATTR=${1:?$usage} SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) ROOT_DIR=$(cd -- "$SCRIPT_DIR/../../" &>/dev/null && pwd) readonly SCRIPT_DIR ROOT_DIR -tmp_link_store=$(mktemp -d) -image_list_file="$tmp_link_store/image-list" -nix -v --show-trace -L build -f "$ROOT_DIR/nix" wireServer.imagesList -o "$image_list_file" --fallback - # 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 --fallback +result=$(mktemp -d -t stream-images.XXXXXX) +nix -v --show-trace -L build "$ROOT_DIR#wireServer.$IMAGES_ATTR.all" --out-link "$result/images" --fallback -xargs -I {} -P 10 "$SCRIPT_DIR/upload-image.sh" "wireServer.$IMAGES_ATTR.{}" < "$image_list_file" +find "$result/images/" -type l -print0 | xargs -0 -I {} -P 10 "$SCRIPT_DIR/upload-image.sh" {} printf '*** Uploading image %s\n' nginz -"$SCRIPT_DIR/upload-image.sh" nginz +nginz_image=$(mktemp -d -t stream-nginz-image.XXXXXX) +nix -v --show-trace -L build "$ROOT_DIR#nginz" --out-link "$nginz_image/image" --fallback +"$SCRIPT_DIR/upload-image.sh" "$nginz_image/image" diff --git a/hack/cabal.project.local.template b/hack/cabal.project.local.template new file mode 100644 index 00000000000..dd27e423715 --- /dev/null +++ b/hack/cabal.project.local.template @@ -0,0 +1,12 @@ +-- disable profilling. if you are using hls/lsp, you probably want to +-- enable this. same as `ghc-options: -fwrite-ide-info`. +profiling: False +profiling-detail: late + +-- .ghc.environment is not used in .cabal-v2 and may conflict with hls. +write-ghc-environment-files: never + +-- disable optimization. very important for dev machines. implies `ghc-options: -O0`. +optimization: False + +test-show-details: direct diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index b47848bfffd..490c70deb6b 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -368,6 +368,17 @@ galley: defaults: status: enabled lockStatus: unlocked + cellsInternal: + defaults: + status: enabled + lockStatus: unlocked + config: + backend: + url: https://cells-beta.wire.com + collabora: + edition: COOL + storage: + perUserQuotaBytes: "1000000000000" allowedGlobalOperations: status: enabled config: @@ -382,6 +393,14 @@ galley: defaults: status: disabled lockStatus: locked + meetings: + defaults: + status: enabled + lockStatus: unlocked + meetingsPremium: + defaults: + status: disabled + lockStatus: locked journal: endpoint: http://fake-aws-sqs:4568 queueName: integration-team-events.fifo diff --git a/integration/integration.cabal b/integration/integration.cabal index 9f9bd2c83da..555ca6a7f1b 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -141,6 +141,7 @@ library Test.FeatureFlags.Apps Test.FeatureFlags.AssetAuditLog Test.FeatureFlags.Cells + Test.FeatureFlags.CellsInternal Test.FeatureFlags.Channels Test.FeatureFlags.ChatBubbles Test.FeatureFlags.ClassifiedDomains @@ -152,6 +153,8 @@ library Test.FeatureFlags.FileSharing Test.FeatureFlags.GuestLinks Test.FeatureFlags.LegalHold + Test.FeatureFlags.Meeting + Test.FeatureFlags.MeetingPremium Test.FeatureFlags.Mls Test.FeatureFlags.MlsE2EId Test.FeatureFlags.MlsMigration @@ -196,6 +199,7 @@ library Test.Search Test.Services Test.Spar + Test.Spar.MultiIngressIdp Test.Spar.MultiIngressSSO Test.Spar.STM Test.Swagger diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index 1276c1c0585..5c7fee6fed2 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -1204,7 +1204,9 @@ data NewApp = NewApp pict :: Maybe [Value], assets :: Maybe [Value], accentId :: Maybe Int, - meta :: Value + meta :: Value, + category :: String, + description :: String } instance Default NewApp where @@ -1214,7 +1216,9 @@ instance Default NewApp where pict = Nothing, assets = Nothing, accentId = Nothing, - meta = object [] + meta = object [], + category = "other", + description = "" } createApp :: (MakesValue creator) => creator -> String -> NewApp -> App Response @@ -1223,13 +1227,24 @@ createApp creator tid new = do submit "POST" $ req & addJSONObject - [ "name" .= new.name, - "picture" .= new.pict, - "assets" .= new.assets, - "accent_id" .= new.accentId, - "metadata" .= new.meta + [ "app" + .= object + [ "name" .= new.name, + "picture" .= new.pict, + "assets" .= new.assets, + "accent_id" .= new.accentId, + "metadata" .= new.meta, + "category" .= new.category, + "description" .= new.description + ], + "password" .= defPassword ] +getApp :: (MakesValue self) => self -> String -> String -> App Response +getApp self tid uid = do + req <- baseRequest self Brig Versioned $ joinHttpPath ["teams", tid, "apps", uid] + submit "GET" req + refreshAppCookie :: (MakesValue u) => u -> String -> String -> App Response refreshAppCookie u tid appId = do req <- baseRequest u Brig Versioned $ joinHttpPath ["teams", tid, "apps", appId, "cookies"] diff --git a/integration/test/API/BrigInternal.hs b/integration/test/API/BrigInternal.hs index d23d793f2e7..03775fc5144 100644 --- a/integration/test/API/BrigInternal.hs +++ b/integration/test/API/BrigInternal.hs @@ -66,7 +66,7 @@ createUser domain cu = do [ "name" .= "integration test team", "icon" .= "default" ] - | cu.team + | cu.team ] ) diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index c6295e4463a..391b162b776 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -595,8 +595,7 @@ updateRole caller target role qcnv = do caller Galley Versioned - ( joinHttpPath ["conversations", cnvDomain, cnvId, "members", tarDomain, tarId] - ) + (joinHttpPath ["conversations", cnvDomain, cnvId, "members", tarDomain, tarId]) submit "PUT" (req & addJSONObject ["conversation_role" .= roleReq]) updateReceiptMode :: @@ -902,7 +901,7 @@ getSelfMember user conv = do req <- baseRequest user Galley Versioned (joinHttpPath ["conversations", domain, cnv, "self"]) submit "GET" req -resetConversation :: (MakesValue user) => user -> String -> Word64 -> App Response +resetConversation :: (HasCallStack, MakesValue user) => user -> String -> Word64 -> App Response resetConversation user groupId epoch = do req <- baseRequest user Galley Versioned (joinHttpPath ["mls", "reset-conversation"]) let payload = object ["group_id" .= groupId, "epoch" .= epoch] diff --git a/integration/test/API/Spar.hs b/integration/test/API/Spar.hs index 1c1296be366..56a8338bf43 100644 --- a/integration/test/API/Spar.hs +++ b/integration/test/API/Spar.hs @@ -135,11 +135,18 @@ deleteScimUserGroup domain token groupId = do submit "DELETE" $ req & addHeader "Authorization" ("Bearer " <> token) filterScimUserGroup :: (HasCallStack, MakesValue domain) => domain -> String -> Maybe String -> App Response -filterScimUserGroup domain token mbFilter = do +filterScimUserGroup domain token mbFilter = filterScimUserGroupPaginate domain token mbFilter Nothing Nothing + +filterScimUserGroupPaginate :: (HasCallStack, MakesValue domain) => domain -> String -> Maybe String -> Maybe Int -> Maybe Int -> App Response +filterScimUserGroupPaginate domain token mbFilter mbStartIndex mbCount = do req <- baseRequest domain Spar Versioned "/scim/v2/Groups" submit "GET" $ req & scimCommonHeaders token - & maybe id (\f -> addQueryParams [("filter", f)]) mbFilter + & addQueryParams + ( maybe [] (\f -> [("filter", f)]) mbFilter + <> maybe [] (\startIndex -> [("startIndex", show startIndex)]) mbStartIndex + <> maybe [] (\count -> [("count", show count)]) mbCount + ) mkScimGroup :: String -> [Value] -> Value mkScimGroup name members = @@ -161,18 +168,30 @@ mkScimUser scimUserId = -- | https://staging-nginz-https.zinfra.io/v12/api/swagger-ui/#/default/idp-create createIdp :: (HasCallStack, MakesValue user) => user -> SAML.IdPMetadata -> App Response -createIdp user metadata = do - req <- baseRequest user Spar Versioned "/identity-providers" - submit "POST" $ req - & addQueryParams [("api_version", "v2")] - & addXML (fromLT $ SAML.encode metadata) +createIdp = (flip createIdpWithZHost) Nothing + +createIdpWithZHost :: (HasCallStack, MakesValue user) => user -> Maybe String -> SAML.IdPMetadata -> App Response +createIdpWithZHost user mbZHost metadata = do + bReq <- baseRequest user Spar Versioned "/identity-providers" + let req = + bReq + & addQueryParams [("api_version", "v2")] + & addXML (fromLT $ SAML.encode metadata) + & addHeader "Content-Type" "application/xml" + submit "POST" (req & maybe id zHost mbZHost) -- | https://staging-nginz-https.zinfra.io/v7/api/swagger-ui/#/default/idp-update updateIdp :: (HasCallStack, MakesValue user) => user -> String -> SAML.IdPMetadata -> App Response -updateIdp user idpId metadata = do - req <- baseRequest user Spar Versioned $ joinHttpPath ["identity-providers", idpId] - submit "PUT" $ req - & addXML (fromLT $ SAML.encode metadata) +updateIdp = (flip updateIdpWithZHost) Nothing + +updateIdpWithZHost :: (HasCallStack, MakesValue user) => user -> Maybe String -> String -> SAML.IdPMetadata -> App Response +updateIdpWithZHost user mbZHost idpId metadata = do + bReq <- baseRequest user Spar Versioned $ joinHttpPath ["identity-providers", idpId] + let req = + bReq + & addXML (fromLT $ SAML.encode metadata) + & addHeader "Content-Type" "application/xml" + submit "PUT" (req & maybe id zHost mbZHost) -- | https://staging-nginz-https.zinfra.io/v7/api/swagger-ui/#/default/idp-get-all getIdps :: (HasCallStack, MakesValue user) => user -> App Response @@ -180,6 +199,16 @@ getIdps user = do req <- baseRequest user Spar Versioned "/identity-providers" submit "GET" req +getIdp :: (HasCallStack, MakesValue user) => user -> String -> App Response +getIdp user idpId = do + req <- baseRequest user Spar Versioned $ joinHttpPath ["identity-providers", idpId] + submit "GET" req + +deleteIdp :: (HasCallStack, MakesValue user) => user -> String -> App Response +deleteIdp user idpId = do + req <- baseRequest user Spar Versioned $ joinHttpPath ["identity-providers", idpId] + submit "DELETE" req + -- | https://staging-nginz-https.zinfra.io/v7/api/swagger-ui/#/default/sso-team-metadata getSPMetadata :: (HasCallStack, MakesValue domain) => domain -> String -> App Response getSPMetadata = (flip getSPMetadataWithZHost) Nothing diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index 8e50d2998c0..54971aa8e13 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -181,7 +181,7 @@ initMLSClient opts cid = do let keys = object [ csSignatureScheme ciphersuite .= T.decodeUtf8 (Base64.encode pkey) - | (ciphersuite, pkey) <- suitePKeys + | (ciphersuite, pkey) <- suitePKeys ] bindResponse ( updateClient @@ -1015,8 +1015,8 @@ removeMemberFromChannel user channel userToBeRemoved = do } resetMLSConversation :: - (HasCallStack, MakesValue cid, MakesValue conv) => - cid -> + (HasCallStack, MakesValue conv) => + ClientIdentity -> conv -> App Value resetMLSConversation cid conv = do @@ -1043,4 +1043,8 @@ resetMLSConversation cid conv = do ) $ Map.delete convId mls.convs } + + mlsConv' <- getMLSConv convId' + keys <- getMLSPublicKeys cid >>= getJSON 200 + resetClientGroup mlsConv'.ciphersuite cid mlsConv'.groupId convId' keys pure conv' diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index 1652c1bac60..61f8497d9d5 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -500,10 +500,17 @@ addJSONToFailureContext name ctx action = do registerTestIdPWithMeta :: (HasCallStack, MakesValue owner) => owner -> App Response registerTestIdPWithMeta owner = fst <$> registerTestIdPWithMetaWithPrivateCreds owner -registerTestIdPWithMetaWithPrivateCreds :: (HasCallStack, MakesValue owner) => owner -> App (Response, (SAML.IdPMetadata, SAML.SignPrivCreds)) -registerTestIdPWithMetaWithPrivateCreds owner = do +registerTestIdPWithMetaWithPrivateCredsForZHost :: + (HasCallStack, MakesValue owner) => + owner -> + Maybe String -> + App (Response, (SAML.IdPMetadata, SAML.SignPrivCreds)) +registerTestIdPWithMetaWithPrivateCredsForZHost owner mbZhost = do SampleIdP idpmeta pCreds _ _ <- makeSampleIdPMetadata - (,(idpmeta, pCreds)) <$> createIdp owner idpmeta + (,(idpmeta, pCreds)) <$> createIdpWithZHost owner mbZhost idpmeta + +registerTestIdPWithMetaWithPrivateCreds :: (HasCallStack, MakesValue owner) => owner -> App (Response, (SAML.IdPMetadata, SAML.SignPrivCreds)) +registerTestIdPWithMetaWithPrivateCreds = flip registerTestIdPWithMetaWithPrivateCredsForZHost Nothing updateTestIdpWithMetaWithPrivateCreds :: (HasCallStack, MakesValue owner) => owner -> String -> App (Response, (SAML.IdPMetadata, SAML.SignPrivCreds)) updateTestIdpWithMetaWithPrivateCreds owner idpId = do diff --git a/integration/test/Test/Apps.hs b/integration/test/Test/Apps.hs index 33637f21122..4af7b903b36 100644 --- a/integration/test/Test/Apps.hs +++ b/integration/test/Test/Apps.hs @@ -20,33 +20,42 @@ module Test.Apps where import API.Brig +import qualified API.BrigInternal as BrigI import SetupHelpers import Testlib.Prelude testCreateApp :: (HasCallStack) => App () testCreateApp = do domain <- make OwnDomain - (alice, tid, [bob]) <- createTeam domain 2 - let new = def {name = "chappie"} :: NewApp - - bindResponse (createApp bob tid new) $ \resp -> do + (owner, tid, [regularMember]) <- createTeam domain 2 + let new = + def + { name = "chappie", + description = "some description of this app", + category = "ai" + } :: + NewApp + + -- Regular team member can't create apps + bindResponse (createApp regularMember tid new) $ \resp -> do resp.status `shouldMatchInt` 403 resp.json %. "label" `shouldMatch` "app-no-permission" - (appId, cookie) <- bindResponse (createApp alice tid new) $ \resp -> do + -- Owner can create an app + (appId, cookie) <- bindResponse (createApp owner tid new) $ \resp -> do resp.status `shouldMatchInt` 200 appId <- resp.json %. "user.id" & asString cookie <- resp.json %. "cookie" & asString pure (appId, cookie) - -- app user should have type "app" + -- App user should have type "app" let appIdObject = object ["domain" .= domain, "id" .= appId] - bindResponse (getUser alice appIdObject) $ \resp -> do + bindResponse (getUser owner appIdObject) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "type" `shouldMatch` "app" - -- creator should have type "regular" - bindResponse (getUser alice alice) $ \resp -> do + -- Creator should have type "regular" + bindResponse (getUser owner owner) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "type" `shouldMatch` "regular" @@ -56,6 +65,37 @@ testCreateApp = do resp.json %. "token_type" `shouldMatch` "Bearer" resp.json %. "access_token" & asString + -- Get app for the app created above succeeds + void $ getApp regularMember tid appId `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + (resp.json %. "name") `shouldMatch` "chappie" + (resp.json %. "description") `shouldMatch` "some description of this app" + (resp.json %. "category") `shouldMatch` "ai" + + -- A teamless user can't get the app + outsideUser <- randomUser OwnDomain def + bindResponse (getApp outsideUser tid appId) $ \resp -> do + resp.status `shouldMatchInt` 403 + resp.json %. "label" `shouldMatch` "app-no-permission" + + -- Another team's owner nor member can't get the app + (owner2, tid2, [regularMember2]) <- createTeam domain 2 + bindResponse (getApp owner2 tid appId) $ \resp -> resp.status `shouldMatchInt` 403 + bindResponse (getApp owner2 tid2 appId) $ \resp -> resp.status `shouldMatchInt` 404 + bindResponse (getApp regularMember2 tid appId) $ \resp -> resp.status `shouldMatchInt` 403 + + -- Category must be any of the values for the Category enum + void $ bindResponse (createApp owner tid new {category = "notinenum"}) $ \resp -> do + resp.status `shouldMatchInt` 400 + + -- App's user is findable from /search/contacts + BrigI.refreshIndex OwnDomain + searchContacts owner new.name OwnDomain `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + docs <- resp.json %. "documents" >>= asList + foundUids <- for docs objId + foundUids `shouldMatch` [appId] + testRefreshAppCookie :: (HasCallStack) => App () testRefreshAppCookie = do (alice, tid, [bob]) <- createTeam OwnDomain 2 diff --git a/integration/test/Test/Cells.hs b/integration/test/Test/Cells.hs index fad0d12c4b3..4efeacab5dc 100644 --- a/integration/test/Test/Cells.hs +++ b/integration/test/Test/Cells.hs @@ -42,7 +42,7 @@ testCellsEvent :: (HasCallStack) => App () testCellsEvent = do (alice, tid, [bob, chaz, dean, eve]) <- createTeam OwnDomain 5 conv <- postConversation alice defProteus {team = Just tid} >>= getJSON 201 - q <- watchCellsEvents (convEvents conv) + q <- watchCellsEventsForTeam tid (convEvents conv) bobId <- bob %. "qualified_id" chazId <- chaz %. "qualified_id" @@ -80,9 +80,8 @@ testCellsEvent = do testCellsCreationEvent :: (HasCallStack) => App () testCellsCreationEvent = do - -- start watcher before creating conversation - q0 <- watchCellsEvents def (alice, tid, _) <- createTeam OwnDomain 1 + q0 <- watchCellsEventsForTeam tid def conv <- postConversation alice defProteus {team = Just tid, cells = True} >>= getJSON 201 let q = q0 {filter = isNotifConv conv} :: QueueConsumer @@ -96,9 +95,8 @@ testCellsCreationEvent = do testCellsDeletionEvent :: (HasCallStack) => App () testCellsDeletionEvent = do - -- start watcher before creating conversation - q0 <- watchCellsEvents def (alice, tid, _) <- createTeam OwnDomain 1 + q0 <- watchCellsEventsForTeam tid def conv <- postConversation alice defProteus {team = Just tid, cells = True} >>= getJSON 201 void $ deleteTeamConversation tid conv alice >>= assertSuccess @@ -116,9 +114,8 @@ testCellsDeletionEvent = do testCellsCreationEventIsSentOnlyOnce :: (HasCallStack) => App () testCellsCreationEventIsSentOnlyOnce = do - -- start watcher before creating conversation - q0 <- watchCellsEvents def (alice, tid, members) <- createTeam OwnDomain 2 + q0 <- watchCellsEventsForTeam tid def conv <- postConversation alice defProteus {team = Just tid, cells = True, qualifiedUsers = members} >>= getJSON 201 let q = q0 {filter = isNotifConv conv} :: QueueConsumer @@ -141,16 +138,16 @@ testCellsFeatureCheck = do testCellsEventOnFeatureToggle :: (HasCallStack) => App () testCellsEventOnFeatureToggle = do - q0 <- watchCellsEvents def (_, tid, _) <- createTeam OwnDomain 1 + q <- watchCellsEventsForTeam tid def I.patchTeamFeatureConfig OwnDomain tid "cells" (object ["status" .= "disabled"]) >>= assertSuccess - getMessage q0 >>= \event -> do + getMessage q >>= \event -> do event %. "payload.0.type" `shouldMatch` "feature-config.update" event %. "payload.0.name" `shouldMatch` "cells" event %. "payload.0.team" `shouldMatch` (asString tid) event %. "payload.0.data.status" `shouldMatch` "disabled" I.patchTeamFeatureConfig OwnDomain tid "cells" (object ["status" .= "enabled"]) >>= assertSuccess - getMessage q0 >>= \event -> do + getMessage q >>= \event -> do event %. "payload.0.type" `shouldMatch` "feature-config.update" event %. "payload.0.name" `shouldMatch` "cells" event %. "payload.0.team" `shouldMatch` (asString tid) @@ -169,7 +166,7 @@ testCellsIgnoredEvents = do (alice, tid, _) <- createTeam OwnDomain 1 conv <- postConversation alice defProteus {team = Just tid} >>= getJSON 201 I.setCellsState alice conv "ready" >>= assertSuccess - q <- watchCellsEvents (convEvents conv) + q <- watchCellsEventsForTeam tid (convEvents conv) void $ updateMessageTimer alice conv 1000 >>= getBody 200 assertNoMessage q @@ -307,3 +304,11 @@ watchCellsEvents opts = do watcher <- ensureWatcher domain chan <- liftIO $ atomically $ dupTChan watcher.broadcast pure QueueConsumer {filter = opts.filter, chan} + +watchCellsEventsForTeam :: String -> WatchCellsEvents -> App QueueConsumer +watchCellsEventsForTeam tid opts = do + q <- watchCellsEvents opts + let isEventForTeam v = fieldEquals @Value v "payload.0.team" tid + -- the cells event queue is shared by tests + -- let's hope this filter reduces the risk of tests interfering with each other + pure $ q {filter = isEventForTeam} diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index 131260d5b7d..dfd5a9d96c9 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -104,9 +104,9 @@ testDynamicBackendsFullyConnectedWhenAllowDynamic = do -- between backends when the federation strategy is 'allowDynamic'. sequence_ [ BrigI.createFedConn x (BrigI.FedConn y "full_search" Nothing) - | x <- [domainA, domainB, domainC], - y <- [domainA, domainB, domainC], - x /= y + | x <- [domainA, domainB, domainC], + y <- [domainA, domainB, domainC], + x /= y ] uidA <- randomUser domainA def {BrigI.team = True} uidB <- randomUser domainB def {BrigI.team = True} diff --git a/integration/test/Test/DomainVerification.hs b/integration/test/Test/DomainVerification.hs index bec68fb8546..f80df2547a2 100644 --- a/integration/test/Test/DomainVerification.hs +++ b/integration/test/Test/DomainVerification.hs @@ -273,8 +273,7 @@ testUpdateTeamInvite = forM_ [ExplicitVersion 8, Versioned] \version -> do -- admin should not be able to set team-invite if the team hasn't been authorized bindResponse - ( updateTeamInvite owner domain (object ["team_invite" .= "team", "team" .= tid]) - ) + (updateTeamInvite owner domain (object ["team_invite" .= "team", "team" .= tid])) $ \resp -> do resp.status `shouldMatchInt` 403 resp.json %. "label" `shouldMatch` "operation-forbidden-for-domain-registration-state" @@ -283,8 +282,7 @@ testUpdateTeamInvite = forM_ [ExplicitVersion 8, Versioned] \version -> do -- non-admin should not be able to set team-invite bindResponse - ( updateTeamInvite mem domain (object ["team_invite" .= "team", "team" .= tid]) - ) + (updateTeamInvite mem domain (object ["team_invite" .= "team", "team" .= tid])) $ \resp -> do resp.status `shouldMatchInt` 403 resp.json %. "label" `shouldMatch` "operation-forbidden-for-domain-registration-state" diff --git a/integration/test/Test/FeatureFlags/Cells.hs b/integration/test/Test/FeatureFlags/Cells.hs index fdb14b7c274..0e622da0001 100644 --- a/integration/test/Test/FeatureFlags/Cells.hs +++ b/integration/test/Test/FeatureFlags/Cells.hs @@ -17,18 +17,98 @@ module Test.FeatureFlags.Cells where -import qualified API.GalleyInternal as Internal +import API.Galley (setTeamFeatureConfigVersioned) import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude +testCells :: (HasCallStack) => APIAccess -> App () +testCells access = + mkFeatureTests "cells" + & addUpdate (validConfig True) + & addUpdate (validConfig False) + & addInvalidUpdate invalidConfig + & runFeatureTests OwnDomain access + testPatchCells :: (HasCallStack) => App () -testPatchCells = checkPatch OwnDomain "cells" enabled +testPatchCells = checkPatch OwnDomain "cells" (validConfig True) + +validConfig :: Bool -> Value +validConfig b = + object + [ "status" .= if b then "enabled" else "disabled", + "config" + .= object + [ "channels" + .= object + [ "enabled" .= True, + "default" .= "enabled" + ], + "groups" + .= object + [ "enabled" .= True, + "default" .= "enabled" + ], + "one2one" + .= object + [ "enabled" .= True, + "default" .= "enabled" + ], + "users" + .= object + [ "externals" .= True, + "guests" .= False + ], + "collabora" + .= object + ["enabled" .= False], + "publicLinks" + .= object + [ "enableFiles" .= True, + "enableFolders" .= True, + "enforcePassword" .= False, + "enforceExpirationMax" .= (0 :: Int), + "enforceExpirationDefault" .= (0 :: Int) + ], + "storage" + .= object + [ "perFileQuotaBytes" .= "100000000", + "recycle" + .= object + [ "autoPurgeDays" .= (30 :: Int), + "disable" .= False, + "allowSkip" .= False + ] + ], + "metadata" + .= object + [ "namespaces" + .= object + [ "usermetaTags" + .= object + [ "defaultValues" .= ([] :: [String]), + "allowFreeValues" .= True + ] + ] + ] + ] + ] + +invalidConfig :: Value +invalidConfig = + object + [ "status" .= "enabled", + "config" .= object ["foox" .= "bar"] + ] -testCellsInternal :: (HasCallStack) => App () -testCellsInternal = do - (alice, tid, _) <- createTeam OwnDomain 0 - Internal.setTeamFeatureLockStatus alice tid "cells" "unlocked" - withWebSocket alice $ \ws -> do - setFlag InternalAPI ws tid "cells" enabled - setFlag InternalAPI ws tid "cells" disabled +testCellsV13 :: (HasCallStack) => App () +testCellsV13 = do + (alice, tid, _) <- createTeam OwnDomain 1 + setTeamFeatureConfigVersioned + (ExplicitVersion 13) + alice + tid + "cells" + enabled + `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 diff --git a/integration/test/Test/FeatureFlags/CellsInternal.hs b/integration/test/Test/FeatureFlags/CellsInternal.hs new file mode 100644 index 00000000000..c202004789d --- /dev/null +++ b/integration/test/Test/FeatureFlags/CellsInternal.hs @@ -0,0 +1,108 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.FeatureFlags.CellsInternal where + +import qualified API.GalleyInternal as Internal +import SetupHelpers +import Test.Cells (getMessage, watchCellsEventsForTeam) +import Test.FeatureFlags.Util +import Testlib.Prelude + +testCellsInternalEvent :: (HasCallStack) => App () +testCellsInternalEvent = do + (alice, tid, _) <- createTeam OwnDomain 0 + q <- watchCellsEventsForTeam tid def + let quota = "234723984" + update = mkFt "enabled" "unlocked" defConf {quota} + setFeature InternalAPI alice tid "cellsInternal" update >>= assertSuccess + event <- getMessage q %. "payload.0" + event %. "name" `shouldMatch` "cellsInternal" + event %. "team" `shouldMatch` tid + event %. "type" `shouldMatch` "feature-config.update" + event %. "data.lockStatus" `shouldMatch` "unlocked" + event %. "data.status" `shouldMatch` "enabled" + event %. "data.config.backend.url" `shouldMatch` "https://cells-beta.wire.com" + event %. "data.config.collabora.edition" `shouldMatch` "COOL" + event %. "data.config.storage.perUserQuotaBytes" `shouldMatch` quota + +testCellsInternal :: (HasCallStack) => App () +testCellsInternal = do + (alice, tid, _) <- createTeam OwnDomain 0 + + withWebSocket alice $ \ws -> do + for_ validCellsInternalUpdates $ setFlag InternalAPI ws tid "cellsInternal" + for_ invalidCellsInternalUpdates $ setFeature InternalAPI alice tid "cellsInternal" >=> getJSON 400 + + -- the feature does not have a public PUT endpoint + setFeature PublicAPI alice tid "cellsInternal" enabled `bindResponse` \resp -> do + resp.status `shouldMatchInt` 404 + resp.json %. "label" `shouldMatch` "no-endpoint" + +validCellsInternalUpdates :: [Value] +validCellsInternalUpdates = + [ mkFt "enabled" "unlocked" defConf, + mkFt "enabled" "unlocked" defConf {collabora = "NO"}, + mkFt "enabled" "unlocked" defConf {collabora = "COOL"}, + mkFt "enabled" "unlocked" defConf {url = "https://wire.com"}, + mkFt "enabled" "unlocked" defConf {quota = "92346832946243"} + ] + +invalidCellsInternalUpdates :: [Value] +invalidCellsInternalUpdates = + [ mkFt "enabled" "unlocked" defConf {collabora = "FOO"}, + mkFt "enabled" "unlocked" defConf {url = "http://wire.com"}, + mkFt "enabled" "unlocked" defConf {quota = "-92346832946243"}, + mkFt "enabled" "unlocked" defConf {quota = "1 TB"}, + mkFt "disabled" "unlocked" defConf + ] + +mkFt :: String -> String -> CellsInternalConfig -> Value +mkFt s ls c = + object + [ "lockStatus" .= ls, + "status" .= s, + "ttl" .= "unlimited", + "config" + .= object + [ "backend" .= object ["url" .= c.url], + "collabora" .= object ["edition" .= c.collabora], + "storage" .= object ["perUserQuotaBytes" .= c.quota] + ] + ] + +defConf :: CellsInternalConfig +defConf = + CellsInternalConfig + { url = "https://cells-beta.wire.com", + collabora = "COOL", + quota = "1000000000000" + } + +testPatchCellsInternal :: (HasCallStack) => App () +testPatchCellsInternal = do + for_ validCellsInternalUpdates $ checkPatch OwnDomain "cellsInternal" + (_, tid, _) <- createTeam OwnDomain 0 + for_ (mkFt "enabled" "locked" defConf : invalidCellsInternalUpdates) + $ Internal.patchTeamFeature OwnDomain tid "cellsInternal" + >=> assertStatus 400 + +data CellsInternalConfig = CellsInternalConfig + { url :: String, + collabora :: String, + quota :: String + } diff --git a/integration/test/Test/FeatureFlags/Meeting.hs b/integration/test/Test/FeatureFlags/Meeting.hs new file mode 100644 index 00000000000..4ed9018952a --- /dev/null +++ b/integration/test/Test/FeatureFlags/Meeting.hs @@ -0,0 +1,31 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.FeatureFlags.Meeting where + +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPatchMeeting :: (HasCallStack) => App () +testPatchMeeting = checkPatch OwnDomain "meetings" enabled + +testMeeting :: (HasCallStack) => APIAccess -> App () +testMeeting access = + mkFeatureTests "meetings" + & addUpdate disabled + & addUpdate enabled + & runFeatureTests OwnDomain access diff --git a/integration/test/Test/FeatureFlags/MeetingPremium.hs b/integration/test/Test/FeatureFlags/MeetingPremium.hs new file mode 100644 index 00000000000..ecbd2098684 --- /dev/null +++ b/integration/test/Test/FeatureFlags/MeetingPremium.hs @@ -0,0 +1,30 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.FeatureFlags.MeetingPremium where + +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPatchMeetingPremium :: (HasCallStack) => App () +testPatchMeetingPremium = checkPatch OwnDomain "meetingsPremium" disabledLocked + +testMeetingPremium :: (HasCallStack) => APIAccess -> App () +testMeetingPremium access = + mkFeatureTests "meetingsPremium" + & addUpdate enabled + & runFeatureTests OwnDomain access diff --git a/integration/test/Test/FeatureFlags/Util.hs b/integration/test/Test/FeatureFlags/Util.hs index d1d646d8099..ce2acde064a 100644 --- a/integration/test/Test/FeatureFlags/Util.hs +++ b/integration/test/Test/FeatureFlags/Util.hs @@ -69,6 +69,63 @@ defEnabledObj conf = "config" .= conf ] +defCellsConfig :: Value +defCellsConfig = + object + [ "channels" + .= object + [ "enabled" .= True, + "default" .= "enabled" + ], + "groups" + .= object + [ "enabled" .= True, + "default" .= "enabled" + ], + "one2one" + .= object + [ "enabled" .= True, + "default" .= "enabled" + ], + "users" + .= object + [ "externals" .= True, + "guests" .= False + ], + "collabora" + .= object + ["enabled" .= False], + "publicLinks" + .= object + [ "enableFiles" .= True, + "enableFolders" .= True, + "enforcePassword" .= False, + "enforceExpirationMax" .= (0 :: Int), + "enforceExpirationDefault" .= (0 :: Int) + ], + "storage" + .= object + [ "perFileQuotaBytes" .= "100000000", + "recycle" + .= object + [ "autoPurgeDays" .= (30 :: Int), + "disable" .= False, + "allowSkip" .= False + ] + ], + "metadata" + .= object + [ "namespaces" + .= object + [ "usermetaTags" + .= object + [ "defaultValues" .= ([] :: [String]), + "allowFreeValues" .= True + ] + ] + ] + ] + defAllFeatures :: Value defAllFeatures = object @@ -149,7 +206,13 @@ defAllFeatures = "allowed_to_open_channels" .= "team-members" ] ], - "cells" .= enabled, + "cells" + .= object + [ "lockStatus" .= "unlocked", + "status" .= "enabled", + "ttl" .= "unlimited", + "config" .= defCellsConfig + ], "assetAuditLog" .= disabledLocked, "allowedGlobalOperations" .= object @@ -165,7 +228,21 @@ defAllFeatures = "chatBubbles" .= disabledLocked, "apps" .= disabledLocked, "simplifiedUserConnectionRequestQRCode" .= enabled, - "stealthUsers" .= disabledLocked + "stealthUsers" .= disabledLocked, + "cellsInternal" + .= object + [ "lockStatus" .= "unlocked", + "status" .= "enabled", + "ttl" .= "unlimited", + "config" + .= object + [ "backend" .= object ["url" .= "https://cells-beta.wire.com"], + "collabora" .= object ["edition" .= "COOL"], + "storage" .= object ["perUserQuotaBytes" .= "1000000000000"] + ] + ], + "meetings" .= enabled, + "meetingsPremium" .= disabledLocked ] hasExplicitLockStatus :: String -> Bool diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index 26aead3de04..98fe4dbfe5f 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -21,6 +21,7 @@ module Test.MLS where import API.Brig (claimKeyPackages, deleteClient) import API.Galley +import qualified API.GalleyInternal as I import Data.Bits import qualified Data.ByteString as B import qualified Data.ByteString.Base64 as Base64 @@ -92,6 +93,28 @@ testPastStaleApplicationMessage otherDomain = do -- bob's application messages are now rejected void $ postMLSMessage bob1 msg2.message >>= getJSON 409 +testEpochZeroApplicationMessage :: (HasCallStack) => App () +testEpochZeroApplicationMessage = do + [alice] <- createAndConnectUsers [make OwnDomain] + alice1 <- createMLSClient def alice + conv <- createNewGroup def alice1 + void $ createAddCommit alice1 conv [] >>= sendAndConsumeCommitBundle + mlsConv <- getMLSConv conv + + -- send message, make sure that's succeeding + msg <- createApplicationMessage mlsConv.convId alice1 "group is initialised" + postMLSMessage alice1 msg.message >>= assertStatus 201 + + -- reset conversation, so it exists on server and client with epoch 0 + convId' <- objConvId =<< resetMLSConversation alice1 conv + + -- send message, make sure that's failing + msg' <- createApplicationMessage convId' alice1 "group not initialised" + postMLSMessage alice1 msg'.message >>= flip withResponse \resp -> do + j <- getJSON 400 resp + j %. "label" `shouldMatch` "mls-protocol-error" + j %. "message" `shouldMatch` "Application messages at epoch 0 are not supported" + testFutureStaleApplicationMessage :: (HasCallStack) => App () testFutureStaleApplicationMessage = do [alice, bob, charlie] <- createAndConnectUsers [OwnDomain, OwnDomain, OwnDomain] @@ -1115,6 +1138,46 @@ testGroupInfoCheckDisabled = do $ \resp -> do resp.status `shouldMatchInt` 201 +testGroupInfoAlreadyBroken :: (HasCallStack) => App () +testGroupInfoAlreadyBroken = do + withModifiedBackend + ( def + { galleyCfg = + setField "settings.checkGroupInfo" True + } + ) + $ \domain -> do + (alice, tid, [bob, charlie, dee]) <- createTeam domain 4 + [alice1, bob1, charlie1, dee1] <- traverse (createMLSClient def) [alice, bob, charlie, dee] + traverse_ (uploadNewKeyPackage def) [bob1, charlie1, dee1] + + conv <- postConversation alice1 defMLS {team = Just tid} >>= getJSON 201 + convId <- objConvId conv + createGroup def alice1 convId + + -- add bob normally + mp1 <- createAddCommit alice1 convId [bob] + void $ sendAndConsumeCommitBundle mp1 + + -- make a commit with an old group info + mp2 <- createAddCommit alice1 convId [charlie] + void $ sendAndConsumeCommitBundle mp2 {groupInfo = mp1.groupInfo} + + -- enable feature + do + I.setTeamFeatureLockStatus alice tid "mls" "unlocked" + mls <- + defAllFeatures + %. "mls.config" + >>= setField "groupInfoDiagnostics" True + let feat = object ["status" .= "enabled", "config" .= mls] + void $ setTeamFeatureConfig alice tid "mls" feat >>= getJSON 200 + + -- make another commit with an old group info + -- the group was already broken previously, so this should be accepted + mp3 <- createAddCommit alice1 convId [dee] + void $ sendAndConsumeCommitBundle mp3 {groupInfo = mp1.groupInfo} + testAddUsersDirectlyShouldFail :: (HasCallStack) => App () testAddUsersDirectlyShouldFail = do [alice, bob] <- replicateM 2 $ randomUser OwnDomain def diff --git a/integration/test/Test/MLS/Notifications.hs b/integration/test/Test/MLS/Notifications.hs index d4c1eb4d0d8..5b5f87d1dc6 100644 --- a/integration/test/Test/MLS/Notifications.hs +++ b/integration/test/Test/MLS/Notifications.hs @@ -17,7 +17,11 @@ module Test.MLS.Notifications where +import API.Common (recipient) import API.Gundeck +import API.GundeckInternal (postPush) +import Control.Concurrent (threadDelay) +import Data.Timeout import MLS.Util import Notifications import SetupHelpers @@ -45,3 +49,78 @@ testWelcomeNotification = do size = Just 10000 } >>= getJSON 200 + +testNotificationPagination :: (HasCallStack) => App () +testNotificationPagination = do + let overrides = + def + { gundeckCfg = + setField "settings.maxPayloadLoadSize" (Just ((2 :: Int) * 1024)) + >=> setField "settings.notificationTTL" (2 #> Second) + } + withModifiedBackend overrides $ \dom -> do + user <- randomUser dom def + + liftIO $ threadDelay 2_100_000 -- let notifications expire + + -- Create a single oversized notification so Cassandra paging stops after the first row. + r <- recipient user + let bigPayload = replicate (3 * 1024) 'x' -- 3 KiB > maxPayloadLoadSize + push = + object + [ "recipients" .= [r], + "payload" .= [object ["blob" .= bigPayload]] + ] + + postPush user [push] >>= assertSuccess + + notifId <- + getNotifications user def `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + notif <- resp.json %. "notifications" >>= asList >>= assertOne + notif %. "id" >>= asString + + -- Re-request starting after that notification + getNotifications user def {since = Just notifId} + `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "notifications" >>= asList >>= shouldBeEmpty + resp.json %. "has_more" `shouldMatch` False + +testNotificationPaginationOversizeSince :: (HasCallStack) => App () +testNotificationPaginationOversizeSince = do + let overrides = + def + { gundeckCfg = + setField "settings.maxPayloadLoadSize" (Just ((2 :: Int) * 1024)) + >=> setField "settings.notificationTTL" (2 #> Second) + } + withModifiedBackend overrides $ \dom -> do + user <- randomUser dom def + liftIO $ threadDelay 2_100_000 -- let notifications expire + r <- recipient user + let bigPayload = replicate (3 * 1024) 'x' + smallPayload = "ok" + mkPush payload = + object + [ "recipients" .= [r], + "payload" .= [object ["blob" .= payload]] + ] + + postPush user [mkPush bigPayload] >>= assertSuccess + + bigNotifId <- + getNotifications user def `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + notif <- resp.json %. "notifications" >>= asList >>= assertOne + notif %. "id" >>= asString + + -- Send a second, small notification that should show up after the anchor. + postPush user [mkPush smallPayload] >>= assertSuccess + + getNotifications user def {since = Just bigNotifId} + `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "has_more" `shouldMatch` False + n <- resp.json %. "notifications" >>= asList >>= assertOne + n %. "payload.0.blob" `shouldMatch` "ok" diff --git a/integration/test/Test/MLS/Reset.hs b/integration/test/Test/MLS/Reset.hs index fac4b0a32bc..f899a202aca 100644 --- a/integration/test/Test/MLS/Reset.hs +++ b/integration/test/Test/MLS/Reset.hs @@ -62,7 +62,7 @@ testResetSelfConversation = do void $ createAddCommit alice1 convId [alice] >>= sendAndConsumeCommitBundle mlsConv <- getMLSConv convId - conv' <- resetMLSConversation alice conv + conv' <- resetMLSConversation alice1 conv conv' %. "group_id" `shouldNotMatch` (mlsConv.groupId :: String) conv' %. "epoch" `shouldMatchInt` 0 convId' <- objConvId conv' @@ -79,11 +79,14 @@ testResetOne2OneConversation :: (HasCallStack) => App () testResetOne2OneConversation = do [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - void . replicateM 2 $ uploadNewKeyPackage def bob1 + void . for [alice1, bob1] $ \cid -> replicateM 2 $ uploadNewKeyPackage def cid otherDomain <- asString OtherDomain conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 convOwnerDomain <- asString $ conv %. "conversation.qualified_id.domain" - let user = if convOwnerDomain == otherDomain then bob else alice + let (user, other) = + if convOwnerDomain == otherDomain + then (bob1, alice) + else (alice1, bob) convId <- objConvId (conv %. "conversation") resetOne2OneGroup def alice1 conv @@ -95,9 +98,9 @@ testResetOne2OneConversation = do conv' %. "group_id" `shouldNotMatch` (mlsConv.groupId :: String) conv' %. "epoch" `shouldMatchInt` 0 convId' <- objConvId conv' - resetOne2OneGroupGeneric def alice1 conv' (conv %. "public_keys") + resetOne2OneGroupGeneric def user conv' (conv %. "public_keys") - void $ createAddCommit alice1 convId' [bob] >>= sendAndConsumeCommitBundle + void $ createAddCommit user convId' [other] >>= sendAndConsumeCommitBundle conv'' <- getConversation user convId >>= getJSON 200 conv'' %. "epoch" `shouldMatchInt` 1 diff --git a/integration/test/Test/Spar.hs b/integration/test/Test/Spar.hs index 5fc7664868c..3ea7e0271c3 100644 --- a/integration/test/Test/Spar.hs +++ b/integration/test/Test/Spar.hs @@ -449,6 +449,72 @@ testSparScimCreateGetSearchUserGroup = do (singleEmptyGroup %. "members" & asList) `shouldMatch` ([] :: [Value]) respGroup4.json `shouldMatch` singleEmptyGroup + -- 4. Pagination + let searchPage substr startIndex count = + filterScimUserGroupPaginate + OwnDomain + tok + (Just $ "displayName co \"" <> substr <> "\"") + (Just startIndex) + (Just count) + createGroup name = createScimUserGroup OwnDomain tok $ mkScimGroup name [mkScimUser scimUserId] + + -- Create 20 groups + let expectedTotalResults = 20 :: Int + forM_ [1 .. expectedTotalResults] $ \n -> createGroup ("newGroupNo" <> show n) + + -- Go through 4 pages (the last one is an empty page) + forM_ [1 .. 4] $ \p -> + let startIndex = (p - 1) * count + 1 -- 1-based index + count = 7 + expectedItemsPerPage = max 0 (min count (expectedTotalResults - startIndex + 1)) -- expected between 0 and `count` depending on if it's a full, half or empty page + in searchPage "newGroupNo" startIndex count `bindResponse` \resp -> do + resp.json %. "startIndex" `shouldMatchInt` startIndex + resp.json %. "totalResults" `shouldMatchInt` expectedTotalResults + resp.json %. "itemsPerPage" `shouldMatchInt` expectedItemsPerPage + resources <- resp.json %. "Resources" & asList + length resources `shouldMatchInt` expectedItemsPerPage + + -- startIndex=0 edge case: the 0 is treated as 1 according to SCIM spec + filterScimUserGroupPaginate OwnDomain tok (Just "displayName co \"newGroupNo\"") (Just 0) (Just 5) `bindResponse` \resp -> do + resp.json %. "startIndex" `shouldMatchInt` 1 + resources <- resp.json %. "Resources" & asList + length resources `shouldMatchInt` 5 + + -- startIndex=-2 edge case: -2 is treated as 1 according to SCIM spec + filterScimUserGroupPaginate OwnDomain tok (Just "displayName co \"newGroupNo\"") (Just (-2)) (Just 9) `bindResponse` \resp -> do + resp.json %. "startIndex" `shouldMatchInt` 1 + resources <- resp.json %. "Resources" & asList + length resources `shouldMatchInt` 9 + + -- Only startIndex, no count + filterScimUserGroupPaginate OwnDomain tok (Just "displayName co \"newGroupNo\"") (Just 5) Nothing `bindResponse` \resp -> do + resp.json %. "startIndex" `shouldMatchInt` 5 + resp.json %. "totalResults" `shouldMatchInt` expectedTotalResults + + -- Only count, no startIndex + filterScimUserGroupPaginate OwnDomain tok (Just "displayName co \"newGroupNo\"") Nothing (Just 3) `bindResponse` \resp -> do + resp.json %. "startIndex" `shouldMatchInt` 1 + resp.json %. "itemsPerPage" `shouldMatchInt` 3 + resources <- resp.json %. "Resources" & asList + length resources `shouldMatchInt` 3 + + -- Filter with empty result + filterScimUserGroupPaginate OwnDomain tok (Just "displayName co \"nonexistent-filter-xyz\"") (Just 1) (Just 10) `bindResponse` \resp -> do + resp.json %. "startIndex" `shouldMatchInt` 1 + resp.json %. "totalResults" `shouldMatchInt` 0 + resp.json %. "itemsPerPage" `shouldMatchInt` 0 + resources <- resp.json %. "Resources" & asList + length resources `shouldMatchInt` 0 + + -- All results in one page + filterScimUserGroupPaginate OwnDomain tok (Just "displayName co \"newGroupNo\"") (Just 1) (Just 100) `bindResponse` \resp -> do + resp.json %. "startIndex" `shouldMatchInt` 1 + resp.json %. "totalResults" `shouldMatchInt` expectedTotalResults + resp.json %. "itemsPerPage" `shouldMatchInt` expectedTotalResults + resources <- resp.json %. "Resources" & asList + length resources `shouldMatchInt` expectedTotalResults + testSparScimUpdateUserGroup :: (HasCallStack) => App () testSparScimUpdateUserGroup = do (alice, tid, []) <- createTeam OwnDomain 1 @@ -639,6 +705,55 @@ testSparScimDeleteUserGroup = do getScimUserGroup OwnDomain tok gid `bindResponse` \resp -> do resp.status `shouldMatchInt` 404 +testSparScimGroupSearchOnlyReturnsScimGroups :: (HasCallStack) => App () +testSparScimGroupSearchOnlyReturnsScimGroups = do + (owner, tid, [regularMember]) <- createTeam OwnDomain 2 + tok <- createScimTokenV6 owner def >>= \resp -> resp.json %. "token" >>= asString + + assertSuccess =<< setTeamFeatureStatus owner tid "validateSAMLemails" "disabled" + assertSuccess =<< setTeamFeatureStatus owner tid "sso" "enabled" + void $ registerTestIdPWithMetaWithPrivateCreds owner + + let mkScimMemberCandidate :: App String + mkScimMemberCandidate = do + scimUserEmail <- randomEmail + scimUser <- randomScimUserWith def {mkExternalId = pure scimUserEmail} + uid <- createScimUser owner tok scimUser >>= getJSON 201 >>= (%. "id") >>= asString + registerInvitedUser OwnDomain tid scimUserEmail + pure uid + + scimUserId <- mkScimMemberCandidate + + -- Create a wire-managed group using the regular team member + regularMemberId <- regularMember %. "id" >>= asString + let wireGroupPayload = + object + [ "name" .= "wire-managed-group", + "members" .= [regularMemberId] + ] + wireGroupResp <- createUserGroup owner wireGroupPayload + wireGroupResp.status `shouldMatchInt` 200 + wireGroupId <- wireGroupResp.json %. "id" >>= asString + + -- Verify the wire-managed group was created with managedBy = "wire" + wireGroupGet <- getUserGroup owner wireGroupId + wireGroupGet.status `shouldMatchInt` 200 + wireGroupGet.json %. "managedBy" `shouldMatch` "wire" + + -- Create a SCIM-managed group using the SCIM user + scimGroupResp <- createScimUserGroup OwnDomain tok $ mkScimGroup "scim-managed-group" [mkScimUser scimUserId] + scimGroupResp.status `shouldMatchInt` 201 + scimGroupId <- scimGroupResp.json %. "id" >>= asString + + -- Call the SCIM groups search endpoint (without filter) + filterScimUserGroup OwnDomain tok Nothing `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resources <- resp.json %. "Resources" >>= asList + resourceIds <- for resources $ \g -> g %. "id" >>= asString + + -- Assert: Only the SCIM-managed group should be returned, not the wire-managed group + resourceIds `shouldMatch` [scimGroupId] + ---------------------------------------------------------------------- -- saml stuff diff --git a/integration/test/Test/Spar/MultiIngressIdp.hs b/integration/test/Test/Spar/MultiIngressIdp.hs new file mode 100644 index 00000000000..8294782cb4a --- /dev/null +++ b/integration/test/Test/Spar/MultiIngressIdp.hs @@ -0,0 +1,302 @@ +module Test.Spar.MultiIngressIdp where + +import API.GalleyInternal +import API.Spar +import Control.Lens ((.~), (^.)) +import qualified SAML2.WebSSO.Test.Util as SAML +import qualified SAML2.WebSSO.Types as SAML +import SetupHelpers +import Testlib.Prelude + +ernieZHost :: String +ernieZHost = "nginz-https.ernie.example.com" + +bertZHost :: String +bertZHost = "nginz-https.bert.example.com" + +kermitZHost :: String +kermitZHost = "nginz-https.kermit.example.com" + +-- | Create a `MultiIngressDomainConfig` JSON object with the given @zhost@ +makeSpDomainConfig :: String -> Value +makeSpDomainConfig zhost = + object + [ "spAppUri" .= ("https://webapp." ++ zhost), + "spSsoUri" .= ("https://nginz-https." ++ zhost ++ "/sso"), + "contacts" .= [object ["type" .= ("ContactTechnical" :: String)]] + ] + +testMultiIngressIdpSimpleCase :: (HasCallStack) => App () +testMultiIngressIdpSimpleCase = do + withModifiedBackend + def + { sparCfg = + removeField "saml.spSsoUri" + >=> removeField "saml.spAppUri" + >=> removeField "saml.contacts" + >=> setField + "saml.spDomainConfigs" + ( object + [ ernieZHost .= makeSpDomainConfig ernieZHost, + bertZHost .= makeSpDomainConfig bertZHost + ] + ) + } + $ \domain -> do + (owner, tid, _) <- createTeam domain 1 + void $ setTeamFeatureStatus owner tid "sso" "enabled" + + -- Create IdP for one domain + SAML.SampleIdP idpmeta _ _ _ <- SAML.makeSampleIdPMetadata + idpId <- + createIdpWithZHost owner (Just ernieZHost) idpmeta `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` ernieZHost + resp.jsonBody %. "id" >>= asString + + getIdp owner idpId `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` ernieZHost + + -- Update IdP for another domain + updateIdpWithZHost owner (Just bertZHost) idpId idpmeta `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` bertZHost + + getIdp owner idpId `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` bertZHost + +-- We must guard against domains being filled up with multiple IdPs and then +-- being configured as multi-ingress domains. Then, we'd have multiple IdPs for +-- a multi-ingress domain and cannot decide which one to choose. The solution +-- to this is that unconfigured domains' IdPs store no domain. I.e. the +-- assignment of domains to IdPs begins when the domain is configured as +-- multi-ingress domain. +testUnconfiguredDomain :: (HasCallStack) => App () +testUnconfiguredDomain = forM_ [Nothing, Just kermitZHost] $ \unconfiguredZHost -> do + withModifiedBackend + def + { sparCfg = + removeField "saml.spSsoUri" + >=> removeField "saml.spAppUri" + >=> removeField "saml.contacts" + >=> setField + "saml.spDomainConfigs" + (object [ernieZHost .= makeSpDomainConfig ernieZHost]) + } + $ \domain -> do + (owner, tid, _) <- createTeam domain 1 + void $ setTeamFeatureStatus owner tid "sso" "enabled" + + SAML.SampleIdP idpmeta1 _ _ _ <- SAML.makeSampleIdPMetadata + idpId1 <- + createIdpWithZHost owner (Just ernieZHost) idpmeta1 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` ernieZHost + resp.jsonBody %. "id" >>= asString + + -- From configured domain to unconfigured -> no multi-ingress domain + updateIdpWithZHost owner (unconfiguredZHost) idpId1 idpmeta1 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + + getIdp owner idpId1 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + + -- From unconfigured back to configured -> add multi-ingress domain + updateIdpWithZHost owner (Just ernieZHost) idpId1 idpmeta1 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` ernieZHost + + getIdp owner idpId1 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` ernieZHost + + -- Create unconfigured -> no multi-ingress domain + SAML.SampleIdP idpmeta2 _ _ _ <- SAML.makeSampleIdPMetadata + idpId2 <- + createIdpWithZHost owner (unconfiguredZHost) idpmeta2 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + resp.jsonBody %. "id" >>= asString + + getIdp owner idpId2 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + + -- Create a second unconfigured -> no multi-ingress domain + SAML.SampleIdP idpmeta3 _ _ _ <- SAML.makeSampleIdPMetadata + idpId3 <- + createIdpWithZHost owner (unconfiguredZHost) idpmeta3 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + resp.jsonBody %. "id" >>= asString + + getIdp owner idpId3 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + +testMultiIngressAtMostOneIdPPerDomain :: (HasCallStack) => App () +testMultiIngressAtMostOneIdPPerDomain = do + withModifiedBackend + def + { sparCfg = + removeField "saml.spSsoUri" + >=> removeField "saml.spAppUri" + >=> removeField "saml.contacts" + >=> setField + "saml.spDomainConfigs" + ( object + [ ernieZHost .= makeSpDomainConfig ernieZHost, + bertZHost .= makeSpDomainConfig bertZHost + ] + ) + } + $ \domain -> do + (owner, tid, _) <- createTeam domain 1 + void $ setTeamFeatureStatus owner tid "sso" "enabled" + + SAML.SampleIdP idpmeta1 _ _ _ <- SAML.makeSampleIdPMetadata + idpId1 <- + createIdpWithZHost owner (Just ernieZHost) idpmeta1 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "id" >>= asString + + -- Creating a second IdP for the same domain -> failure + SAML.SampleIdP idpmeta2 _ _ _ <- SAML.makeSampleIdPMetadata + _idpId2 <- + createIdpWithZHost owner (Just ernieZHost) idpmeta2 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 409 + resp.jsonBody %. "label" `shouldMatch` "idp-duplicate-domain-for-team" + + -- Create an IdP for one domain and update it to another that already has one -> failure + SAML.SampleIdP idpmeta3 _ _ _ <- SAML.makeSampleIdPMetadata + idpId3 <- + createIdpWithZHost owner (Just bertZHost) idpmeta2 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "id" >>= asString + + updateIdpWithZHost owner (Just ernieZHost) idpId3 idpmeta3 + `bindResponse` \resp -> do + resp.status `shouldMatchInt` 409 + resp.jsonBody %. "label" `shouldMatch` "idp-duplicate-domain-for-team" + + -- Create an IdP with no domain and update it to a domain that already has one -> failure + SAML.SampleIdP idpmeta4 _ _ _ <- SAML.makeSampleIdPMetadata + idpId4 <- + createIdpWithZHost owner Nothing idpmeta4 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "id" >>= asString + + updateIdpWithZHost owner (Just ernieZHost) idpId4 idpmeta4 + `bindResponse` \resp -> do + resp.status `shouldMatchInt` 409 + resp.jsonBody %. "label" `shouldMatch` "idp-duplicate-domain-for-team" + + -- Updating an IdP itself should still work + updateIdpWithZHost + owner + (Just ernieZHost) + idpId1 + -- The edIssuer needs to stay unchanged. Otherwise, deletion will fail + -- with a 404 (see bug https://wearezeta.atlassian.net/browse/WPB-20407) + (idpmeta2 & SAML.edIssuer .~ (idpmeta1 ^. SAML.edIssuer)) + `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` ernieZHost + + -- After deletion of the IdP of a domain, a new one can be created + deleteIdp owner idpId1 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 204 + + SAML.SampleIdP idpmeta5 _ _ _ <- SAML.makeSampleIdPMetadata + idpId5 <- + createIdpWithZHost owner (Just ernieZHost) idpmeta5 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` ernieZHost + resp.jsonBody %. "id" >>= asString + + -- After deletion of the IdP of a domain, one can be moved from another domain + SAML.SampleIdP idpmeta6 _ _ _ <- SAML.makeSampleIdPMetadata + createIdpWithZHost owner (Just bertZHost) idpmeta6 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 409 + resp.jsonBody %. "label" `shouldMatch` "idp-duplicate-domain-for-team" + + deleteIdp owner idpId3 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 204 + + idpId6 <- + createIdpWithZHost owner (Just bertZHost) idpmeta6 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` bertZHost + resp.jsonBody %. "id" >>= asString + + updateIdpWithZHost owner (Just ernieZHost) idpId6 idpmeta6 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 409 + resp.jsonBody %. "label" `shouldMatch` "idp-duplicate-domain-for-team" + + deleteIdp owner idpId5 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 204 + + updateIdpWithZHost owner (Just ernieZHost) idpId6 idpmeta6 + `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` ernieZHost + +-- We only record the domain for multi-ingress setups. +testNonMultiIngressSetupsCanHaveMoreIdPsPerDomain :: (HasCallStack) => App () +testNonMultiIngressSetupsCanHaveMoreIdPsPerDomain = do + (owner, tid, _) <- createTeam OwnDomain 1 + void $ setTeamFeatureStatus owner tid "sso" "enabled" + + -- With Z-Host header + SAML.SampleIdP idpmeta1 _ _ _ <- SAML.makeSampleIdPMetadata + idpId1 <- + createIdpWithZHost owner (Just ernieZHost) idpmeta1 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + resp.jsonBody %. "id" >>= asString + + SAML.SampleIdP idpmeta2 _ _ _ <- SAML.makeSampleIdPMetadata + idpId2 <- + createIdpWithZHost owner (Just ernieZHost) idpmeta2 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + resp.jsonBody %. "id" >>= asString + + SAML.SampleIdP idpmeta3 _ _ _ <- SAML.makeSampleIdPMetadata + updateIdpWithZHost owner (Just ernieZHost) idpId1 idpmeta3 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + + SAML.SampleIdP idpmeta4 _ _ _ <- SAML.makeSampleIdPMetadata + updateIdpWithZHost owner (Just ernieZHost) idpId2 idpmeta4 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + + -- Without Z-Host header + SAML.SampleIdP idpmeta5 _ _ _ <- SAML.makeSampleIdPMetadata + idpId5 <- + createIdpWithZHost owner Nothing idpmeta5 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + resp.jsonBody %. "id" >>= asString + + SAML.SampleIdP idpmeta6 _ _ _ <- SAML.makeSampleIdPMetadata + idpId6 <- + createIdpWithZHost owner Nothing idpmeta6 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + resp.jsonBody %. "id" >>= asString + + SAML.SampleIdP idpmeta7 _ _ _ <- SAML.makeSampleIdPMetadata + updateIdpWithZHost owner Nothing idpId5 idpmeta7 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null + + SAML.SampleIdP idpmeta8 _ _ _ <- SAML.makeSampleIdPMetadata + updateIdpWithZHost owner Nothing idpId6 idpmeta8 `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.jsonBody %. "extraInfo.domain" `shouldMatch` Null diff --git a/integration/test/Test/Spar/MultiIngressSSO.hs b/integration/test/Test/Spar/MultiIngressSSO.hs index 3df8c2e50f6..0f5829e6665 100644 --- a/integration/test/Test/Spar/MultiIngressSSO.hs +++ b/integration/test/Test/Spar/MultiIngressSSO.hs @@ -35,8 +35,34 @@ import qualified Text.XML as XML import qualified Text.XML.Cursor as XML import qualified Text.XML.DSig as SAML -testMultiIngressSSO :: (HasCallStack) => App () -testMultiIngressSSO = do +-- | Test multi-ingress SSO with an IdP that is not bound to a domain. +-- +-- The IdP is created via a non-multi-ingress way/domain. It is valid for all +-- domains - no matter if they are configured as multi-ingress domains or not. +-- However, the SP must be consistent in the communication: If the SAML login +-- flow was started on one domain, it must return to exactly this domain. +testMultiIngressSSOGeneralIdp :: (HasCallStack) => App () +testMultiIngressSSOGeneralIdp = multiIngressSSOCommonTest (const . registerTestIdPWithMetaWithPrivateCreds) + +-- | Test multi-ingress SSO with an IdP that is bound to a domain. +-- +-- The IdP is created on a multi-ingress domain. The details of managing +-- multi-ingress IdPs are covered in `Test.Spar.MultiIngressIdp`. Here we want +-- to test that logins are possible with such an IdP, ensuring we haven't +-- broken basic functionality. +testMultiIngressSSODomainBoundIdp :: (HasCallStack) => App () +testMultiIngressSSODomainBoundIdp = multiIngressSSOCommonTest registerTestIdPWithMetaWithPrivateCredsForZHost + +multiIngressSSOCommonTest :: + (HasCallStack) => + ( forall owner. + (HasCallStack, MakesValue owner) => + owner -> + Maybe String -> + App (Response, (SAML.IdPMetadata, SAML.SignPrivCreds)) + ) -> + App () +multiIngressSSOCommonTest registerTestIdPWithMetaWithPrivateCredsFn = do let ernieZHost = "nginz-https.ernie.example.com" bertZHost = "nginz-https.bert.example.com" kermitZHost = "nginz-https.kermit.example.com" @@ -69,12 +95,12 @@ testMultiIngressSSO = do (owner, tid, _) <- createTeam domain 1 void $ setTeamFeatureStatus owner tid "sso" "enabled" - (idp, idpMeta) <- registerTestIdPWithMetaWithPrivateCreds owner + (idp, idpMeta) <- registerTestIdPWithMetaWithPrivateCredsFn owner (Just ernieZHost) idpId <- asString $ idp.json %. "id" ernieEmail <- ("ernie@" <>) <$> randomDomain - checkMetadataSPIssuer domain ernieZHost tid - checkAuthnSPIssuer domain ernieZHost idpId tid + checkSPMetadata domain ernieZHost tid + checkAuthnRequest domain ernieZHost idpId tid finalizeLoginWithWrongZHost bertZHost ernieZHost domain tid ernieEmail (idpId, idpMeta) `bindResponse` \resp -> do resp.status `shouldMatchInt` 200 @@ -89,8 +115,8 @@ testMultiIngressSSO = do makeSuccessfulSamlLogin domain ernieZHost tid ernieEmail idpId idpMeta bertEmail <- ("bert@" <>) <$> randomDomain - checkMetadataSPIssuer domain bertZHost tid - checkAuthnSPIssuer domain bertZHost idpId tid + checkSPMetadata domain bertZHost tid + checkAuthnRequest domain bertZHost idpId tid makeSuccessfulSamlLogin domain bertZHost tid bertEmail idpId idpMeta @@ -107,8 +133,11 @@ testMultiIngressSSO = do finalizeLoginWithWrongZHost bertZHost kermitZHost domain tid kermitEmail (idpId, idpMeta) `bindResponse` \resp -> do resp.status `shouldMatchInt` 404 -checkAuthnSPIssuer :: (HasCallStack) => String -> String -> String -> String -> App () -checkAuthnSPIssuer domain host idpId tid = +-- | Check the AuthnRequest by the SP (Wire backend) to be sent to the IdP +-- +-- Most important: The @Issuer@ must fit to the multi-ingress domain (@host@). +checkAuthnRequest :: (HasCallStack) => String -> String -> String -> String -> App () +checkAuthnRequest domain host idpId tid = initiateSamlLoginWithZHost domain (Just host) idpId `bindResponse` \authnreq -> do authnreq.status `shouldMatchInt` 200 @@ -133,8 +162,9 @@ checkAuthnSPIssuer domain host idpId tid = getIssuerUrl authnreq.body `shouldMatch` targetSPUrl -checkMetadataSPIssuer :: (HasCallStack) => String -> String -> String -> App () -checkMetadataSPIssuer domain host tid = +-- | Check the metadata of the ServiceProvider (i.e. of the Wire backend on multi-ingress domain @host@) +checkSPMetadata :: (HasCallStack) => String -> String -> String -> App () +checkSPMetadata domain host tid = getSPMetadataWithZHost domain (Just host) tid `bindResponse` \resp -> do resp.status `shouldMatchInt` 200 diff --git a/integration/test/Test/Spar/STM.hs b/integration/test/Test/Spar/STM.hs index 0cbef532a2d..cbf8fb0c3a0 100644 --- a/integration/test/Test/Spar/STM.hs +++ b/integration/test/Test/Spar/STM.hs @@ -9,11 +9,12 @@ -- and thus get property-based integration tests! module Test.Spar.STM (testCreateIdpsAndScimsV7) where -import API.BrigInternal (getInvitationByEmail) +import API.BrigInternal import API.Common (defPassword) import API.GalleyInternal (setTeamFeatureStatus) import API.Nginz (login) import API.Spar +import Control.Retry import qualified Data.Map as Map import qualified SAML2.WebSSO as SAML import SetupHelpers @@ -235,7 +236,16 @@ validateStateLoginAllUsers owner tid state = do void $ loginWithSamlEmail True tid email idp bindResponse (deleteScimUser owner (unScimToken tok) uid) $ \resp -> do resp.status `shouldMatchInt` 204 + waitForUserGone uid void $ loginWithSamlEmail False tid email idp + where + waitForUserGone :: String -> App () + waitForUserGone uid = do + let pol = limitRetriesByCumulativeDelay 12_000_000 (fullJitterBackoff 5_000) + void $ recoverAll pol $ const do + getUsersId OwnDomain [uid] `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json `shouldMatch` ([] :: [()]) validateError :: Response -> Int -> String -> App () validateError resp errStatus errLabel = do diff --git a/integration/test/Test/Swagger.hs b/integration/test/Test/Swagger.hs index 6b64f9bb924..0b7ce31ec02 100644 --- a/integration/test/Test/Swagger.hs +++ b/integration/test/Test/Swagger.hs @@ -30,7 +30,7 @@ import Testlib.Prelude import UnliftIO.Temporary existingVersions :: Set Int -existingVersions = Set.fromList [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] +existingVersions = Set.fromList [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] internalApis :: Set String internalApis = Set.fromList ["brig", "cannon", "cargohold", "cannon", "spar"] diff --git a/integration/test/Test/UserGroup.hs b/integration/test/Test/UserGroup.hs index ac0101c68b3..c03c1389e22 100644 --- a/integration/test/Test/UserGroup.hs +++ b/integration/test/Test/UserGroup.hs @@ -359,15 +359,15 @@ testUserGroupGetGroupsAllInputs = do includeMemberCount = includeMemberCount', includeChannels = includeChannels' } - | q' <- qs, - sortBy' <- sortByKeysList, - sortOrder' <- sortOrders, - pSize' <- pSizes, - lastName' <- lastNames, - lastCreatedAt' <- lastCreatedAts, - lastId' <- lastIds, - includeMemberCount' <- [False, True], - includeChannels' <- [False, True] + | q' <- qs, + sortBy' <- sortByKeysList, + sortOrder' <- sortOrders, + pSize' <- pSizes, + lastName' <- lastNames, + lastCreatedAt' <- lastCreatedAts, + lastId' <- lastIds, + includeMemberCount' <- [False, True], + includeChannels' <- [False, True] ] where qs = [Nothing, Just "A"] diff --git a/integration/test/Testlib/Certs.hs b/integration/test/Testlib/Certs.hs index 69d7b9c59b8..ed56b0b50e0 100644 --- a/integration/test/Testlib/Certs.hs +++ b/integration/test/Testlib/Certs.hs @@ -20,7 +20,7 @@ module Testlib.Certs where import Crypto.Hash.Algorithms (SHA256 (SHA256)) import qualified Crypto.PubKey.RSA as RSA import qualified Crypto.PubKey.RSA.PKCS15 as PKCS15 -import Crypto.Store.PKCS8 (PrivateKeyFormat (PKCS8Format), keyToPEM) +import Crypto.Store.PKCS8 (PrivateKeyFormat (PKCS8Format), keyPairFromPrivKey, keyToPEM) import Crypto.Store.X509 (pubKeyToPEM) import Data.ASN1.OID (OIDable (getObjectID)) import Data.Hourglass @@ -43,7 +43,7 @@ signedCertToString = toPem . PEM "CERTIFICATE" [] . encodeSignedObject -- | convert a private key to string privateKeyToString :: RSA.PrivateKey -> String -privateKeyToString = toPem . keyToPEM PKCS8Format . PrivKeyRSA +privateKeyToString = toPem . keyToPEM PKCS8Format . keyPairFromPrivKey . PrivKeyRSA -- | convert a public key to string publicKeyToString :: RSA.PublicKey -> String diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index 67a90228ad0..1a65d7f5ea1 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -133,7 +133,7 @@ mkGlobalEnv cfgFile = do gFederationV1Domain = intConfig.federationV1.originDomain, gFederationV2Domain = intConfig.federationV2.originDomain, gDynamicDomains = (.domain) <$> Map.elems intConfig.dynamicBackends, - gDefaultAPIVersion = 14, + gDefaultAPIVersion = 15, gManager = manager, gServicesCwdBase = devEnvProjectRoot <&> ( "services"), gBackendResourcePool = resourcePool, diff --git a/integration/test/Testlib/MockIntegrationService.hs b/integration/test/Testlib/MockIntegrationService.hs index 71dc2477cd3..7962fb4052f 100644 --- a/integration/test/Testlib/MockIntegrationService.hs +++ b/integration/test/Testlib/MockIntegrationService.hs @@ -37,6 +37,7 @@ import qualified Data.Aeson import qualified Data.ByteString.Lazy as LBS import Data.Streaming.Network import Data.String.Conversions (cs) +import Data.Type.Equality import Network.HTTP.Types import Network.Socket import qualified Network.Socket as Socket diff --git a/integration/test/Testlib/ModService.hs b/integration/test/Testlib/ModService.hs index e111bcb76b2..3939c17164f 100644 --- a/integration/test/Testlib/ModService.hs +++ b/integration/test/Testlib/ModService.hs @@ -304,10 +304,10 @@ updateServiceMapInConfig resource forSrv config = ) config [ (srv, berInternalServicePorts resource srv :: Int) - | srv <- allServices, - -- if a service is not enabled, do not set its endpoint configuration, - -- unless we are starting the service itself - berEnableService resource srv || srv == forSrv + | srv <- allServices, + -- if a service is not enabled, do not set its endpoint configuration, + -- unless we are starting the service itself + berEnableService resource srv || srv == forSrv ] startBackend :: diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index 1ae1ddf06dd..0566cc00f22 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -28,7 +28,7 @@ import Data.Foldable import Data.Function import Data.Functor import Data.List -import Data.Maybe (fromMaybe) +import Data.Maybe import Data.String (IsString (fromString)) import Data.String.Conversions (cs) import Data.Text (Text) @@ -188,10 +188,12 @@ runMigrations :: App () runMigrations = do cwdBase <- asks (.servicesCwdBase) let brig = "brig" - let (cwd, exe) = case cwdBase of + (cwd, exe) = case cwdBase of Nothing -> (Nothing, brig) Just dir -> (Just (dir brig), "../../dist" brig) + -- servicesCwdBase is only set for local binaries + isLocal = isJust cwd getConfig <- readAndUpdateConfig def backendA Brig config <- liftIO getConfig tempFile <- liftIO $ writeTempFile "/tmp" "brig-migrations.yaml" (cs $ Yaml.encode config) @@ -199,7 +201,7 @@ runMigrations = do pool <- asks (.resourcePool) lowerCodensity $ do resources <- acquireResources (length dynDomains) pool - let dbnames = [backendA.berPostgresqlDBName, backendB.berPostgresqlDBName] <> map (.berPostgresqlDBName) resources + let dbnames = [dbs | isLocal, dbs <- [backendA.berPostgresqlDBName, backendB.berPostgresqlDBName]] <> map (.berPostgresqlDBName) resources for_ dbnames $ runMigration exe tempFile cwd liftIO $ putStrLn "Postgres migrations finished" where diff --git a/libs/bilge/bilge.cabal b/libs/bilge/bilge.cabal index 8e64bbe92e5..9aefddd77d4 100644 --- a/libs/bilge/bilge.cabal +++ b/libs/bilge/bilge.cabal @@ -98,6 +98,7 @@ library , uri-bytestring , wai , wai-extra + , wai-utilities , wire-otel default-language: GHC2021 diff --git a/libs/bilge/default.nix b/libs/bilge/default.nix index 1844d50b1d2..0f90d357e62 100644 --- a/libs/bilge/default.nix +++ b/libs/bilge/default.nix @@ -26,6 +26,7 @@ , uri-bytestring , wai , wai-extra +, wai-utilities , wire-otel }: mkDerivation { @@ -54,6 +55,7 @@ mkDerivation { uri-bytestring wai wai-extra + wai-utilities wire-otel ]; description = "Library for composing HTTP requests"; diff --git a/libs/bilge/src/Bilge/Assert.hs b/libs/bilge/src/Bilge/Assert.hs index a13e624aa51..5c669040225 100644 --- a/libs/bilge/src/Bilge/Assert.hs +++ b/libs/bilge/src/Bilge/Assert.hs @@ -43,6 +43,7 @@ import Data.ByteString qualified as S import Data.ByteString.Lazy qualified as Lazy import Imports import Network.HTTP.Client +import Network.Wai.Utilities.Exception (displayExceptionNoBacktrace) import System.Console.ANSI import Text.Printf @@ -98,7 +99,7 @@ io m a - printErr e = error $ title "Error executing request: " ++ err (show e) + printErr e = error $ title "Error executing request: " ++ err (displayExceptionNoBacktrace e) -- | Like ' Msg -> Msg rpcExceptionMsg (RPCException sys req ex) = - "remote" .= sys ~~ "path" .= HTTP.path req ~~ headers ~~ msg (show ex) + "remote" .= sys ~~ "path" .= HTTP.path req ~~ headers ~~ msg (displayExceptionNoBacktrace ex) where headers = foldr hdr id (HTTP.requestHeaders req) hdr (k, v) x = x ~~ original k .= v diff --git a/libs/brig-types/brig-types.cabal b/libs/brig-types/brig-types.cabal index 817720e0b65..2ce9b7dc42f 100644 --- a/libs/brig-types/brig-types.cabal +++ b/libs/brig-types/brig-types.cabal @@ -14,7 +14,6 @@ library exposed-modules: Brig.Types.Activation Brig.Types.Connection - Brig.Types.Instances Brig.Types.Intra Brig.Types.Provider.Tag Brig.Types.Team @@ -72,13 +71,12 @@ library -funbox-strict-fields -Wredundant-constraints -Wunused-packages build-depends: - base >=4 && <5 - , bytestring-conversion >=0.2 + base >=4 && <5 , cassandra-util - , containers >=0.5 + , containers >=0.5 , imports - , QuickCheck >=2.9 - , types-common >=0.16 + , QuickCheck >=2.9 + , types-common >=0.16 , wire-api default-language: GHC2021 diff --git a/libs/brig-types/default.nix b/libs/brig-types/default.nix index d427109a406..4611943535f 100644 --- a/libs/brig-types/default.nix +++ b/libs/brig-types/default.nix @@ -5,7 +5,6 @@ { mkDerivation , aeson , base -, bytestring-conversion , cassandra-util , containers , gitignoreSource @@ -24,7 +23,6 @@ mkDerivation { src = gitignoreSource ./.; libraryHaskellDepends = [ base - bytestring-conversion cassandra-util containers imports diff --git a/libs/brig-types/src/Brig/Types/Instances.hs b/libs/brig-types/src/Brig/Types/Instances.hs deleted file mode 100644 index ca5fb8f6aa0..00000000000 --- a/libs/brig-types/src/Brig/Types/Instances.hs +++ /dev/null @@ -1,90 +0,0 @@ -{-# OPTIONS_GHC -fno-warn-orphans #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Brig.Types.Instances () where - -import Brig.Types.Provider.Tag -import Cassandra.CQL -import Data.ByteString.Conversion -import Imports -import Wire.API.Provider -import Wire.API.Provider.Service -import Wire.API.User.Client.Prekey - -instance Cql PrekeyId where - ctype = Tagged IntColumn - toCql = CqlInt . fromIntegral . keyId - fromCql (CqlInt i) = pure $ PrekeyId (fromIntegral i) - fromCql _ = Left "PrekeyId: Int expected" - -instance Cql ServiceTag where - ctype = Tagged BigIntColumn - - fromCql (CqlBigInt i) = case intToTag i of - Just t -> pure t - Nothing -> Left $ "unexpected service tag: " ++ show i - fromCql _ = Left "service tag: int expected" - - toCql = CqlBigInt . tagToInt - -instance Cql ServiceKeyPEM where - ctype = Tagged BlobColumn - - fromCql (CqlBlob b) = - maybe - (Left "service key pem: malformed key") - pure - (fromByteString' b) - fromCql _ = Left "service key pem: blob expected" - - toCql = CqlBlob . toByteString - -instance Cql ServiceKey where - ctype = - Tagged - ( UdtColumn - "pubkey" - [ ("typ", IntColumn), - ("size", IntColumn), - ("pem", BlobColumn) - ] - ) - - fromCql (CqlUdt fs) = do - t <- required "typ" - s <- required "size" - p <- required "pem" - case (t :: Int32) of - 0 -> pure $! ServiceKey RsaServiceKey s p - _ -> Left $ "Unexpected service key type: " ++ show t - where - required :: (Cql r) => Text -> Either String r - required f = - maybe - (Left ("ServiceKey: Missing required field '" ++ show f ++ "'")) - fromCql - (lookup f fs) - fromCql _ = Left "service key: udt expected" - - toCql (ServiceKey RsaServiceKey siz pem) = - CqlUdt - [ ("typ", CqlInt 0), - ("size", toCql siz), - ("pem", toCql pem) - ] diff --git a/libs/brig-types/src/Brig/Types/Provider/Tag.hs b/libs/brig-types/src/Brig/Types/Provider/Tag.hs index 7104ba2713b..2aa2704e352 100644 --- a/libs/brig-types/src/Brig/Types/Provider/Tag.hs +++ b/libs/brig-types/src/Brig/Types/Provider/Tag.hs @@ -38,7 +38,7 @@ import Data.Bits import Data.Range import Data.Set qualified as Set import Imports -import Wire.API.Provider.Service.Tag +import Wire.API.Provider.Service.Tag (ServiceTag (..)) newtype Bucket = Bucket Int32 deriving newtype (Cql, Show) diff --git a/libs/dns-util/dns-util.cabal b/libs/dns-util/dns-util.cabal index 4cfa56f240e..7b120e36e1c 100644 --- a/libs/dns-util/dns-util.cabal +++ b/libs/dns-util/dns-util.cabal @@ -11,6 +11,14 @@ license: AGPL-3 license-file: LICENSE build-type: Simple +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +flag nix-dev-env + description: In a Nix dev environment. + default: False + manual: True + library exposed-modules: Wire.Network.DNS.Effect @@ -132,7 +140,9 @@ test-suite spec -threaded -rtsopts -with-rtsopts=-N -Wredundant-constraints -Wunused-packages -Wno-x-partial - build-tool-depends: hspec-discover:hspec-discover + if !flag(nix-dev-env) + build-tool-depends: hspec-discover:hspec-discover + build-depends: base >=4.6 && <5.0 , dns diff --git a/libs/extended/extended.cabal b/libs/extended/extended.cabal index a771fe15901..3828324caa2 100644 --- a/libs/extended/extended.cabal +++ b/libs/extended/extended.cabal @@ -16,6 +16,14 @@ license: AGPL-3 license-file: LICENSE build-type: Simple +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +flag nix-dev-env + description: In a Nix dev environment. + default: False + manual: True + library -- cabal-fmt: expand src exposed-modules: @@ -172,7 +180,9 @@ test-suite extended-tests -threaded -with-rtsopts=-N -Wredundant-constraints -Wunused-packages - build-tool-depends: hspec-discover:hspec-discover + if !flag(nix-dev-env) + build-tool-depends: hspec-discover:hspec-discover + build-depends: aeson , base diff --git a/libs/galley-types/src/Galley/Types/Teams.hs b/libs/galley-types/src/Galley/Types/Teams.hs index 2d97f3f5430..6177db2ef4e 100644 --- a/libs/galley-types/src/Galley/Types/Teams.hs +++ b/libs/galley-types/src/Galley/Types/Teams.hs @@ -1,9 +1,5 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE StandaloneKindSignatures #-} {-# LANGUAGE StrictData #-} -{-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -Wno-ambiguous-fields #-} -- This file is part of the Wire Server implementation. @@ -24,9 +20,7 @@ -- with this program. If not, see . module Galley.Types.Teams - ( TeamCreationTime (..), - tcTime, - GetFeatureDefaults (..), + ( GetFeatureDefaults (..), FeatureDefaults (..), FeatureFlags, featureDefaults, @@ -38,7 +32,7 @@ module Galley.Types.Teams ) where -import Control.Lens (makeLenses, view) +import Control.Lens (view) import Data.Aeson import Data.Aeson.Key qualified as Key import Data.Aeson.Types qualified as A @@ -53,11 +47,6 @@ import Wire.API.Team.Feature import Wire.API.Team.Member import Wire.API.Team.Permission --- This is the cassandra timestamp of writetime(binding) -newtype TeamCreationTime = TeamCreationTime - { _tcTime :: Int64 - } - -- | Used to extract the feature config type out of 'FeatureDefaults' or -- related types. type family ConfigOf a @@ -228,6 +217,13 @@ newtype instance FeatureDefaults ChannelsConfig deriving (FromJSON) via Defaults (LockableFeature ChannelsConfig) deriving (ParseFeatureDefaults) via OptionalField ChannelsConfig +newtype instance FeatureDefaults CellsInternalConfig + = CellsInternalDefaults (LockableFeature CellsInternalConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature CellsInternalConfig) + deriving (ParseFeatureDefaults) via OptionalField CellsInternalConfig + data instance FeatureDefaults ExposeInvitationURLsToTeamAdminConfig = ExposeInvitationURLsToTeamAdminDefaults deriving stock (Eq, Show) @@ -334,6 +330,20 @@ newtype instance FeatureDefaults StealthUsersConfig deriving (FromJSON) via Defaults (LockableFeature StealthUsersConfig) deriving (ParseFeatureDefaults) via OptionalField StealthUsersConfig +newtype instance FeatureDefaults MeetingsConfig + = MeetingDefaults (LockableFeature MeetingsConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature MeetingsConfig) + deriving (ParseFeatureDefaults) via OptionalField MeetingsConfig + +newtype instance FeatureDefaults MeetingsPremiumConfig + = MeetingPremiumDefaults (LockableFeature MeetingsPremiumConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature MeetingsPremiumConfig) + deriving (ParseFeatureDefaults) via OptionalField MeetingsPremiumConfig + featureKey :: forall cfg. (IsFeatureConfig cfg) => Key.Key featureKey = Key.fromText $ featureName @cfg @@ -399,8 +409,6 @@ instance (FromJSON a) => FromJSON (Defaults a) where parseJSON = withObject "default object" $ \ob -> Defaults <$> (ob .: "defaults") -makeLenses ''TeamCreationTime - notTeamMember :: [UserId] -> [TeamMember] -> [UserId] notTeamMember uids tmms = Set.toList $ diff --git a/libs/hscim/default.nix b/libs/hscim/default.nix index 477d12f2e53..d0a64d40d04 100644 --- a/libs/hscim/default.nix +++ b/libs/hscim/default.nix @@ -10,6 +10,7 @@ , base , bytestring , case-insensitive +, containers , email-validate , gitignoreSource , hashable @@ -21,8 +22,10 @@ , http-api-data , http-media , http-types +, HUnit , hw-hspec-hedgehog , indexed-traversable +, lens-aeson , lib , list-t , microlens @@ -43,6 +46,7 @@ , time , utf8-string , uuid +, vector , wai , wai-extra , wai-utilities @@ -62,6 +66,7 @@ mkDerivation { base bytestring case-insensitive + containers email-validate hashable hspec @@ -113,14 +118,17 @@ mkDerivation { hspec-expectations hspec-wai http-types + HUnit hw-hspec-hedgehog indexed-traversable + lens-aeson microlens network-uri servant servant-server stm-containers text + vector wai wai-extra ]; diff --git a/libs/hscim/hscim.cabal b/libs/hscim/hscim.cabal index e29f6db05f6..96d62972d02 100644 --- a/libs/hscim/hscim.cabal +++ b/libs/hscim/hscim.cabal @@ -25,6 +25,14 @@ source-repository head location: https://github.com/wireapp/wire-server subdir: libs/hscim +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +flag nix-dev-env + description: In a Nix dev environment. + default: False + manual: True + library exposed-modules: Web.Scim.AttrName @@ -92,6 +100,7 @@ library , base , bytestring , case-insensitive + , containers , email-validate , hashable , hspec @@ -206,7 +215,9 @@ test-suite spec -Wall -threaded -rtsopts -with-rtsopts=-N -Wredundant-constraints -Wunused-packages - build-tool-depends: hspec-discover:hspec-discover + if !flag(nix-dev-env) + build-tool-depends: hspec-discover:hspec-discover + build-depends: aeson , attoparsec @@ -219,14 +230,17 @@ test-suite spec , hspec-expectations , hspec-wai , http-types + , HUnit , hw-hspec-hedgehog , indexed-traversable + , lens-aeson , microlens , network-uri , servant , servant-server , stm-containers , text + , vector , wai , wai-extra diff --git a/libs/hscim/src/Web/Scim/Class/Group.hs b/libs/hscim/src/Web/Scim/Class/Group.hs index 943484696a7..16ef2d0140b 100644 --- a/libs/hscim/src/Web/Scim/Class/Group.hs +++ b/libs/hscim/src/Web/Scim/Class/Group.hs @@ -30,7 +30,7 @@ where import Data.Aeson import qualified Data.Aeson as Aeson -import Data.Text +import Data.Text hiding (show) import Servant import Servant.API.Generic import Servant.Server.Generic @@ -85,6 +85,8 @@ data GroupSite tag route = GroupSite { gsGetGroups :: route :- QueryParam "filter" Filter + :> QueryParam "startIndex" Int + :> QueryParam "count" Int :> Get '[SCIM] (ListResponse (StoredGroup tag)), gsGetGroup :: route @@ -119,6 +121,8 @@ class (Monad m, GroupTypes tag, AuthDB tag m) => GroupDB tag m where getGroups :: AuthInfo tag -> Maybe Filter -> + Maybe Int -> + Maybe Int -> ScimHandler m (ListResponse (StoredGroup tag)) -- | Get a single group by ID. @@ -179,9 +183,9 @@ groupServer :: GroupSite tag (AsServerT (ScimHandler m)) groupServer authData = GroupSite - { gsGetGroups = \mbFilter -> do + { gsGetGroups = \mbFilter mbStartIndex mbCount -> do auth <- authCheck @tag authData - getGroups @tag auth mbFilter, + getGroups @tag auth mbFilter mbStartIndex mbCount, gsGetGroup = \gid -> do auth <- authCheck @tag authData getGroup @tag auth gid, diff --git a/libs/hscim/src/Web/Scim/Schema/AuthenticationScheme.hs b/libs/hscim/src/Web/Scim/Schema/AuthenticationScheme.hs index 45c54868649..0b37f8d5712 100644 --- a/libs/hscim/src/Web/Scim/Schema/AuthenticationScheme.hs +++ b/libs/hscim/src/Web/Scim/Schema/AuthenticationScheme.hs @@ -25,7 +25,7 @@ module Web.Scim.Schema.AuthenticationScheme where import Data.Aeson -import Data.Text +import Data.Text hiding (show) import GHC.Generics import Network.URI.Static import Web.Scim.Schema.Common diff --git a/libs/hscim/src/Web/Scim/Schema/User/Address.hs b/libs/hscim/src/Web/Scim/Schema/User/Address.hs index 4d53badf500..1a725936701 100644 --- a/libs/hscim/src/Web/Scim/Schema/User/Address.hs +++ b/libs/hscim/src/Web/Scim/Schema/User/Address.hs @@ -18,7 +18,7 @@ module Web.Scim.Schema.User.Address where import Data.Aeson -import Data.Text hiding (dropWhile) +import Data.Text hiding (dropWhile, show) import GHC.Generics (Generic) import Web.Scim.Schema.Common diff --git a/libs/hscim/src/Web/Scim/Schema/User/Email.hs b/libs/hscim/src/Web/Scim/Schema/User/Email.hs index cd52a80a7e8..0b8bf7e919b 100644 --- a/libs/hscim/src/Web/Scim/Schema/User/Email.hs +++ b/libs/hscim/src/Web/Scim/Schema/User/Email.hs @@ -19,7 +19,7 @@ module Web.Scim.Schema.User.Email where import Control.Applicative ((<|>)) import Data.Aeson -import Data.Text hiding (dropWhile) +import Data.Text hiding (dropWhile, show) import Data.Text.Encoding (decodeUtf8, encodeUtf8) import GHC.Generics (Generic) import qualified Text.Email.Validate as Email diff --git a/libs/hscim/src/Web/Scim/Schema/User/Phone.hs b/libs/hscim/src/Web/Scim/Schema/User/Phone.hs index 883a1b77140..5d4be4c4172 100644 --- a/libs/hscim/src/Web/Scim/Schema/User/Phone.hs +++ b/libs/hscim/src/Web/Scim/Schema/User/Phone.hs @@ -18,7 +18,7 @@ module Web.Scim.Schema.User.Phone where import Data.Aeson -import Data.Text hiding (dropWhile) +import Data.Text hiding (dropWhile, show) import GHC.Generics (Generic) import Web.Scim.Schema.Common diff --git a/libs/hscim/src/Web/Scim/Server/Mock.hs b/libs/hscim/src/Web/Scim/Server/Mock.hs index eb2dc9ce5ea..3b819c16621 100644 --- a/libs/hscim/src/Web/Scim/Server/Mock.hs +++ b/libs/hscim/src/Web/Scim/Server/Mock.hs @@ -1,4 +1,5 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE ViewPatterns #-} {-# OPTIONS_GHC -fno-warn-orphans #-} -- This file is part of the Wire Server implementation. @@ -29,7 +30,11 @@ import Control.Monad.Reader import Control.Monad.STM (STM, atomically) import Data.Aeson import qualified Data.CaseInsensitive as CI +import qualified Data.Foldable as Fold import Data.Hashable +import Data.Maybe (fromMaybe) +import Data.Sequence (Seq) +import qualified Data.Sequence as Seq import Data.Text (Text, pack) import Data.Time.Calendar import Data.Time.Clock @@ -50,7 +55,7 @@ import Web.Scim.Schema.Error import Web.Scim.Schema.ListResponse import Web.Scim.Schema.Meta import Web.Scim.Schema.ResourceType -import Web.Scim.Schema.Schema (Schema (Group20, User20)) +import Web.Scim.Schema.Schema (Schema (Group20, ListResponse20, User20)) import Web.Scim.Schema.User hiding (displayName) -- | Tag used in the mock server. @@ -155,7 +160,7 @@ instance GroupTypes Mock where type GroupId Mock = Id instance GroupDB Mock TestServer where - getGroups () mbFilter = do + getGroups () mbFilter mbStartIndex mbCount = do m <- asks groupDB groups <- map snd <$> liftSTM (ListT.toList $ STMMap.listT m) case mbFilter of @@ -170,7 +175,7 @@ instance GroupDB Mock TestServer where in pureSorted $ filter p groups _ -> throwScim $ badRequest InvalidFilter $ Just "Only displayName filter supported" where - pureSorted groups = pure $ fromList $ sortWith (Common.id . thing) groups + pureSorted groups = pure $ toPage (fromMaybe 1 mbStartIndex) mbCount $ sortWith (Common.id . thing) groups getGroup () gid = do m <- asks groupDB @@ -202,6 +207,31 @@ instance GroupDB Mock TestServer where Nothing -> throwScim (notFound "Group" (pack (show gid))) Just _ -> liftSTM $ STMMap.delete gid m +toPage :: forall a. Int -> Maybe Int -> [a] -> ListResponse a +toPage (max 1 -> startIx) mbCount list = case mbCount of + Nothing -> + ListResponse + { Web.Scim.Schema.ListResponse.schemas = [ListResponse20], + totalResults = totalResults', + startIndex = startIx, + itemsPerPage = Seq.length list', + resources = Fold.toList list' + } + Just count -> + let (page, _rest) = Seq.splitAt (fromIntegral safeCount) list' + safeCount = max 0 (min count maxBound) + in ListResponse + { Web.Scim.Schema.ListResponse.schemas = [ListResponse20], + totalResults = totalResults', + startIndex = startIx, + itemsPerPage = Seq.length page, + resources = Fold.toList page + } + where + totalResults' = length list + list' :: Seq a + list' = Seq.drop (startIx - 1) (Seq.fromList list) + ---------------------------------------------------------------------------- -- AuthDB @@ -234,9 +264,11 @@ createMeta rType = lastModified = testDate, version = Weak "testVersion", location = - Common.URI $ -- FUTUREWORK: getting the actual schema, authority, and path here - -- is a bit of work, but it may be required one day. - URI "https:" (Just $ URI.URIAuth "" "example.com" "") "/Users/id" "" "" + Common.URI + (URI "https:" (Just $ URI.URIAuth "" "example.com" "") "/Users/id" "" "") + -- FUTUREWORK: getting the actual schema, authority, and + -- path here is a bit of work, but it may be required one + -- day. } -- Natural transformation from our transformer stack to the Servant stack diff --git a/libs/hscim/src/Web/Scim/Test/Util.hs b/libs/hscim/src/Web/Scim/Test/Util.hs index da75b438a47..d4ef837eeed 100644 --- a/libs/hscim/src/Web/Scim/Test/Util.hs +++ b/libs/hscim/src/Web/Scim/Test/Util.hs @@ -62,7 +62,7 @@ import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as BS8 import qualified Data.ByteString.Lazy as L import Data.Proxy -import Data.Text +import Data.Text hiding (show) import Data.UUID as UUID import Data.UUID.V4 as UUID import GHC.Stack diff --git a/libs/hscim/test/Test/Class/GroupSpec.hs b/libs/hscim/test/Test/Class/GroupSpec.hs index a48f92aa3cf..8a2de4e85c2 100644 --- a/libs/hscim/test/Test/Class/GroupSpec.hs +++ b/libs/hscim/test/Test/Class/GroupSpec.hs @@ -22,21 +22,43 @@ module Test.Class.GroupSpec ) where +import Control.Monad +import qualified Data.Aeson as A +import qualified Data.Aeson.Lens as A +import qualified Data.ByteString as BS import Data.ByteString.Lazy (ByteString) +import Data.String +import qualified Data.Vector as V +import Lens.Micro import Network.Wai (Application) +import Network.Wai.Test import Servant (Proxy (Proxy)) import Servant.API.Generic +import Test.HUnit import Test.Hspec hiding (shouldSatisfy) import Test.Hspec.Wai hiding (patch, post, put, shouldRespondWith) import Web.Scim.Server (GroupAPI, groupServer, mkapp) import Web.Scim.Server.Mock import Web.Scim.Test.Util +fail_ :: (HasCallStack) => String -> WaiSession () a +fail_ = liftIO . assertFailure + +getJson :: BS.ByteString -> WaiSession () A.Value +getJson path' = + get path' + >>= ( \case + Just v -> pure v + Nothing -> fail_ "Response doesn't parse as JSON!" + ) + . A.decode + . simpleBody + app :: IO Application app = do storage <- emptyTestStorage let auth = Just "authorized" - pure $ + pure $ do mkapp @Mock (Proxy @(GroupAPI Mock)) (toServant (groupServer @Mock auth)) @@ -50,6 +72,45 @@ spec = with app $ do it "can insert then retrieve stored group" $ do post "/" adminGroup `shouldRespondWith` 201 get "/" `shouldRespondWith` groups + + it "paginates groups" $ do + forM_ [0 .. 4 :: Int] $ \_ -> post "/" adminGroup `shouldRespondWith` 201 + + let numFieldMatch :: A.Value -> String -> Int -> WaiSession () () + numFieldMatch v field expected = do + case v ^? A.key (fromString field) . A._Number of + Just gotten -> + when (gotten /= fromIntegral expected) $ do + fail_ $ field <> " is " <> show gotten <> " instead of " <> show expected + Nothing -> fail_ $ "missing field: " <> field + + hasMembers :: (HasCallStack) => WaiSession () A.Value -> Int -> Int -> Int -> WaiSession () () + hasMembers getPage expectedStartIndex expectedItemsPerPage expectedTotalResults = do + page :: A.Value <- getPage + case page ^? A.key "Resources" . A._Array of + Just v -> do + let l = V.length v + when (l /= expectedItemsPerPage) $ fail_ $ "ListResponse Resources has length " <> show l <> " instead of " <> show expectedItemsPerPage + Nothing -> fail_ "missing Resources field" + numFieldMatch page "totalResults" expectedTotalResults + numFieldMatch page "itemsPerPage" expectedItemsPerPage + numFieldMatch page "startIndex" expectedStartIndex + + hasMembers (getJson "/?startIndex=1&count=2") 1 2 5 + hasMembers (getJson "/?startIndex=3&count=2") 3 2 5 + hasMembers (getJson "/?startIndex=5&count=2") 5 1 5 + + hasMembers (getJson "/?startIndex=1&count=100") 1 5 5 + hasMembers (getJson "/?startIndex=6&count=100") 6 0 5 + + hasMembers (getJson "/?startIndex=1") 1 5 5 + hasMembers (getJson "/?startIndex=2") 2 4 5 + hasMembers (getJson "/?startIndex=3") 3 3 5 + hasMembers (getJson "/?startIndex=4") 4 2 5 + hasMembers (getJson "/?startIndex=5") 5 1 5 + hasMembers (getJson "/?startIndex=6") 6 0 5 + hasMembers (getJson "/") 1 5 5 + describe "GET /Groups/:id" $ do it "responds with 404 for unknown group" $ do get "/9999" `shouldRespondWith` 404 diff --git a/libs/http2-manager/http2-manager.cabal b/libs/http2-manager/http2-manager.cabal index 3aed0c465ba..87d6d58241e 100644 --- a/libs/http2-manager/http2-manager.cabal +++ b/libs/http2-manager/http2-manager.cabal @@ -20,6 +20,14 @@ extra-source-files: test/resources/unit-ca-key.pem test/resources/unit-ca.pem +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +flag nix-dev-env + description: In a Nix dev environment. + default: False + manual: True + library -- cabal-fmt: expand src exposed-modules: @@ -53,8 +61,8 @@ flag test-trailing-dot default: True test-suite http2-manager-tests - type: exitcode-stdio-1.0 - main-is: Main.hs + type: exitcode-stdio-1.0 + main-is: Main.hs ghc-options: -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path @@ -69,8 +77,11 @@ test-suite http2-manager-tests Main Test.HTTP2.Client.ManagerSpec - hs-source-dirs: test - build-tool-depends: hspec-discover:hspec-discover + hs-source-dirs: test + + if !flag(nix-dev-env) + build-tool-depends: hspec-discover:hspec-discover + build-depends: async , base diff --git a/libs/http2-manager/src/HTTP2/Client/Manager/Internal.hs b/libs/http2-manager/src/HTTP2/Client/Manager/Internal.hs index 1982979812a..5de12d91da9 100644 --- a/libs/http2-manager/src/HTTP2/Client/Manager/Internal.hs +++ b/libs/http2-manager/src/HTTP2/Client/Manager/Internal.hs @@ -43,7 +43,9 @@ import qualified Data.Text.Encoding as Text import Data.Unique import Foreign.Marshal.Alloc (mallocBytes) import GHC.IO.Exception +import qualified Network.HPACK as HPACK import qualified Network.HTTP2.Client as HTTP2 +import qualified Network.HTTP2.Client.Internal as HTTP2 import qualified Network.Socket as NS import qualified OpenSSL.Session as SSL import System.IO.Error @@ -157,9 +159,20 @@ sendRequestWithConnection conn req k = do result :: MVar r <- newEmptyMVar threadKilled :: MVar SomeException <- newEmptyMVar putMVar (connectionActionMVar conn) (SendRequest (Request req (putMVar result <=< k) threadKilled)) - race (takeMVar result) (takeMVar threadKilled) >>= \case + + let waitResult = takeMVar result + waitError = takeMVar threadKilled + waitDeath = do + res <- waitCatch (backgroundThread conn) + pure $ + case res of + Left e -> e + Right _ -> SomeException ConnectionAlreadyClosed + + race waitResult (race waitError waitDeath) >>= \case Left r -> pure r - Right (SomeException e) -> throw e + Right (Left e) -> throwIO e + Right (Right e) -> throwIO e -- | Make an HTTP2 request, if it is the first time the 'Http2Manager' sees this -- target, it creates the connection and keeps it around for @@ -338,12 +351,13 @@ startPersistentHTTP2ConnectionWithHook ctx (tlsEnabled, hostname, port) cl remov } -- Sends error to requests which show up too late, i.e. after the -- connection is already closed - tooLateNotifier e = forever $ do - takeMVar sendReqMVar >>= \case - SendRequest Request {..} -> do - -- No need to get stuck here - void $ tryPutMVar exceptionMVar (SomeException e) - CloseConnection -> pure () + tooLateNotifier e = do + forever $ do + takeMVar sendReqMVar >>= \case + SendRequest Request {..} -> do + -- No need to get stuck here + void $ tryPutMVar exceptionMVar (SomeException e) + CloseConnection -> pure () -- Sends errors to the request threads when an error occurs cleanupThreadsWith (SomeException e) = do @@ -360,6 +374,7 @@ startPersistentHTTP2ConnectionWithHook ctx (tlsEnabled, hostname, port) cl remov -- 1 second is hopefully enough to ensure that this thread is seen -- as finished. void $ async $ race_ (tooLateNotifier e) (threadDelay 1_000_000) + throwIO e hostnameForTLS = if removeTrailingDot @@ -466,11 +481,17 @@ data ConnectionAlreadyClosed = ConnectionAlreadyClosed instance Exception ConnectionAlreadyClosed -bufsize :: Int +bufsize :: HPACK.BufferSize bufsize = 4096 allocHTTP2Config :: Transport -> IO HTTP2.Config -allocHTTP2Config (InsecureTransport sock) = HTTP2.allocSimpleConfig sock bufsize +allocHTTP2Config (InsecureTransport sock) = do + res <- try $ HTTP2.allocSimpleConfig sock bufsize + case res of + Left (e :: SomeException) -> do + throwIO e + Right conf -> do + pure conf allocHTTP2Config (SecureTransport ssl) = do buf <- mallocBytes bufsize timmgr <- System.TimeManager.initialize $ 30 * 1000000 @@ -505,5 +526,6 @@ allocHTTP2Config (SecureTransport ssl) = do HTTP2.confPositionReadMaker = HTTP2.defaultPositionReadMaker, HTTP2.confTimeoutManager = timmgr, HTTP2.confMySockAddr = mysa, - HTTP2.confPeerSockAddr = peersa + HTTP2.confPeerSockAddr = peersa, + HTTP2.confReadNTimeout = False } diff --git a/libs/http2-manager/test/Test/HTTP2/Client/ManagerSpec.hs b/libs/http2-manager/test/Test/HTTP2/Client/ManagerSpec.hs index 352f1c68fb0..15b99661f20 100644 --- a/libs/http2-manager/test/Test/HTTP2/Client/ManagerSpec.hs +++ b/libs/http2-manager/test/Test/HTTP2/Client/ManagerSpec.hs @@ -52,6 +52,7 @@ import qualified Network.HTTP2.Client as Client import qualified Network.HTTP2.Client as HTTP2 import Network.HTTP2.Server (defaultServerConfig) import qualified Network.HTTP2.Server as Server +import qualified Network.HTTP2.Server.Internal as Server import Network.Socket import qualified Network.Socket as NS import qualified OpenSSL.Session as SSL @@ -222,7 +223,7 @@ specTemplate mCtx = do -- to know what happens when we don't wait for the background thread to go -- away. Just deadConn <- Map.lookup (isJust mCtx, "localhost", port) <$> readTVarIO (connections mgr) - wait $ backgroundThread deadConn + void $ waitCatch $ backgroundThread deadConn withTestServerOnPort mCtx port $ \TestServer {..} -> do echoTest mgr (isJust mCtx) port @@ -325,7 +326,8 @@ allocServerConfig (Right ssl) = do Server.confPositionReadMaker = Server.defaultPositionReadMaker, Server.confTimeoutManager = timmgr, Server.confMySockAddr = mysa, - Server.confPeerSockAddr = peersa + Server.confPeerSockAddr = peersa, + HTTP2.confReadNTimeout = False } testServerOnSocket :: Maybe SSL.SSLContext -> Socket -> IORef Int -> IORef (Map Unique (Async ())) -> IO () diff --git a/libs/imports/src/Imports.hs b/libs/imports/src/Imports.hs index a0a1cfcb518..afa40e3c576 100644 --- a/libs/imports/src/Imports.hs +++ b/libs/imports/src/Imports.hs @@ -226,6 +226,7 @@ import Prelude ($!), (^), (^^), + type (~), ) import Prelude qualified as P diff --git a/libs/metrics-wai/metrics-wai.cabal b/libs/metrics-wai/metrics-wai.cabal index 78002563cec..53a354f59e4 100644 --- a/libs/metrics-wai/metrics-wai.cabal +++ b/libs/metrics-wai/metrics-wai.cabal @@ -10,6 +10,14 @@ license: AGPL-3 license-file: LICENSE build-type: Simple +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +flag nix-dev-env + description: In a Nix dev environment. + default: False + manual: True + library exposed-modules: Data.Metrics.Middleware.Prometheus @@ -138,7 +146,9 @@ test-suite unit -threaded -with-rtsopts=-N -Wredundant-constraints -Wunused-packages - build-tool-depends: hspec-discover:hspec-discover + if !flag(nix-dev-env) + build-tool-depends: hspec-discover:hspec-discover + build-depends: base >=4 && <5 , containers diff --git a/libs/metrics-wai/src/Data/Metrics/Servant.hs b/libs/metrics-wai/src/Data/Metrics/Servant.hs index 3d3313ee2a0..c0a791fa499 100644 --- a/libs/metrics-wai/src/Data/Metrics/Servant.hs +++ b/libs/metrics-wai/src/Data/Metrics/Servant.hs @@ -28,8 +28,7 @@ module Data.Metrics.Servant where import Data.ByteString.UTF8 qualified as UTF8 import Data.Id -import Data.Metrics.Types -import Data.Metrics.Types qualified as Metrics +import Data.Metrics.Types as Metrics import Data.Proxy import Data.Text.Encoding import Data.Text.Encoding.Error @@ -37,8 +36,7 @@ import Data.Tree import GHC.TypeLits import Imports import Network.Wai qualified as Wai -import Network.Wai.Middleware.Prometheus -import Network.Wai.Middleware.Prometheus qualified as Promth +import Network.Wai.Middleware.Prometheus as Promth import Servant.API import Servant.Multipart diff --git a/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal b/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal index 87dc102d657..cc89c97c7c1 100644 --- a/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal +++ b/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal @@ -10,6 +10,14 @@ copyright: (c) 2020 Wire Swiss GmbH license: AGPL-3 build-type: Simple +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +flag nix-dev-env + description: In a Nix dev environment. + default: False + manual: True + library -- cabal-fmt: expand src exposed-modules: @@ -164,7 +172,9 @@ test-suite spec -Wno-redundant-constraints -Werror -threaded -rtsopts -with-rtsopts=-N -Wredundant-constraints -Wunused-packages - build-tool-depends: hspec-discover:hspec-discover + if !flag(nix-dev-env) + build-tool-depends: hspec-discover:hspec-discover + build-depends: base , containers diff --git a/libs/polysemy-wire-zoo/src/Polysemy/TinyLog.hs b/libs/polysemy-wire-zoo/src/Polysemy/TinyLog.hs index 4a72cb8f8c9..b40f904e7f9 100644 --- a/libs/polysemy-wire-zoo/src/Polysemy/TinyLog.hs +++ b/libs/polysemy-wire-zoo/src/Polysemy/TinyLog.hs @@ -53,8 +53,7 @@ logErrors showError msg action = Polysemy.Error.catch action $ \e -> do logAndIgnoreErrors :: forall e r. - ( Member TinyLog r - ) => + (Member TinyLog r) => (e -> Text) -> Text -> Sem (Error e ': r) () -> diff --git a/libs/polysemy-wire-zoo/src/Wire/Sem/Now/Spec.hs b/libs/polysemy-wire-zoo/src/Wire/Sem/Now/Spec.hs index 84b789646ba..9e7b65ee139 100644 --- a/libs/polysemy-wire-zoo/src/Wire/Sem/Now/Spec.hs +++ b/libs/polysemy-wire-zoo/src/Wire/Sem/Now/Spec.hs @@ -65,5 +65,4 @@ prop_nowNow = pure $ simpleLaw (liftA2 (<=) E.get E.get) - ( pure True - ) + (pure True) diff --git a/libs/saml2-web-sso/default.nix b/libs/saml2-web-sso/default.nix index cf699cf0738..e4d714b7145 100644 --- a/libs/saml2-web-sso/default.nix +++ b/libs/saml2-web-sso/default.nix @@ -72,6 +72,7 @@ , uuid , wai , wai-extra +, wai-utilities , warp , word8 , xml-conduit @@ -150,6 +151,7 @@ mkDerivation { uuid wai wai-extra + wai-utilities warp word8 xml-conduit diff --git a/libs/saml2-web-sso/saml2-web-sso.cabal b/libs/saml2-web-sso/saml2-web-sso.cabal index 4d05fc179b5..da09daed9ed 100644 --- a/libs/saml2-web-sso/saml2-web-sso.cabal +++ b/libs/saml2-web-sso/saml2-web-sso.cabal @@ -145,6 +145,7 @@ library , uuid >=1.3.13 , wai >=3.2.2.1 , wai-extra >=3.0.28 + , wai-utilities , warp >=3.2.28 , word8 >=0.1.3 , xml-conduit >=1.8.0.1 diff --git a/libs/saml2-web-sso/src/SAML2/WebSSO/Cookie.hs b/libs/saml2-web-sso/src/SAML2/WebSSO/Cookie.hs index 2afd63ddf29..d11cdc59b19 100644 --- a/libs/saml2-web-sso/src/SAML2/WebSSO/Cookie.hs +++ b/libs/saml2-web-sso/src/SAML2/WebSSO/Cookie.hs @@ -52,7 +52,7 @@ headerValueToCookie txt = do let cookie = parseSetCookie $ cs txt case ["missing cookie name" | setCookieName cookie == ""] <> [ cs $ "wrong cookie name: got " <> setCookieName cookie <> ", expected " <> cookieName (Proxy @name) - | setCookieName cookie /= cookieName (Proxy @name) + | setCookieName cookie /= cookieName (Proxy @name) ] <> ["missing cookie value" | setCookieValue cookie == ""] of errs@(_ : _) -> throwError $ ST.intercalate ", " errs diff --git a/libs/saml2-web-sso/src/SAML2/WebSSO/Test/Util/Misc.hs b/libs/saml2-web-sso/src/SAML2/WebSSO/Test/Util/Misc.hs index 7760f80d2b1..9d511b92c42 100644 --- a/libs/saml2-web-sso/src/SAML2/WebSSO/Test/Util/Misc.hs +++ b/libs/saml2-web-sso/src/SAML2/WebSSO/Test/Util/Misc.hs @@ -24,11 +24,13 @@ import Control.Monad import Control.Monad.IO.Class import Data.ByteString.Base64.Lazy qualified as EL (encode) import Data.ByteString.Lazy qualified as LBS +import Data.Char (isSpace) import Data.EitherR import Data.Generics.Uniplate.Data import Data.List (sort) import Data.String import Data.String.Conversions +import Data.Text qualified as T import Data.Text.Lazy.IO qualified as LT import Data.Typeable import Data.UUID as UUID @@ -114,11 +116,11 @@ normalizeDocument = renderAndParse . transformBis [ [transformer $ \(Name nm nmspace _prefix) -> Name nm nmspace Nothing], - [transformer $ \(Element nm attrs nodes) -> Element nm attrs (sort . filter (not . isSignature) $ nodes)] + [transformer $ \(Element nm attrs nodes) -> Element nm attrs (sort . filter (not . isIgnorableNode) $ nodes)] ] renderAndParse :: (HasCallStack) => Document -> Document -renderAndParse doc = case parseText def $ renderText def {rsPretty = True} doc of +renderAndParse doc = case parseText def $ renderText def doc of Right doc' -> doc' bad@(Left _) -> error $ "impossible: " <> show bad @@ -126,6 +128,13 @@ isSignature :: Node -> Bool isSignature (NodeElement (Element name _ _)) = name == "{http://www.w3.org/2000/09/xmldsig#}Signature" isSignature _ = False +isIgnorableNode :: Node -> Bool +isIgnorableNode node = isSignature node || isWhitespaceOnly node + +isWhitespaceOnly :: Node -> Bool +isWhitespaceOnly (NodeContent txt) = T.all isSpace txt +isWhitespaceOnly _ = False + ---------------------------------------------------------------------- -- helpers diff --git a/libs/saml2-web-sso/src/SAML2/WebSSO/Types.hs b/libs/saml2-web-sso/src/SAML2/WebSSO/Types.hs index e5fd8b53165..21c57322871 100644 --- a/libs/saml2-web-sso/src/SAML2/WebSSO/Types.hs +++ b/libs/saml2-web-sso/src/SAML2/WebSSO/Types.hs @@ -441,19 +441,19 @@ mkNameID nid@(UNameIDEntity uri) m1 m2 m3 = do mapM_ throwError $ [ "mkNameID: nameIDNameQ, nameIDSPNameQ, nameIDSPProvidedID MUST be omitted for entity NameIDs." <> show [m1, m2, m3] - | all isJust [m1, m2, m3] + | all isJust [m1, m2, m3] ] <> [ "mkNameID: entity URI too long: " <> show uritxt - | uritxt <- [renderURI uri], - ST.length uritxt > 1024 + | uritxt <- [renderURI uri], + ST.length uritxt > 1024 ] pure $ NameID nid Nothing Nothing Nothing mkNameID nid@(UNameIDPersistent txt) m1 m2 m3 = do mapM_ throwError $ [ "mkNameID: persistent text too long: " <> show (nid, ST.length txt) - | ST.length txt > 1024 + | ST.length txt > 1024 ] pure $ NameID nid m1 m2 m3 mkNameID nid m1 m2 m3 = do diff --git a/libs/saml2-web-sso/src/SAML2/WebSSO/XML.hs b/libs/saml2-web-sso/src/SAML2/WebSSO/XML.hs index 01b26ae093e..3a17412898e 100644 --- a/libs/saml2-web-sso/src/SAML2/WebSSO/XML.hs +++ b/libs/saml2-web-sso/src/SAML2/WebSSO/XML.hs @@ -49,6 +49,7 @@ import Data.Typeable (Proxy (Proxy), Typeable) import Data.X509 qualified as X509 import GHC.Stack import Network.URI qualified as URI +import Network.Wai.Utilities.Exception (displayExceptionNoBacktrace) import SAML2.Bindings.Identifiers qualified as HX import SAML2.Core qualified as HX import SAML2.Metadata.Metadata qualified as HX @@ -83,10 +84,10 @@ defNameSpaces = encode :: forall a. (HasXMLRoot a) => a -> LT encode = Text.XML.renderText settings . renderToDocument where - settings = def {rsNamespaces = nameSpaces (Proxy @a), rsXMLDeclaration = False} + settings = def {rsNamespaces = nameSpaces (Proxy @a), rsXMLDeclaration = True} decode :: forall m a. (HasXMLRoot a, MonadError String m) => LT -> m a -decode = either (throwError . show @SomeException) parseFromDocument . parseText def +decode = either (throwError . displayExceptionNoBacktrace @SomeException) parseFromDocument . parseText def encodeElem :: forall a. (HasXML a) => a -> LT encodeElem = Text.XML.renderText settings . mkDocument' . render @@ -96,7 +97,7 @@ encodeElem = Text.XML.renderText settings . mkDocument' . render mkDocument' bad = error $ "encodeElem: " <> show bad decodeElem :: forall a m. (HasXML a, MonadError String m) => LT -> m a -decodeElem = either (throwError . show @SomeException) parseFromDocument . parseText def +decodeElem = either (throwError . displayExceptionNoBacktrace @SomeException) parseFromDocument . parseText def renderToDocument :: (HasXMLRoot a) => a -> Document renderToDocument = mkDocument . renderRoot @@ -516,7 +517,7 @@ exportConditions conds = HX.conditions = [HX.OneTimeUse | conds ^. condOneTimeUse] <> [ HX.AudienceRestriction (HX.Audience . exportURI <$> hsrs) - | hsrs <- conds ^. condAudienceRestriction + | hsrs <- conds ^. condAudienceRestriction ] } diff --git a/libs/saml2-web-sso/src/Text/XML/DSig.hs b/libs/saml2-web-sso/src/Text/XML/DSig.hs index d42b9f38ac2..e5cbd50c176 100644 --- a/libs/saml2-web-sso/src/Text/XML/DSig.hs +++ b/libs/saml2-web-sso/src/Text/XML/DSig.hs @@ -63,7 +63,6 @@ import Data.Either (isRight) import Data.EitherR (fmapL) import Data.Foldable (toList) import Data.Hourglass qualified as Hourglass -import Data.List (foldl') import Data.List.NonEmpty (NonEmpty ((:|))) import Data.List.NonEmpty qualified as NL import Data.List.NonEmpty qualified as NonEmpty @@ -73,6 +72,7 @@ import Data.UUID as UUID import Data.X509 qualified as X509 import GHC.Stack import Network.URI (URI (..), parseRelativeReference) +import Network.Wai.Utilities.Exception import SAML2.XML qualified as HS hiding (Node, URI) import SAML2.XML.Canonical qualified as HS import SAML2.XML.Signature qualified as HS @@ -146,7 +146,7 @@ parseKeyInfo doVerify (cs @LT @LBS -> lbs) = case HS.xmlToSAML @HS.KeyInfo =<< s -- | Call 'stripWhitespaceDoc' on a rendered bytestring. stripWhitespaceLBS :: (m ~ Either String) => LBS -> m LBS -stripWhitespaceLBS lbs = renderLBS def . stripWhitespace <$> fmapL show (parseLBS def lbs) +stripWhitespaceLBS lbs = renderLBS def . stripWhitespace <$> fmapL displayExceptionNoBacktrace (parseLBS def lbs) renderKeyInfo :: (HasCallStack) => X509.SignedCertificate -> LT renderKeyInfo cert = cs . ourSamlToXML . HS.KeyInfo Nothing $ NonEmpty.singleton (HS.X509Data (NonEmpty.singleton (HS.X509Certificate cert))) @@ -225,8 +225,8 @@ mkSignCredsWithCert mValidSince size = do verify :: forall m. (MonadError String m) => NonEmpty SignCreds -> LBS -> String -> m HXTC.XmlTree verify creds el sid = case unsafePerformIO (try @SomeException $ verifyIO creds el sid) of Right (_, Right xml) -> pure xml - Right (_, Left exc) -> throwError $ show exc - Left exc -> throwError $ show exc + Right (_, Left signErr) -> throwError $ show signErr + Left exc -> throwError $ displayExceptionNoBacktrace exc -- | Convenient wrapper that picks the ID of the root element node and passes it to `verify`. verifyRoot :: forall m. (MonadError String m) => NonEmpty SignCreds -> LBS -> m HXTC.XmlTree @@ -234,7 +234,7 @@ verifyRoot creds el = do signedID <- do XML.Document _ (XML.Element _ attrs _) _ <- either - (throwError . ("Could not parse signed document: " <>) . cs . show) + (throwError . ("Could not parse signed document: " <>) . cs . displayExceptionNoBacktrace) pure (XML.parseLBS XML.def el) maybe @@ -273,7 +273,7 @@ verifySignatureUnenvelopedSigs :: HS.PublicKeys -> String -> HXTC.XmlTree -> IO verifySignatureUnenvelopedSigs pks xid doc = catchAll $ warpResult <$> verifySignature pks xid doc where catchAll :: IO (Either HS.SignatureError a) -> IO (Either HS.SignatureError a) - catchAll = handle $ pure . Left . HS.SignatureVerificationLegacyFailure . Left . (show @SomeException) + catchAll = handle $ pure . Left . HS.SignatureVerificationLegacyFailure . Left . (displayExceptionNoBacktrace @SomeException) warpResult :: Maybe HXTC.XmlTree -> Either HS.SignatureError HXTC.XmlTree warpResult (Just xml) = Right xml @@ -414,7 +414,7 @@ signRootAt sigPos (SignPrivCreds hashAlg (SignPrivKeyRSA keypair)) doc = } ] docCanonic :: SBS <- - either (throwError . show) (pure . cs) . unsafePerformIO . try @SomeException $ + either (throwError . displayExceptionNoBacktrace) (pure . cs) . unsafePerformIO . try @SomeException $ HS.applyTransforms transforms (HXT.mkRoot [] [docInHXT]) let digest :: SBS digest = case hashAlg of @@ -438,7 +438,7 @@ signRootAt sigPos (SignPrivCreds hashAlg (SignPrivKeyRSA keypair)) doc = -- (note that there are two rounds of SHA256 application, hence two mentions of the has alg here) signedInfoSBS :: SBS <- - either (throwError . show) (pure . cs) . unsafePerformIO . try @SomeException $ + either (throwError . displayExceptionNoBacktrace) (pure . cs) . unsafePerformIO . try @SomeException $ HS.applyCanonicalization (HS.signedInfoCanonicalizationMethod signedInfo) Nothing $ HS.samlToDoc signedInfo sigval :: SBS <- diff --git a/libs/saml2-web-sso/test/Test/SAML2/WebSSO/APISpec.hs b/libs/saml2-web-sso/test/Test/SAML2/WebSSO/APISpec.hs index ab886a44bfd..10ef6681629 100644 --- a/libs/saml2-web-sso/test/Test/SAML2/WebSSO/APISpec.hs +++ b/libs/saml2-web-sso/test/Test/SAML2/WebSSO/APISpec.hs @@ -102,7 +102,7 @@ spec = describe "API" $ do <> "
" method=\"post\" accept-charset=\"utf-8\">" <> " " value=\"PHNhbWxwOkxvZ291dFJlcXVlc3QgSUQ9ImQyYjdjMzg4Y2VjMzZmYTdjMzljMjhmZDI5ODY0NGE4IiBJc3N1ZUluc3RhbnQ9IjIwMDQtMDEtMjFUMTk6MDA6NDlaIiBWZXJzaW9uPSIyLjAiIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiPiAgICA8SXNzdWVyIHhtbG5zPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5odHRwczovL0lkZW50aXR5UHJvdmlkZXIuY29tL1NBTUw8L0lzc3Vlcj4gICAgPE5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OnBlcnNpc3RlbnQiIHhtbG5zPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj4wMDVhMDZlMC1hZDgyLTExMGQtYTU1Ni0wMDQwMDViMTNhMmI8L05hbWVJRD4gICAgPHNhbWxwOlNlc3Npb25JbmRleD4xPC9zYW1scDpTZXNzaW9uSW5kZXg+PC9zYW1scDpMb2dvdXRSZXF1ZXN0Pg==\"/>" + <> " value=\"PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbHA6TG9nb3V0UmVxdWVzdCBJRD0iZDJiN2MzODhjZWMzNmZhN2MzOWMyOGZkMjk4NjQ0YTgiIElzc3VlSW5zdGFudD0iMjAwNC0wMS0yMVQxOTowMDo0OVoiIFZlcnNpb249IjIuMCIgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCI+ICAgIDxJc3N1ZXIgeG1sbnM9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPmh0dHBzOi8vSWRlbnRpdHlQcm92aWRlci5jb20vU0FNTDwvSXNzdWVyPiAgICA8TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6cGVyc2lzdGVudCIgeG1sbnM9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPjAwNWEwNmUwLWFkODItMTEwZC1hNTU2LTAwNDAwNWIxM2EyYjwvTmFtZUlEPiAgICA8c2FtbHA6U2Vzc2lvbkluZGV4PjE8L3NhbWxwOlNlc3Npb25JbmRleD48L3NhbWxwOkxvZ291dFJlcXVlc3Q+\"/>" <> "" @@ -110,8 +110,8 @@ spec = describe "API" $ do <> "" <> "" Right (SomeSAMLRequest -> doc) = XML.parseText XML.def have - spuri = [uri|https://ServiceProvider.com/SAML/SLO/Browser/%%|] - Right want `shouldBe` (fmapL show . parseText def . cs $ mimeRender (Proxy @HTML) (FormRedirect spuri doc)) + spuri = [uri|https://ServiceProvider.com/SAML/SLO/Browser/%25%25|] + (fmapL show . parseText def . cs $ mimeRender (Proxy @HTML) (FormRedirect spuri doc)) `shouldBe` Right want describe "simpleVerifyAuthnResponse" $ do let check :: Bool -> Maybe Bool -> Bool -> Spec diff --git a/libs/schema-profunctor/src/Data/Schema.hs b/libs/schema-profunctor/src/Data/Schema.hs index b02c3375f49..3cb88657d5b 100644 --- a/libs/schema-profunctor/src/Data/Schema.hs +++ b/libs/schema-profunctor/src/Data/Schema.hs @@ -30,6 +30,7 @@ module Data.Schema ObjectSchema, ObjectSchemaP, ToSchema (..), + ToObjectSchema (..), Schema (..), mkSchema, schemaDoc, @@ -852,6 +853,9 @@ instance HasOpt NamedSwaggerDoc where class ToSchema a where schema :: ValueSchema NamedSwaggerDoc a +class ToObjectSchema a where + objectSchema :: ObjectSchema SwaggerDoc a + -- Newtype wrappers for deriving via newtype Schema a = Schema {getSchema :: a} diff --git a/libs/types-common-aws/src/Util/Test/SQS.hs b/libs/types-common-aws/src/Util/Test/SQS.hs index f9d015a1631..7e96760b4cf 100644 --- a/libs/types-common-aws/src/Util/Test/SQS.hs +++ b/libs/types-common-aws/src/Util/Test/SQS.hs @@ -161,8 +161,6 @@ parseDeleteMessage url m = do sendEnv :: ( MonadReader AWS.Env m, MonadResource m, - Typeable a, - Typeable (AWS.AWSResponse a), AWS.AWSRequest a ) => a -> diff --git a/libs/types-common-journal/types-common-journal.cabal b/libs/types-common-journal/types-common-journal.cabal index 958378b2367..d4ffce94fbf 100644 --- a/libs/types-common-journal/types-common-journal.cabal +++ b/libs/types-common-journal/types-common-journal.cabal @@ -20,6 +20,14 @@ custom-setup , Cabal >=3.12 , proto-lens-setup +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +flag nix-dev-env + description: In a Nix dev environment. + default: False + manual: True + library exposed-modules: Data.Proto @@ -80,7 +88,10 @@ library -Wredundant-constraints -Wunused-packages ghc-prof-options: -fprof-auto-exported - build-tool-depends: proto-lens-protoc:proto-lens-protoc + + if !flag(nix-dev-env) + build-tool-depends: proto-lens-protoc:proto-lens-protoc + build-depends: base >=4 && <5 , bytestring diff --git a/libs/types-common/src/Data/CommaSeparatedList.hs b/libs/types-common/src/Data/CommaSeparatedList.hs index fa4f07396f2..ec73778e9d8 100644 --- a/libs/types-common/src/Data/CommaSeparatedList.hs +++ b/libs/types-common/src/Data/CommaSeparatedList.hs @@ -29,7 +29,7 @@ import Data.Range (Bounds, Range) import Data.Text qualified as Text import Data.Text.Encoding (decodeUtf8With, encodeUtf8) import Data.Text.Encoding.Error -import Imports +import Imports hiding (List) import Servant (FromHttpApiData (..), ToHttpApiData (toQueryParam)) newtype CommaSeparatedList a = CommaSeparatedList {fromCommaSeparatedList :: [a]} diff --git a/libs/types-common/src/Data/Json/Util.hs b/libs/types-common/src/Data/Json/Util.hs index cb387058b74..96a1f9ae6b7 100644 --- a/libs/types-common/src/Data/Json/Util.hs +++ b/libs/types-common/src/Data/Json/Util.hs @@ -46,6 +46,9 @@ module Data.Json.Util fromBase64TextLenient, fromBase64Text, toBase64Text, + + -- * Other + BigIntString (..), ) where @@ -65,9 +68,11 @@ import Data.ByteString.UTF8 qualified as UTF8 import Data.Fixed import Data.OpenApi qualified as S import Data.Schema +import Data.Text qualified as T import Data.Text qualified as Text import Data.Text.Encoding qualified as Text import Data.Text.Encoding.Error qualified as Text +import Data.Text.Read qualified as TR import Data.Time.Clock import Data.Time.Format (formatTime, parseTimeM) import Data.Time.Lens qualified as TL @@ -90,6 +95,54 @@ infixr 5 # (#) = append {-# INLINE (#) #-} +----------------------------------------------------------------------------- +-- BigIntString + +-- | A wrapper type for arbitrary-precision /signed/ integer values that must +-- be serialized and deserialized as decimal strings in JSON and OpenAPI +-- schemas. +-- +-- This type is intended for situations where numeric values may grow beyond +-- the safe integer range of JavaScript (2^53-1), and therefore cannot be +-- represented as JSON numbers without losing precision. Instead, values are +-- encoded as textual decimal representations, ensuring: +-- +-- * Arbitrary size support – backed by 'Integer', so values never overflow. +-- * Lossless JSON round-trips – encoded as JSON strings rather than numbers. +-- * Type-safe usage in APIs – OpenAPI schema ('ToSchema') reflects a +-- string-based representation with integer parsing rules. +-- +-- The textual form must be a (possibly negative) decimal integer without +-- fractional parts, for example: +-- +-- * @"0"@ +-- * @"42"@ +-- * @"-9000"@ +-- +-- This type is generic and not bound to any specific unit. It can represent +-- large counts of seconds, bytes, IDs, or any other quantity that must +-- remain precise end-to-end across systems with differing numeric +-- capabilities. +newtype BigIntString = BigIntString {unBigIntString :: Integer} + deriving (Show, Eq, Ord, Generic, Arbitrary) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema BigIntString + +instance ToSchema BigIntString where + schema = toText .= (BigIntString <$> bigNatStringSchema) + where + toText :: BigIntString -> Text + toText = T.pack . show . unBigIntString + + bigNatStringSchema :: ValueSchemaP NamedSwaggerDoc Text Integer + bigNatStringSchema = schema `withParser` p + where + p :: Text -> A.Parser Integer + p txt = do + (n, rest) <- either fail pure (TR.signed TR.decimal txt :: Either String (Integer, Text)) + unless (T.null rest) $ + fail "value must be an integer string without decimals" + pure n + ----------------------------------------------------------------------------- -- UTCTimeMillis diff --git a/libs/types-common/src/Data/Range.hs b/libs/types-common/src/Data/Range.hs index c9adb0bc213..ed74973b2e7 100644 --- a/libs/types-common/src/Data/Range.hs +++ b/libs/types-common/src/Data/Range.hs @@ -92,7 +92,7 @@ import Data.Text.Lazy qualified as TL import Data.Type.Bool import Data.Type.Ord import GHC.TypeNats -import Imports +import Imports hiding (List) import Servant (FromHttpApiData (..)) import System.Random (Random) import Test.QuickCheck (Arbitrary (arbitrary, shrink), Gen) diff --git a/libs/types-common/test/Test/Properties.hs b/libs/types-common/test/Test/Properties.hs index e8ce0320de7..1de6d1ac065 100644 --- a/libs/types-common/test/Test/Properties.hs +++ b/libs/types-common/test/Test/Properties.hs @@ -35,6 +35,7 @@ import Data.ByteString.Lazy as L import Data.Domain (Domain) import Data.Handle (Handle) import Data.Id +import Data.Json.Util import Data.Json.Util qualified as Util import Data.Nonce (Nonce) import Data.ProtocolBuffers.Internal @@ -217,6 +218,10 @@ tests = [ testProperty "decode . encode = id" $ \(x :: Nonce) -> bsRoundtrip x, jsonRoundtrip @Nonce + ], + testGroup + "BigIntString" + [ jsonRoundtrip @BigIntString ] ] diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Exception.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Exception.hs new file mode 100644 index 00000000000..45f43e1cefe --- /dev/null +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Exception.hs @@ -0,0 +1,13 @@ +module Network.Wai.Utilities.Exception where + +import Control.Exception +import Imports + +-- | `displayException` with empty `ExceptionContext` +-- +-- Starting with GHC 9.10, exceptions carry a context that contains backtraces. +-- Displaying these is not always desired; e.g. for HTTP response bodies. +displayExceptionNoBacktrace :: (Exception e) => e -> String +displayExceptionNoBacktrace = trim . displayException . toException + where + trim = (dropWhileEnd isSpace) . (dropWhile isSpace) diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Headers.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Headers.hs index 56049d0ecdf..f994dd996cb 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Headers.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Headers.hs @@ -20,7 +20,7 @@ module Network.Wai.Utilities.Headers where import Data.ByteString import Data.ByteString.Conversion (FromByteString (..), ToByteString (..), fromByteString', toByteString') import Data.OpenApi.ParamSchema (ToParamSchema (..)) -import Data.Text as T +import Data.Text as T hiding (show) import Data.Text.Encoding import Data.Text.Encoding.Error import Imports diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs index 2442bd62c37..b5e32d35201 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs @@ -67,7 +67,6 @@ import Data.UUID qualified as UUID import Data.UUID.V4 qualified as UUID import Imports import Network.HTTP.Types -import Network.HTTP2.Internal import Network.Wai import Network.Wai.Handler.Warp import Network.Wai.Handler.Warp.Internal (TimeoutThread) @@ -211,8 +210,6 @@ errorHandlers = _ -> pure . Left $ Wai.mkError status500 "server-error" "Server Error", - -- similar to ThreadKilled, but this is thrown by the HTTP2 client - Handler $ \(x :: KilledByHttp2ThreadManager) -> throwIO x, Handler $ \(_ :: InvalidRequest) -> pure . Left $ Wai.mkError status400 "client-error" "Invalid Request", diff --git a/libs/wai-utilities/wai-utilities.cabal b/libs/wai-utilities/wai-utilities.cabal index 559ce68e460..d600a3c4634 100644 --- a/libs/wai-utilities/wai-utilities.cabal +++ b/libs/wai-utilities/wai-utilities.cabal @@ -11,6 +11,14 @@ license: AGPL-3.0-only license-file: LICENSE build-type: Simple +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +flag nix-dev-env + description: In a Nix dev environment. + default: False + manual: True + common common-all default-language: GHC2021 ghc-options: @@ -65,6 +73,7 @@ library exposed-modules: Network.Wai.Utilities Network.Wai.Utilities.Error + Network.Wai.Utilities.Exception Network.Wai.Utilities.Headers Network.Wai.Utilities.JSONResponse Network.Wai.Utilities.MockServer @@ -105,15 +114,17 @@ library , warp-tls test-suite wai-utilities-tests - import: common-all - type: exitcode-stdio-1.0 - main-is: Main.hs - ghc-options: -threaded -with-rtsopts=-N - hs-source-dirs: test - build-tool-depends: hspec-discover:hspec-discover + import: common-all + type: exitcode-stdio-1.0 + main-is: Main.hs + ghc-options: -threaded -with-rtsopts=-N + hs-source-dirs: test + + if !flag(nix-dev-env) + build-tool-depends: hspec-discover:hspec-discover -- cabal-fmt: expand test -Main - other-modules: Network.Wai.Utilities.ServerSpec + other-modules: Network.Wai.Utilities.ServerSpec build-depends: , bytestring , hspec diff --git a/libs/wire-api-federation/default.nix b/libs/wire-api-federation/default.nix index b39e2071bf0..272b4e73de1 100644 --- a/libs/wire-api-federation/default.nix +++ b/libs/wire-api-federation/default.nix @@ -6,7 +6,6 @@ , aeson , aeson-pretty , amqp -, async , base , bytestring , bytestring-conversion @@ -54,7 +53,6 @@ mkDerivation { libraryHaskellDepends = [ aeson amqp - async base bytestring bytestring-conversion 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 d706935aff7..b4e76e41b85 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Client.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Client.hs @@ -35,7 +35,6 @@ module Wire.API.Federation.Client ) where -import Control.Concurrent.Async import Control.Exception qualified as E import Control.Monad.Catch import Control.Monad.Codensity @@ -65,7 +64,6 @@ import Network.HTTP.Media qualified as HTTP import Network.HTTP.Types qualified as HTTP import Network.HTTP2.Client qualified as HTTP2 import Network.Wai.Utilities.Error qualified as Wai -import OpenSSL.Session qualified as SSL import Servant.Client import Servant.Client.Core import Servant.Types.SourceT @@ -128,27 +126,13 @@ liftCodensity = FederatorClient . lift . lift . lift headersFromTable :: HTTP2.TokenHeaderTable -> [HTTP.Header] headersFromTable (headerList, _) = flip map headerList $ first HTTP2.tokenKey --- This opens a new http2 connection. Using a http2-manager leads to this problem https://wearezeta.atlassian.net/browse/WPB-4787 --- FUTUREWORK: Replace with H2Manager.withHTTP2Request once the bugs are solved. -withNewHttpRequest :: H2Manager.Target -> HTTP2.Request -> (HTTP2.Response -> IO a) -> IO a -withNewHttpRequest target req k = do - ctx <- SSL.context - let cacheLimit = 20 - sslRemoveTrailingDot = False - tcpConnectionTimeout = 30_000_000 - sendReqMVar <- newEmptyMVar - thread <- liftIO . async $ H2Manager.startPersistentHTTP2Connection ctx target cacheLimit sslRemoveTrailingDot tcpConnectionTimeout sendReqMVar - let newConn = H2Manager.HTTP2Conn thread (putMVar sendReqMVar H2Manager.CloseConnection) sendReqMVar - H2Manager.sendRequestWithConnection newConn req \resp -> - k resp `finally` newConn.disconnect - performHTTP2Request :: Http2Manager -> H2Manager.Target -> HTTP2.Request -> IO (Either FederatorClientHTTP2Error (ResponseF Builder)) -performHTTP2Request _mgr target req = try $ do - withNewHttpRequest target req $ consumeStreamingResponseWith $ \resp -> do +performHTTP2Request mgr target req = try $ do + H2Manager.withHTTP2Request mgr target req $ consumeStreamingResponseWith $ \resp -> do b <- fmap (fromRight mempty) . runExceptT @@ -186,8 +170,7 @@ instance (KnownComponent c) => RunClient (FederatorClient c) where { requestHeaders = ( versionHeader, toByteString' - ( versionInt (fromMaybe V0 v) - ) + (versionInt (fromMaybe V0 v)) ) :<| requestHeaders req } @@ -255,7 +238,7 @@ withHTTP2StreamingRequest successfulStatus req handleResponse = do $ Codensity $ \k -> E.catches - (withNewHttpRequest (False, hostname, port) req' (consumeStreamingResponseWith (k . Right))) + (H2Manager.withHTTP2Request (ceHttp2Manager env) (False, hostname, port) req' (consumeStreamingResponseWith (k . Right))) [ E.Handler $ k . Left . FederatorClientHTTP2Error, E.Handler $ k . Left . FederatorClientHTTP2Error . FederatorClientConnectionError, E.Handler $ k . Left . FederatorClientHTTP2Error . FederatorClientHTTP2Exception, 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 f93c1ed6ae6..befd35a88aa 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs @@ -63,8 +63,7 @@ type UnnamedFedEndpointWithMods (mods :: [Type]) path input output = type FedEndpointWithMods (mods :: [Type]) name input output = Named name - ( UnnamedFedEndpointWithMods mods (FedPath name) input output - ) + (UnnamedFedEndpointWithMods mods (FedPath name) input output) type FedEndpoint name input output = FedEndpointWithMods '[] name input output @@ -156,8 +155,7 @@ data StreamPostWithRemoteIp framing (ct :: Type) a -- Server-side simply delegates to the standard 'StreamPost' implementation. instance - ( HasServer (StreamPost framing ct a) context - ) => + (HasServer (StreamPost framing ct a) context) => HasServer (StreamPostWithRemoteIp framing ct a) context where type ServerT (StreamPostWithRemoteIp framing ct a) m = ServerT (StreamPost framing ct a) m 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 b80d873552f..3fee0083d71 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Error.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Error.hs @@ -95,6 +95,7 @@ import Network.HTTP.Types.Status import Network.HTTP.Types.Status qualified as HTTP import Network.HTTP2.Client qualified as HTTP2 import Network.Wai.Utilities.Error qualified as Wai +import Network.Wai.Utilities.Exception import OpenSSL.Session (SomeSSLException) import Servant.Client import Wire.API.Error @@ -227,21 +228,21 @@ federationRemoteHTTP2Error target path = \case ( Wai.mkError unexpectedFederationResponseStatus "federation-http2-error" - (LT.pack (displayException e)) + (LT.pack (displayExceptionNoBacktrace e)) ) & addErrData (FederatorClientTLSException e) -> ( Wai.mkError (HTTP.mkStatus 525 "SSL Handshake Failure") "federation-tls-error" - (LT.pack (displayException e)) + (LT.pack (displayExceptionNoBacktrace e)) ) & addErrData (FederatorClientConnectionError e) -> ( Wai.mkError federatorConnectionRefusedStatus "federation-connection-refused" - (LT.pack (displayException e)) + (LT.pack (displayExceptionNoBacktrace e)) ) & addErrData where @@ -259,12 +260,12 @@ federationClientHTTP2Error (FederatorClientConnectionError e) = Wai.mkError HTTP.status500 "federation-not-available" - (LT.pack (displayException e)) + (LT.pack (displayExceptionNoBacktrace e)) federationClientHTTP2Error e = Wai.mkError HTTP.status500 "federation-local-error" - (LT.pack (displayException e)) + (LT.pack (displayExceptionNoBacktrace e)) federationRemoteResponseError :: SrvTarget -> Text -> HTTP.Status -> LByteString -> Wai.Error federationRemoteResponseError target path status body = @@ -310,7 +311,7 @@ federationServantErrorToWai (UnsupportedContentType mediaType res) = <> LT.pack (show mediaType) ) federationServantErrorToWai (ConnectionError e) = - federationUnavailable . T.pack . displayException $ e + federationUnavailable . T.pack . displayExceptionNoBacktrace $ e federationErrorContentType :: ResponseF a -> LT.Text federationErrorContentType = diff --git a/libs/wire-api-federation/wire-api-federation.cabal b/libs/wire-api-federation/wire-api-federation.cabal index e39c5af0969..a451e2f01c0 100644 --- a/libs/wire-api-federation/wire-api-federation.cabal +++ b/libs/wire-api-federation/wire-api-federation.cabal @@ -13,6 +13,14 @@ license: AGPL-3 license-file: LICENSE build-type: Simple +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +flag nix-dev-env + description: In a Nix dev environment. + default: False + manual: True + library -- cabal-fmt: expand src exposed-modules: @@ -86,7 +94,6 @@ library build-depends: aeson >=2.0.1.0 , amqp - , async , base >=4.6 && <5.0 , bytestring , bytestring-conversion @@ -192,7 +199,9 @@ test-suite spec -threaded -rtsopts -with-rtsopts=-N -Wredundant-constraints -Wunused-packages - build-tool-depends: hspec-discover:hspec-discover + if !flag(nix-dev-env) + build-tool-depends: hspec-discover:hspec-discover + build-depends: aeson >=2.0.1.0 , aeson-pretty diff --git a/libs/wire-api/src/Wire/API/App.hs b/libs/wire-api/src/Wire/API/App.hs index 26f5492aca0..b93894bd0d5 100644 --- a/libs/wire-api/src/Wire/API/App.hs +++ b/libs/wire-api/src/Wire/API/App.hs @@ -18,40 +18,114 @@ module Wire.API.App where import Data.Aeson qualified as A +import Data.HashMap.Strict qualified as HM +import Data.Misc import Data.OpenApi qualified as S +import Data.Range import Data.Schema import Imports import Wire.API.User import Wire.API.User.Auth +import Wire.Arbitrary as Arbitrary data NewApp = NewApp + { app :: GetApp, + password :: PlainTextPassword6 + } + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via Schema NewApp + +data GetApp = GetApp { name :: Name, pict :: Pict, assets :: [Asset], accentId :: ColourId, - meta :: A.Object + meta :: A.Object, + category :: Category, + description :: Range 0 300 Text } - deriving (A.FromJSON, A.ToJSON, S.ToSchema) via Schema NewApp + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via Schema GetApp + +data Category + = Security + | Collaboration + | Productivity + | Automation + | Files + | AI + | Developer + | Support + | Finance + | HR + | Integration + | Compliance + | Other + deriving (Eq, Ord, Show, Read, Generic) + deriving (Arbitrary) via GenericUniform Category + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via (Schema Category) + +categoryTextMapping :: [(Text, Category)] +categoryTextMapping = + [ ("security", Security), + ("collaboration", Collaboration), + ("productivity", Productivity), + ("automation", Automation), + ("files", Files), + ("ai", AI), + ("developer", Developer), + ("support", Support), + ("finance", Finance), + ("hr", HR), + ("integration", Integration), + ("compliance", Compliance), + ("other", Other) + ] + +categoryMap :: HM.HashMap Text Category +categoryMap = HM.fromList categoryTextMapping + +categoryFromText :: Text -> Maybe Category +categoryFromText text' = HM.lookup text' categoryMap + +categoryToText :: Category -> Text +categoryToText = \case + Security -> "security" + Collaboration -> "collaboration" + Productivity -> "productivity" + Automation -> "automation" + Files -> "files" + AI -> "ai" + Developer -> "developer" + Support -> "support" + Finance -> "finance" + HR -> "hr" + Integration -> "integration" + Compliance -> "compliance" + Other -> "other" + +instance ToSchema Category where + schema = + enum @Text "Category" $ + mconcat $ + map (uncurry element) categoryTextMapping instance ToSchema NewApp where schema = object "NewApp" $ NewApp + <$> (.app) .= field "app" schema + <*> (.password) .= field "password" schema + +instance ToSchema GetApp where + schema = + object "GetApp" $ + GetApp <$> (.name) .= field "name" schema <*> (.pict) .= (fromMaybe noPict <$> optField "picture" schema) <*> (.assets) .= (fromMaybe [] <$> optField "assets" (array schema)) <*> (.accentId) .= (fromMaybe defaultAccentId <$> optField "accent_id" schema) <*> (.meta) .= field "metadata" jsonObject - -defNewApp :: Name -> NewApp -defNewApp name = - NewApp - { name, - pict = noPict, - assets = [], - accentId = defaultAccentId, - meta = mempty - } + <*> (.category) .= field "category" schema + <*> (.description) .= field "description" schema data CreatedApp = CreatedApp { user :: User, diff --git a/libs/wire-api/src/Wire/API/Connection.hs b/libs/wire-api/src/Wire/API/Connection.hs index cf56bd1f451..d7843692c5e 100644 --- a/libs/wire-api/src/Wire/API/Connection.hs +++ b/libs/wire-api/src/Wire/API/Connection.hs @@ -50,7 +50,7 @@ import Data.OpenApi qualified as S import Data.Qualified (Qualified (qUnqualified), Remote, deprecatedSchema) import Data.Range import Data.Schema -import Data.Text as Text +import Data.Text as Text hiding (show) import Imports import Servant.API import Wire.API.Routes.MultiTablePaging diff --git a/libs/wire-api/src/Wire/API/Conversation.hs b/libs/wire-api/src/Wire/API/Conversation.hs index 839d31fe012..d10ad9c6f0d 100644 --- a/libs/wire-api/src/Wire/API/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Conversation.hs @@ -954,8 +954,7 @@ newConvSchema v sch = <*> newConvCells .= (fromMaybe False <$> optField "cells" schema) <*> newConvChannelAddPermission .= maybe_ - ( optFieldWithDocModifier "add_permission" (description ?~ "Channel add permission") schema - ) + (optFieldWithDocModifier "add_permission" (description ?~ "Channel add permission") schema) <*> newConvSkipCreator .= ( fromMaybe False <$> optFieldWithDocModifier diff --git a/libs/wire-api/src/Wire/API/Conversation/CellsState.hs b/libs/wire-api/src/Wire/API/Conversation/CellsState.hs index 5ecee42ae83..63b73576ab4 100644 --- a/libs/wire-api/src/Wire/API/Conversation/CellsState.hs +++ b/libs/wire-api/src/Wire/API/Conversation/CellsState.hs @@ -84,4 +84,4 @@ instance HasCellsState CellsState where getCellsState = id instance HasCellsState () where - getCellsState = def + getCellsState _ = def diff --git a/libs/wire-api/src/Wire/API/Conversation/Code.hs b/libs/wire-api/src/Wire/API/Conversation/Code.hs index 51a142ddd09..c80b588c535 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Code.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Code.hs @@ -83,8 +83,7 @@ instance ToSchema JoinConversationByCode where data ConversationCode = ConversationCode { conversationKey :: Code.Key, - conversationCode :: Code.Value, - conversationUri :: Maybe HttpsUrl + conversationCode :: Code.Value } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ConversationCode) @@ -103,13 +102,6 @@ conversationCodeObjectSchema = "code" (description ?~ "Conversation code (random)") schema - <*> conversationUri - .= maybe_ - ( optFieldWithDocModifier - "uri" - (description ?~ "Full URI (containing key/code) to join a conversation") - schema - ) instance ToSchema ConversationCode where schema = @@ -120,6 +112,7 @@ instance ToSchema ConversationCode where data ConversationCodeInfo = ConversationCodeInfo { code :: ConversationCode, + uri :: HttpsUrl, hasPassword :: Bool } deriving stock (Eq, Show, Generic) @@ -133,11 +126,12 @@ instance ToSchema ConversationCodeInfo where (description ?~ "Contains conversation properties to update") $ ConversationCodeInfo <$> (.code) .= conversationCodeObjectSchema + <*> (.uri) .= (fieldWithDocModifier "uri" (description ?~ "Full URI (containing key/code) to join a conversation") schema) <*> (.hasPassword) .= fieldWithDocModifier "has_password" (description ?~ "Whether the conversation has a password") schema mkConversationCodeInfo :: Bool -> Code.Key -> Code.Value -> HttpsUrl -> ConversationCodeInfo mkConversationCodeInfo hasPw k v (HttpsUrl prefix) = - ConversationCodeInfo (ConversationCode k v (Just (HttpsUrl link))) hasPw + ConversationCodeInfo (ConversationCode k v) (HttpsUrl link) hasPw where q = [("key", toByteString' k), ("code", toByteString' v)] link = prefix & (URI.queryL . URI.queryPairsL) .~ q diff --git a/libs/wire-api/src/Wire/API/Error/Galley.hs b/libs/wire-api/src/Wire/API/Error/Galley.hs index d9899c158b5..540f6391cbb 100644 --- a/libs/wire-api/src/Wire/API/Error/Galley.hs +++ b/libs/wire-api/src/Wire/API/Error/Galley.hs @@ -428,6 +428,7 @@ data TeamFeatureError | MLSProtocolMismatch | MLSE2EIDMissingCrlProxy | EmptyDownloadLocation + | InvalidStatusUpdate instance IsSwaggerError TeamFeatureError where -- Do not display in Swagger @@ -460,6 +461,7 @@ instance (Member (Error DynError) r) => ServerEffect (Error TeamFeatureError) r MLSProtocolMismatch -> DynError 400 "mls-protocol-mismatch" "The default protocol needs to be part of the supported protocols" MLSE2EIDMissingCrlProxy -> DynError 400 "mls-e2eid-missing-crl-proxy" "The field 'crlProxy' is missing in the request payload" EmptyDownloadLocation -> DynError 400 "empty-download-location" "Download location cannot be empty" + InvalidStatusUpdate -> DynError 400 "invalid-status-update" "Status must be enabled and lock status must be unlocked" type instance ErrorEffect TeamFeatureError = Error TeamFeatureError diff --git a/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs b/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs index 9b5671791da..3be4adf6c66 100644 --- a/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs +++ b/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs @@ -39,6 +39,7 @@ import Data.Text qualified as T import Data.Text.Encoding qualified as T import Data.UUID qualified as UUID import Imports +import Network.Wai.Utilities.Exception import Web.HttpApiData (FromHttpApiData (parseHeader)) import Wire.API.Conversation hiding (newGroupId) import Wire.API.MLS.Group @@ -116,7 +117,7 @@ getParts = do eDomain <- T.decodeUtf8' . L.toStrict <$> getRemainingLazyByteString - domain <- either (fail . displayException) pure eDomain + domain <- either (fail . displayExceptionNoBacktrace) pure eDomain pure GroupIdParts { convType, @@ -148,7 +149,7 @@ getDomain = do len <- fromIntegral <$> getWord16be domain <- T.decodeUtf8' <$> getByteString len case domain of - Left e -> fail (displayException e) + Left e -> fail (displayExceptionNoBacktrace e) Right d -> pure (Domain d) newGroupId :: ConvType -> Qualified ConvOrSubConvId -> GroupId diff --git a/libs/wire-api/src/Wire/API/Pagination.hs b/libs/wire-api/src/Wire/API/Pagination.hs index beb4dda1e88..8fae686b1fa 100644 --- a/libs/wire-api/src/Wire/API/Pagination.hs +++ b/libs/wire-api/src/Wire/API/Pagination.hs @@ -22,10 +22,12 @@ import Data.Aeson qualified as A import Data.Bifunctor (first) import Data.Default import Data.OpenApi qualified as S +import Data.Proxy import Data.Range as Range import Data.Schema import Data.Text qualified as T import GHC.Generics +import GHC.TypeNats import Imports import Servant.API import Test.QuickCheck.Gen as Arbitrary @@ -69,25 +71,30 @@ instance S.ToParamSchema SortOrder where -------------------------------------------------------------------------------- -newtype PageSize = PageSize {fromPageSize :: Range 1 500 Int32} +newtype PageSize = PageSize {fromPageSize :: Range 0 MaxPageSize Word} deriving (Eq, Show, Ord, Generic) deriving (A.FromJSON, A.ToJSON, S.ToSchema) via Schema PageSize pageSizeToInt :: PageSize -> Int -pageSizeToInt = fromIntegral . pageSizeToInt32 +pageSizeToInt = fromIntegral . pageSizeToWord -pageSizeToInt32 :: PageSize -> Int32 -pageSizeToInt32 = fromRange . fromPageSize +pageSizeToWord :: PageSize -> Word +pageSizeToWord = fromRange . fromPageSize -pageSizeFromInt :: Int32 -> Either Text PageSize +pageSizeFromInt :: Word -> Either Text PageSize pageSizeFromInt = fmap PageSize . first T.pack . Range.checkedEither +type MaxPageSize = 500 :: Nat + +maxPageSize :: (Num i) => i +maxPageSize = fromIntegral $ natVal (Proxy @MaxPageSize) + -- | Doesn't crash on bad input, but shrinks it into the allowed range. -pageSizeFromIntUnsafe :: Int32 -> PageSize -pageSizeFromIntUnsafe = PageSize . unsafeRange . (+ 1) . (`mod` 500) . (+ (-1)) +pageSizeFromIntegralTotal :: (Integral i) => i -> PageSize +pageSizeFromIntegralTotal = PageSize . unsafeRange . fromIntegral . min maxPageSize . max 0 instance Arbitrary PageSize where - arbitrary = pageSizeFromIntUnsafe <$> arbitrary + arbitrary = pageSizeFromIntegralTotal <$> (arbitrary @Int) instance ToSchema PageSize where schema = PageSize <$> fromPageSize .= schema diff --git a/libs/wire-api/src/Wire/API/Provider/Service.hs b/libs/wire-api/src/Wire/API/Provider/Service.hs index 9008dcf742e..979ae525e5c 100644 --- a/libs/wire-api/src/Wire/API/Provider/Service.hs +++ b/libs/wire-api/src/Wire/API/Provider/Service.hs @@ -51,7 +51,7 @@ module Wire.API.Provider.Service ) where -import Cassandra.CQL qualified as Cql +import Cassandra.CQL hiding (Set) import Control.Lens (makeLenses, (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Aeson qualified as A @@ -124,6 +124,40 @@ instance ToSchema ServiceKey where <*> serviceKeySize .= field "size" schema <*> serviceKeyPEM .= field "pem" schema +instance Cql ServiceKey where + ctype = + Tagged + ( UdtColumn + "pubkey" + [ ("typ", IntColumn), + ("size", IntColumn), + ("pem", BlobColumn) + ] + ) + + fromCql (CqlUdt fs) = do + t <- required "typ" + s <- required "size" + p <- required "pem" + case (t :: Int32) of + 0 -> pure $! ServiceKey RsaServiceKey s p + _ -> Left $ "Unexpected service key type: " ++ show t + where + required :: (Cql r) => Text -> Either String r + required f = + maybe + (Left ("ServiceKey: Missing required field '" ++ show f ++ "'")) + fromCql + (lookup f fs) + fromCql _ = Left "service key: udt expected" + + toCql (ServiceKey RsaServiceKey siz pem) = + CqlUdt + [ ("typ", CqlInt 0), + ("size", toCql siz), + ("pem", toCql pem) + ] + -- | Other types may be supported in the future. data ServiceKeyType = RsaServiceKey @@ -189,6 +223,18 @@ instance Arbitrary ServiceKeyPEM where "-----END PUBLIC KEY-----" ] +instance Cql ServiceKeyPEM where + ctype = Tagged BlobColumn + + fromCql (CqlBlob b) = + maybe + (Left "service key pem: malformed key") + pure + (fromByteString' b) + fromCql _ = Left "service key pem: blob expected" + + toCql = CqlBlob . toByteString + -------------------------------------------------------------------------------- -- Service @@ -236,7 +282,7 @@ instance S.ToSchema ServiceToken where tweak = fmap $ S.schema . S.example ?~ tok tok = "sometoken" -deriving instance Cql.Cql ServiceToken +deriving instance Cql ServiceToken -------------------------------------------------------------------------------- -- ServiceProfile 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 95aaaebab1d..b643e7f0052 100644 --- a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs +++ b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs @@ -34,18 +34,29 @@ module Wire.API.Provider.Service.Tag matchAll, match1, match, + Bucket (..), + defBucket, + foldTags, + unfoldTags, + unfoldTagsInto, + diffTags, + nonEmptyTags, + tagToInt, + intToTag, ) where +import Cassandra.CQL hiding (Set) import Control.Lens (Prism', prism) import Data.Aeson (FromJSON, ToJSON (toJSON)) import Data.Attoparsec.ByteString (IResult (..), parse) +import Data.Bits import Data.ByteString (toStrict) import Data.ByteString.Builder qualified as BB import Data.ByteString.Char8 qualified as C8 import Data.ByteString.Conversion import Data.OpenApi qualified as S -import Data.Range (Range, fromRange, rangedSchema) +import Data.Range import Data.Range qualified as Range import Data.Schema import Data.Set qualified as Set @@ -189,6 +200,16 @@ instance S.ToParamSchema ServiceTag where S._schemaEnum = Just (toJSON <$> [(minBound :: ServiceTag) ..]) } +instance Cql ServiceTag where + ctype = Tagged BigIntColumn + + fromCql (CqlBigInt i) = case intToTag i of + Just t -> pure t + Nothing -> Left $ "unexpected service tag: " ++ show i + fromCql _ = Left "service tag: int expected" + + toCql = CqlBigInt . tagToInt + -------------------------------------------------------------------------------- -- Bounded ServiceTag Queries @@ -311,3 +332,109 @@ match1 = matchAll . match match :: ServiceTag -> MatchAll match = MatchAll . Set.singleton + +newtype Bucket = Bucket Int32 + deriving newtype (Cql, Show) + +-- | Bucketing allows us to distribute individual tag bitmasks +-- across multiple wide rows, if it should become necessary. +-- If a tag bitmask it spread across buckets, lookups and deletes +-- on that bitmask will require O(n) queries, where /n/ is the number +-- of buckets for a specific tag, whereas writes can stay at O(1) +-- by always writing to the "newest" bucket. +defBucket :: Bucket +defBucket = Bucket 201608 + +foldTags :: Range 1 3 (Set ServiceTag) -> Int64 +foldTags = foldl' (.|.) 0 . map tagToInt . Set.toList . fromRange + +unfoldTags :: Range 0 3 (Set ServiceTag) -> [Int64] +unfoldTags s = case map tagToInt (Set.toList (fromRange s)) of + [] -> [] + [t] -> [t] + ts@[t, u] -> (t .|. u) : ts + ts@[t, u, v] -> (t .|. u) : (t .|. v) : (u .|. v) : (t .|. u .|. v) : ts + _ -> error "Brig.Provider.DB.Tag: unfoldTags: Too many tags." + +unfoldTagsInto :: Range 1 3 (Set ServiceTag) -> [Int64] -> [Int64] +unfoldTagsInto xs ys = + let xs' = unfoldTags (rcast xs) + in xs' ++ concatMap (\x -> map (.|. x) ys) xs' + +diffTags :: + Range 0 3 (Set ServiceTag) -> + Range 0 3 (Set ServiceTag) -> + Range 0 3 (Set ServiceTag) +diffTags a b = unsafeRange $ Set.difference (fromRange a) (fromRange b) + +nonEmptyTags :: Range m 3 (Set ServiceTag) -> Maybe (Range 1 3 (Set ServiceTag)) +nonEmptyTags r + | Set.null (fromRange r) = Nothing + | otherwise = Just (unsafeRange (fromRange r)) + +tagToInt :: ServiceTag -> Int64 +tagToInt AudioTag = 0b1 +tagToInt BooksTag = 0b10 +tagToInt BusinessTag = 0b100 +tagToInt DesignTag = 0b1000 +tagToInt EducationTag = 0b10000 +tagToInt EntertainmentTag = 0b100000 +tagToInt FinanceTag = 0b1000000 +tagToInt FitnessTag = 0b10000000 +tagToInt FoodDrinkTag = 0b100000000 +tagToInt GamesTag = 0b1000000000 +tagToInt GraphicsTag = 0b10000000000 +tagToInt HealthTag = 0b100000000000 +tagToInt IntegrationTag = 0b1000000000000 +tagToInt LifestyleTag = 0b10000000000000 +tagToInt MediaTag = 0b100000000000000 +tagToInt MedicalTag = 0b1000000000000000 +tagToInt MoviesTag = 0b10000000000000000 +tagToInt MusicTag = 0b100000000000000000 +tagToInt NewsTag = 0b1000000000000000000 +tagToInt PhotographyTag = 0b10000000000000000000 +tagToInt PollTag = 0b100000000000000000000 +tagToInt ProductivityTag = 0b1000000000000000000000 +tagToInt QuizTag = 0b10000000000000000000000 +tagToInt RatingTag = 0b100000000000000000000000 +tagToInt ShoppingTag = 0b1000000000000000000000000 +tagToInt SocialTag = 0b10000000000000000000000000 +tagToInt SportsTag = 0b100000000000000000000000000 +tagToInt TravelTag = 0b1000000000000000000000000000 +tagToInt TutorialTag = 0b10000000000000000000000000000 +tagToInt VideoTag = 0b100000000000000000000000000000 +tagToInt WeatherTag = 0b1000000000000000000000000000000 + +intToTag :: Int64 -> Maybe ServiceTag +intToTag 0b1 = pure AudioTag +intToTag 0b10 = pure BooksTag +intToTag 0b100 = pure BusinessTag +intToTag 0b1000 = pure DesignTag +intToTag 0b10000 = pure EducationTag +intToTag 0b100000 = pure EntertainmentTag +intToTag 0b1000000 = pure FinanceTag +intToTag 0b10000000 = pure FitnessTag +intToTag 0b100000000 = pure FoodDrinkTag +intToTag 0b1000000000 = pure GamesTag +intToTag 0b10000000000 = pure GraphicsTag +intToTag 0b100000000000 = pure HealthTag +intToTag 0b1000000000000 = pure IntegrationTag +intToTag 0b10000000000000 = pure LifestyleTag +intToTag 0b100000000000000 = pure MediaTag +intToTag 0b1000000000000000 = pure MedicalTag +intToTag 0b10000000000000000 = pure MoviesTag +intToTag 0b100000000000000000 = pure MusicTag +intToTag 0b1000000000000000000 = pure NewsTag +intToTag 0b10000000000000000000 = pure PhotographyTag +intToTag 0b100000000000000000000 = pure PollTag +intToTag 0b1000000000000000000000 = pure ProductivityTag +intToTag 0b10000000000000000000000 = pure QuizTag +intToTag 0b100000000000000000000000 = pure RatingTag +intToTag 0b1000000000000000000000000 = pure ShoppingTag +intToTag 0b10000000000000000000000000 = pure SocialTag +intToTag 0b100000000000000000000000000 = pure SportsTag +intToTag 0b1000000000000000000000000000 = pure TravelTag +intToTag 0b10000000000000000000000000000 = pure TutorialTag +intToTag 0b100000000000000000000000000000 = pure VideoTag +intToTag 0b1000000000000000000000000000000 = pure WeatherTag +intToTag _ = Nothing 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 36bb424f2fb..c14a2a2eb3b 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -283,6 +283,9 @@ type GetGroupsInternal = :> "user-groups" :> Capture "tid" TeamId :> QueryParam' [Optional, Strict] "nameContains" Text.Text + :> QueryParam' [Optional, Strict] "managedBy" ManagedBy + :> QueryParam' [Required, Strict] "startIndex" Word + :> QueryParam' [Optional, Strict] "count" Word :> Get '[Servant.JSON] UserGroupPageWithMembers ) diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Enterprise.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Enterprise.hs index ca5e5702970..b8e3e0509a2 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Enterprise.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Enterprise.hs @@ -29,8 +29,7 @@ type InternalAPI = "i" :> InternalAPIBase type InternalAPIBase = Named "status" - ( "status" :> MultiVerb 'GET '[JSON] '[RespondEmpty 200 "OK"] () - ) + ("status" :> MultiVerb 'GET '[JSON] '[RespondEmpty 200 "OK"] ()) :<|> Named "create-verification-token" ( "create-verification-token" diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs index fbc2fd06e23..ea44ce08034 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -94,6 +94,8 @@ type IFeatureAPI = :<|> IFeatureStatusLockStatusPut AppsConfig :<|> IFeatureStatusLockStatusPut SimplifiedUserConnectionRequestQRCodeConfig :<|> IFeatureStatusLockStatusPut StealthUsersConfig + :<|> IFeatureStatusLockStatusPut MeetingsConfig + :<|> IFeatureStatusLockStatusPut MeetingsPremiumConfig -- all feature configs :<|> Named "feature-configs-internal" @@ -117,8 +119,7 @@ type InternalAPI = "i" :> InternalAPIBase type InternalAPIBase = Named "status" - ( "status" :> MultiVerb 'GET '[JSON] '[RespondEmpty 200 "OK"] () - ) + ("status" :> MultiVerb 'GET '[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 @@ -264,6 +265,12 @@ type ITeamsAPIBase = :> ReqBody '[JSON] UserIds :> Get '[JSON] TeamMemberInfoList ) + :<|> Named + "unchecked-select-team-members" + ( "get-by-ids" + :> ReqBody '[JSON] UserIds + :> Post '[JSON] [TeamMember] + ) :<|> Named "unchecked-get-team-member" ( Capture "uid" UserId @@ -304,6 +311,13 @@ type ITeamsAPIBase = :> CanThrow 'NotATeamMember :> MultiVerb1 'GET '[JSON] (RespondEmpty 200 "User is team owner") ) + :<|> Named + "finalize-delete-team" + ( "finalize-delete" + :> ZLocalUser + :> ZOptConn + :> PostNoContent + ) :<|> "search-visibility" :> ( Named "get-search-visibility-internal" (Get '[JSON] TeamSearchVisibilityView) :<|> Named 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 ceac0a84454..313a12fe372 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -1040,8 +1040,7 @@ type UserClientAPI = :> MultiVerb1 'GET '[JSON] - ( VersionedRespond 'V6 200 "List of clients" [Client] - ) + (VersionedRespond 'V6 200 "List of clients" [Client]) ) :<|> Named "list-clients@v7" @@ -1053,8 +1052,7 @@ type UserClientAPI = :> MultiVerb1 'GET '[JSON] - ( VersionedRespond 'V7 200 "List of clients" [Client] - ) + (VersionedRespond 'V7 200 "List of clients" [Client]) ) :<|> Named "list-clients" @@ -1065,8 +1063,7 @@ type UserClientAPI = :> MultiVerb1 'GET '[JSON] - ( Respond 200 "List of clients" [Client] - ) + (Respond 200 "List of clients" [Client]) ) :<|> Named "get-client@v6" @@ -2118,6 +2115,16 @@ type AppsAPI = :> ReqBody '[JSON] NewApp :> Post '[JSON] CreatedApp ) + :<|> Named + "get-app" + ( Summary "Get app" + :> ZLocalUser + :> "teams" + :> Capture "tid" TeamId + :> "apps" + :> Capture "uid" UserId + :> Get '[JSON] GetApp + ) :<|> Named "refresh-app-cookie" ( Summary "Get a new app authentication token" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/DomainVerification.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/DomainVerification.hs index 03022fef789..e7447c46a0a 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/DomainVerification.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/DomainVerification.hs @@ -299,8 +299,7 @@ instance ToSchema DomainRedirectResponseV9 where DomainRedirectResponse <$> (\r -> True <$ guard r.propagateUserExists) .= maybe_ - ( fromMaybe False <$> optField "due_to_existing_account" schema - ) + (fromMaybe False <$> optField "due_to_existing_account" schema) <*> (.redirect) .= domainRedirectSchemaV9 type DomainRedirectResponseV10 = DomainRedirectResponse V10 @@ -319,8 +318,7 @@ instance ToSchema DomainRedirectResponseV10 where DomainRedirectResponse <$> (\r -> True <$ guard r.propagateUserExists) .= maybe_ - ( fromMaybe False <$> optField "due_to_existing_account" schema - ) + (fromMaybe False <$> optField "due_to_existing_account" schema) <*> (.redirect) .= domainRedirectSchema V10 type DomainVerificationChallengeAPI = @@ -459,6 +457,12 @@ type DomainVerificationAPI = :<|> Named "get-domain-registration" ( Summary "Get domain registration configuration by email" + :> Description + "- `due_to_existing_account`: boolean (optional, only present if `domain_redirect` is `no-registration`)\n\ + \- `backend`: object (optional, must be present if `domain_redirect` is `backend`)\n\ + \ - `config_url`: string (required)\n\ + \ - `webapp_url`: string (optional)\n\ + \- `sso_code`: string (optional, must be present if `domain_redirect` is `sso`)" :> From V10 :> CanThrow DomainVerificationInvalidDomain :> "get-domain-registration" 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 d7f5bdf35a5..b326f6e7715 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 @@ -29,6 +29,7 @@ import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.Routes.Public import Wire.API.Routes.Version +import Wire.API.Routes.Versioned import Wire.API.Team.Feature import Wire.API.Team.SearchVisibility (TeamSearchVisibilityView) @@ -66,7 +67,9 @@ type FeatureAPI = :<|> AllDeprecatedFeatureConfigAPI DeprecatedFeatureConfigs :<|> FeatureAPIGet DomainRegistrationConfig :<|> FeatureAPIGetPut ChannelsConfig - :<|> FeatureAPIGetPut CellsConfig + :<|> FeatureAPIGet CellsConfig + :<|> Until 'V14 ::> VersionedFeatureAPIPut "put-CellsConfig@v13" V13 CellsConfig + :<|> From 'V14 ::> FeatureAPIPut CellsConfig :<|> FeatureAPIGet AllowedGlobalOperationsConfig :<|> FeatureAPIGet AssetAuditLogConfig :<|> FeatureAPIGet ConsumableNotificationsConfig @@ -74,6 +77,28 @@ type FeatureAPI = :<|> FeatureAPIGet AppsConfig :<|> FeatureAPIGet SimplifiedUserConnectionRequestQRCodeConfig :<|> FeatureAPIGet StealthUsersConfig + :<|> FeatureAPIGet CellsInternalConfig + :<|> FeatureAPIGetPut MeetingsConfig + :<|> FeatureAPIGetPut MeetingsPremiumConfig + +type VersionedFeatureAPIPut named reqBodyVersion cfg = + Named + named + ( Description (FeatureAPIDesc cfg) + :> ZUser + :> Summary (AppendSymbol "Put config for " (FeatureSymbol cfg)) + :> CanThrow OperationDenied + :> CanThrow 'NotATeamMember + :> CanThrow 'TeamNotFound + :> CanThrow TeamFeatureError + :> CanThrowMany (FeatureErrors cfg) + :> "teams" + :> Capture "tid" TeamId + :> "features" + :> FeatureSymbol cfg + :> VersionedReqBody reqBodyVersion '[Servant.JSON] (Feature cfg) + :> Put '[Servant.JSON] (LockableFeature cfg) + ) type DeprecationNotice1 = "This endpoint is potentially used by the old Android client. It is not used by iOS, team management, or webapp as of June 2022" 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 09020317e3a..642b9dc5225 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs @@ -136,8 +136,8 @@ type APIIDP = :<|> Named "idp-get-raw" (ZOptUser :> IdpGetRaw) :<|> Named "idp-get-all" (ZOptUser :> IdpGetAll) :<|> Named "idp-create@v7" (Until 'V8 :> AuthProtect "TeamAdmin" :> IdpCreate) -- (change is semantic, see handler) - :<|> Named "idp-create" (From 'V8 :> AuthProtect "TeamAdmin" :> IdpCreate) - :<|> Named "idp-update" (ZOptUser :> IdpUpdate) + :<|> Named "idp-create" (From 'V8 :> AuthProtect "TeamAdmin" :> ZHostOpt :> IdpCreate) + :<|> Named "idp-update" (ZOptUser :> ZHostOpt :> IdpUpdate) :<|> Named "idp-delete" (ZOptUser :> IdpDelete) type IdpGetRaw = Capture "id" SAML.IdPId :> "raw" :> Get '[RawXML] RawIdPMetadata @@ -170,8 +170,7 @@ type IdpDelete = type SsoSettingsGet = Named "sso-settings" - ( Get '[JSON] SsoSettings - ) + (Get '[JSON] SsoSettings) sparSPIssuer :: (Functor m, SAML.HasConfig m) => Maybe TeamId -> Maybe Domain -> m (Maybe SAML.Issuer) sparSPIssuer mbtid = (SAML.Issuer <$$>) . sparResponseURI mbtid diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Util.hs b/libs/wire-api/src/Wire/API/Routes/Public/Util.hs index 5517bb6635e..014fc647f34 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Util.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Util.hs @@ -23,6 +23,7 @@ module Wire.API.Routes.Public.Util where import Control.Comonad import Data.Maybe import Data.SOP (I (..), NS (..)) +import Imports import Servant import Servant.OpenApi.Internal.Orphans () import Wire.API.Routes.MultiVerb diff --git a/libs/wire-api/src/Wire/API/Routes/Version.hs b/libs/wire-api/src/Wire/API/Routes/Version.hs index 5c422255089..d54784f796b 100644 --- a/libs/wire-api/src/Wire/API/Routes/Version.hs +++ b/libs/wire-api/src/Wire/API/Routes/Version.hs @@ -98,7 +98,7 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (GenericUniform)) -- and 'developmentVersions' stay in sync; everything else here should keep working without -- change. See also documentation in the *docs* directory. -- https://docs.wire.com/developer/developer/api-versioning.html#version-bump-checklist -data Version = V0 | V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 | V11 | V12 | V13 | V14 +data Version = V0 | V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 | V11 | V12 | V13 | V14 | V15 deriving stock (Eq, Ord, Bounded, Enum, Show, Generic) deriving (FromJSON, ToJSON) via (Schema Version) deriving (Arbitrary) via (GenericUniform Version) @@ -131,6 +131,8 @@ instance RenderableSymbol V13 where renderSymbol = "V13" instance RenderableSymbol V14 where renderSymbol = "V14" +instance RenderableSymbol V15 where renderSymbol = "V15" + -- | Manual enumeration of version integrals (the `` in the constructor `V`). -- -- This is not the same as 'fromEnum': we will remove unsupported versions in the future, @@ -153,6 +155,7 @@ versionInt V11 = 11 versionInt V12 = 12 versionInt V13 = 13 versionInt V14 = 14 +versionInt V15 = 15 supportedVersions :: [Version] supportedVersions = [minBound .. maxBound] @@ -274,7 +277,8 @@ isDevelopmentVersion V10 = False isDevelopmentVersion V11 = False isDevelopmentVersion V12 = False isDevelopmentVersion V13 = False -isDevelopmentVersion V14 = True +isDevelopmentVersion V14 = False +isDevelopmentVersion V15 = True developmentVersions :: [Version] developmentVersions = filter isDevelopmentVersion supportedVersions @@ -299,8 +303,7 @@ instance ToSchema VersionExp where <> tag _VersionExpDevelopment ( unnamed - ( enum @Text "VersionExpDevelopment" (element "development" ()) - ) + (enum @Text "VersionExpDevelopment" (element "development" ())) ) deriving via Schema VersionExp instance (FromJSON VersionExp) diff --git a/libs/wire-api/src/Wire/API/Team.hs b/libs/wire-api/src/Wire/API/Team.hs index 283dbaff55b..61be43b4b0c 100644 --- a/libs/wire-api/src/Wire/API/Team.hs +++ b/libs/wire-api/src/Wire/API/Team.hs @@ -58,6 +58,10 @@ module Wire.API.Team newTeamDeleteData, tdAuthPassword, tdVerificationCode, + + -- * Misc + TeamCreationTime (..), + tcTime, ) where @@ -220,6 +224,13 @@ instance ToSchema Icon where desc = "S3 asset key for an icon image with retention information. Allows special value 'default'." +-- | Cassandra writetime(binding) timestamp +newtype TeamCreationTime = TeamCreationTime + { _tcTime :: Int64 + } + +makeLenses ''TeamCreationTime + data TeamUpdateData = TeamUpdateData { _nameUpdate :: Maybe (Range 1 256 Text), _iconUpdate :: Maybe Icon, diff --git a/libs/wire-api/src/Wire/API/Team/Export.hs b/libs/wire-api/src/Wire/API/Team/Export.hs index c31040c5e42..156f541e5ed 100644 --- a/libs/wire-api/src/Wire/API/Team/Export.hs +++ b/libs/wire-api/src/Wire/API/Team/Export.hs @@ -35,6 +35,7 @@ import Data.Time.Clock import Data.Time.Format import Data.Vector (fromList) import Imports +import Network.Wai.Utilities.Exception import Test.QuickCheck import Wire.API.Team.Role (Role) import Wire.API.User (AccountStatus (..), Name) @@ -150,7 +151,7 @@ parseByteString bstr = parseUTCTime :: ByteString -> Parser UTCTime parseUTCTime b = do - s <- either (fail . displayException) pure $ T.decodeUtf8' b + s <- either (fail . displayExceptionNoBacktrace) pure $ T.decodeUtf8' b parseTimeM False defaultTimeLocale timestampFormat (T.unpack s) parseAccountStatus :: ByteString -> Parser AccountStatus diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 01727938772..5f560380d29 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -85,7 +85,25 @@ module Wire.API.Team.Feature EnforceFileDownloadLocationConfig, LimitedEventFanoutConfig (..), DomainRegistrationConfig (..), - CellsConfig (..), + CellsConfig, + CellsConfigB (..), + CellsProperty (..), + CellsPropertyStatus (..), + CellsUsers (..), + CellsCollaboraStatus (..), + CellsPublicLinks (..), + CellsConfigStorage (..), + CellsRecycle (..), + CellsMetadata (..), + CellsNamespaces (..), + CellsUserMetaTags (..), + CellsInternalConfig, + CellsInternalConfigB (..), + CellsCollabora (..), + CollaboraEdition (..), + CellsBackend (..), + CellsStorage (..), + NumBytes (..), AllowedGlobalOperationsConfig (..), AssetAuditLogConfig (..), ConsumableNotificationsConfig (..), @@ -93,6 +111,8 @@ module Wire.API.Team.Feature AppsConfig (..), SimplifiedUserConnectionRequestQRCodeConfig (..), StealthUsersConfig (..), + MeetingsConfig (..), + MeetingsPremiumConfig (..), Features, AllFeatures, NpProject (..), @@ -126,7 +146,7 @@ import Data.Id import Data.Json.Util import Data.Kind import Data.Map qualified as M -import Data.Misc (HttpsUrl) +import Data.Misc (HttpsUrl (..)) import Data.Monoid hiding (All, First) import Data.OpenApi qualified as S import Data.Proxy @@ -145,13 +165,15 @@ import GHC.TypeLits import Generics.SOP qualified as GSOP import Imports hiding (All, First) import Servant (FromHttpApiData (..), ToHttpApiData (..)) -import Test.QuickCheck (getPrintableString) +import Test.QuickCheck (choose, getPrintableString) import Test.QuickCheck.Arbitrary (arbitrary) import Test.QuickCheck.Gen (suchThat) +import URI.ByteString.QQ qualified as URI.QQ import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite import Wire.API.Routes.Named hiding (unnamed) import Wire.API.Routes.Version +import Wire.API.Routes.Versioned import Wire.API.Team.Feature.Profunctor import Wire.Arbitrary (Arbitrary, GenericUniform (..)) @@ -208,6 +230,10 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -- 'docs/src/understand/team-feature-settings.md') class ( Default cfg, + -- \| Should be "pure MyFeatureConfig" if the feature doesn't have config, + -- which results in a trivial empty schema and the "config" field being + -- omitted/ignored in the JSON encoder / parser. + ToObjectSchema cfg, ToSchema cfg, Default (LockableFeature cfg), KnownSymbol (FeatureSymbol cfg), @@ -219,12 +245,6 @@ class type FeatureSymbol cfg :: Symbol featureSingleton :: FeatureSingleton cfg - objectSchema :: - -- | Should be "pure MyFeatureConfig" if the feature doesn't have config, - -- which results in a trivial empty schema and the "config" field being - -- omitted/ignored in the JSON encoder / parser. - ObjectSchema SwaggerDoc cfg - data FeatureSingleton cfg where FeatureSingletonGuestLinksConfig :: FeatureSingleton GuestLinksConfig FeatureSingletonLegalholdConfig :: FeatureSingleton LegalholdConfig @@ -256,6 +276,9 @@ data FeatureSingleton cfg where FeatureSingletonSimplifiedUserConnectionRequestQRCodeConfig :: FeatureSingleton SimplifiedUserConnectionRequestQRCodeConfig FeatureSingletonAssetAuditLogConfig :: FeatureSingleton AssetAuditLogConfig FeatureSingletonStealthUsersConfig :: FeatureSingleton StealthUsersConfig + FeatureSingletonCellsInternalConfig :: FeatureSingleton CellsInternalConfig + FeatureSingletonMeetingsConfig :: FeatureSingleton MeetingsConfig + FeatureSingletonMeetingsPremiumConfig :: FeatureSingleton MeetingsPremiumConfig type family DeprecatedFeatureName (v :: Version) (cfg :: Type) :: Symbol @@ -434,7 +457,7 @@ forgetLock ws = Feature ws.status ws.config withLockStatus :: LockStatus -> Feature a -> LockableFeature a withLockStatus ls (Feature s c) = LockableFeature s ls c -instance (ToSchema cfg, IsFeatureConfig cfg) => ToSchema (Feature cfg) where +instance (ToSchema cfg, ToObjectSchema cfg) => ToSchema (Feature cfg) where schema = object name $ Feature @@ -448,6 +471,12 @@ instance (ToSchema cfg, IsFeatureConfig cfg) => ToSchema (Feature cfg) where inner = schema @cfg name = fromMaybe "" (getName (schemaDoc inner)) <> ".Feature" +instance + (ToObjectSchema (Versioned v cfg), ToSchema (Versioned v cfg)) => + ToSchema (Versioned v (Feature cfg)) + where + schema = Versioned . fmap unVersioned <$> (fmap Versioned . unVersioned) .= schema @(Feature (Versioned v cfg)) + ---------------------------------------------------------------------- -- FeatureTTL @@ -617,12 +646,13 @@ instance ToSchema GuestLinksConfig where instance Default (LockableFeature GuestLinksConfig) where def = defUnlockedFeature +instance ToObjectSchema GuestLinksConfig where + objectSchema = pure GuestLinksConfig + instance IsFeatureConfig GuestLinksConfig where type FeatureSymbol GuestLinksConfig = "conversationGuestLinks" featureSingleton = FeatureSingletonGuestLinksConfig - objectSchema = pure GuestLinksConfig - -------------------------------------------------------------------------------- -- Legalhold feature @@ -635,10 +665,12 @@ data LegalholdConfig = LegalholdConfig instance Default (LockableFeature LegalholdConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} +instance ToObjectSchema LegalholdConfig where + objectSchema = pure LegalholdConfig + instance IsFeatureConfig LegalholdConfig where type FeatureSymbol LegalholdConfig = "legalhold" featureSingleton = FeatureSingletonLegalholdConfig - objectSchema = pure LegalholdConfig instance ToSchema LegalholdConfig where schema = object "LegalholdConfig" objectSchema @@ -656,10 +688,12 @@ data SSOConfig = SSOConfig instance Default (LockableFeature SSOConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} +instance ToObjectSchema SSOConfig where + objectSchema = pure SSOConfig + instance IsFeatureConfig SSOConfig where type FeatureSymbol SSOConfig = "sso" featureSingleton = FeatureSingletonSSOConfig - objectSchema = pure SSOConfig instance ToSchema SSOConfig where schema = object "SSOConfig" objectSchema @@ -678,10 +712,12 @@ data SearchVisibilityAvailableConfig = SearchVisibilityAvailableConfig instance Default (LockableFeature SearchVisibilityAvailableConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} +instance ToObjectSchema SearchVisibilityAvailableConfig where + objectSchema = pure SearchVisibilityAvailableConfig + instance IsFeatureConfig SearchVisibilityAvailableConfig where type FeatureSymbol SearchVisibilityAvailableConfig = "searchVisibility" featureSingleton = FeatureSingletonSearchVisibilityAvailableConfig - objectSchema = pure SearchVisibilityAvailableConfig instance ToSchema SearchVisibilityAvailableConfig where schema = object "SearchVisibilityAvailableConfig" objectSchema @@ -704,10 +740,12 @@ instance ToSchema ValidateSAMLEmailsConfig where instance Default (LockableFeature ValidateSAMLEmailsConfig) where def = defUnlockedFeature +instance ToObjectSchema ValidateSAMLEmailsConfig where + objectSchema = pure ValidateSAMLEmailsConfig + instance IsFeatureConfig ValidateSAMLEmailsConfig where type FeatureSymbol ValidateSAMLEmailsConfig = "validateSAMLemails" featureSingleton = FeatureSingletonValidateSAMLEmailsConfig - objectSchema = pure ValidateSAMLEmailsConfig type instance DeprecatedFeatureName V2 ValidateSAMLEmailsConfig = "validate-saml-emails" @@ -724,10 +762,12 @@ data DigitalSignaturesConfig = DigitalSignaturesConfig instance Default (LockableFeature DigitalSignaturesConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} +instance ToObjectSchema DigitalSignaturesConfig where + objectSchema = pure DigitalSignaturesConfig + instance IsFeatureConfig DigitalSignaturesConfig where type FeatureSymbol DigitalSignaturesConfig = "digitalSignatures" featureSingleton = FeatureSingletonDigitalSignaturesConfig - objectSchema = pure DigitalSignaturesConfig type instance DeprecatedFeatureName V2 DigitalSignaturesConfig = "digital-signatures" @@ -794,10 +834,12 @@ instance Default ConferenceCallingConfig where instance Default (LockableFeature ConferenceCallingConfig) where def = defLockedFeature {status = FeatureStatusEnabled} +instance ToObjectSchema ConferenceCallingConfig where + objectSchema = fromMaybe def <$> optField "config" schema + instance IsFeatureConfig ConferenceCallingConfig where type FeatureSymbol ConferenceCallingConfig = "conferenceCalling" featureSingleton = FeatureSingletonConferenceCallingConfig - objectSchema = fromMaybe def <$> optField "config" schema instance (OptWithDefault f) => ToSchema (ConferenceCallingConfigB Covered f) where schema = @@ -822,10 +864,12 @@ instance ToSchema SndFactorPasswordChallengeConfig where instance Default (LockableFeature SndFactorPasswordChallengeConfig) where def = defLockedFeature +instance ToObjectSchema SndFactorPasswordChallengeConfig where + objectSchema = pure SndFactorPasswordChallengeConfig + instance IsFeatureConfig SndFactorPasswordChallengeConfig where type FeatureSymbol SndFactorPasswordChallengeConfig = "sndFactorPasswordChallenge" featureSingleton = FeatureSingletonSndFactorPasswordChallengeConfig - objectSchema = pure SndFactorPasswordChallengeConfig -------------------------------------------------------------------------------- -- SearchVisibilityInbound feature @@ -840,10 +884,12 @@ data SearchVisibilityInboundConfig = SearchVisibilityInboundConfig instance Default (LockableFeature SearchVisibilityInboundConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} +instance ToObjectSchema SearchVisibilityInboundConfig where + objectSchema = pure SearchVisibilityInboundConfig + instance IsFeatureConfig SearchVisibilityInboundConfig where type FeatureSymbol SearchVisibilityInboundConfig = "searchVisibilityInbound" featureSingleton = FeatureSingletonSearchVisibilityInboundConfig - objectSchema = pure SearchVisibilityInboundConfig instance ToSchema SearchVisibilityInboundConfig where schema = object "SearchVisibilityInboundConfig" objectSchema @@ -879,11 +925,12 @@ instance ToSchema ClassifiedDomainsConfig where instance Default (LockableFeature ClassifiedDomainsConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} +instance ToObjectSchema ClassifiedDomainsConfig where + objectSchema = field "config" schema + instance IsFeatureConfig ClassifiedDomainsConfig where type FeatureSymbol ClassifiedDomainsConfig = "classifiedDomains" - featureSingleton = FeatureSingletonClassifiedDomainsConfig - objectSchema = field "config" schema ---------------------------------------------------------------------- -- AppLock feature @@ -927,11 +974,12 @@ instance (FieldF f) => ToSchema (AppLockConfigB Covered f) where instance Default (LockableFeature AppLockConfig) where def = defUnlockedFeature +instance ToObjectSchema AppLockConfig where + objectSchema = field "config" schema + instance IsFeatureConfig AppLockConfig where type FeatureSymbol AppLockConfig = "appLock" - featureSingleton = FeatureSingletonAppLockConfig - objectSchema = field "config" schema newtype EnforceAppLock = EnforceAppLock Bool deriving stock (Eq, Show, Ord, Generic) @@ -956,10 +1004,12 @@ instance Default FileSharingConfig where instance Default (LockableFeature FileSharingConfig) where def = defUnlockedFeature +instance ToObjectSchema FileSharingConfig where + objectSchema = pure FileSharingConfig + instance IsFeatureConfig FileSharingConfig where type FeatureSymbol FileSharingConfig = "fileSharing" featureSingleton = FeatureSingletonFileSharingConfig - objectSchema = pure FileSharingConfig instance ToSchema FileSharingConfig where schema = object "FileSharingConfig" objectSchema @@ -1004,10 +1054,12 @@ instance (FieldF f) => ToSchema (SelfDeletingMessagesConfigB Covered f) where instance Default (LockableFeature SelfDeletingMessagesConfig) where def = defUnlockedFeature +instance ToObjectSchema SelfDeletingMessagesConfig where + objectSchema = field "config" schema + instance IsFeatureConfig SelfDeletingMessagesConfig where type FeatureSymbol SelfDeletingMessagesConfig = "selfDeletingMessages" featureSingleton = FeatureSingletonSelfDeletingMessagesConfig - objectSchema = field "config" schema ---------------------------------------------------------------------- -- MLSConfig @@ -1073,10 +1125,12 @@ instance (FieldF f) => ToSchema (MLSConfigB Covered f) where instance Default (LockableFeature MLSConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} +instance ToObjectSchema MLSConfig where + objectSchema = field "config" schema + instance IsFeatureConfig MLSConfig where type FeatureSymbol MLSConfig = "mls" featureSingleton = FeatureSingletonMLSConfig - objectSchema = field "config" schema ---------------------------------------------------------------------- -- ChannelsConfig @@ -1134,10 +1188,12 @@ instance (FieldF f) => ToSchema (ChannelsConfigB Covered f) where instance Default (LockableFeature ChannelsConfig) where def = defLockedFeature +instance ToObjectSchema ChannelsConfig where + objectSchema = field "config" schema + instance IsFeatureConfig ChannelsConfig where type FeatureSymbol ChannelsConfig = "channels" featureSingleton = FeatureSingletonChannelsConfig - objectSchema = field "config" schema ---------------------------------------------------------------------- -- ExposeInvitationURLsToTeamAdminConfig @@ -1151,10 +1207,12 @@ data ExposeInvitationURLsToTeamAdminConfig = ExposeInvitationURLsToTeamAdminConf instance Default (LockableFeature ExposeInvitationURLsToTeamAdminConfig) where def = defLockedFeature +instance ToObjectSchema ExposeInvitationURLsToTeamAdminConfig where + objectSchema = pure ExposeInvitationURLsToTeamAdminConfig + instance IsFeatureConfig ExposeInvitationURLsToTeamAdminConfig where type FeatureSymbol ExposeInvitationURLsToTeamAdminConfig = "exposeInvitationURLsToTeamAdmin" featureSingleton = FeatureSingletonExposeInvitationURLsToTeamAdminConfig - objectSchema = pure ExposeInvitationURLsToTeamAdminConfig instance ToSchema ExposeInvitationURLsToTeamAdminConfig where schema = object "ExposeInvitationURLsToTeamAdminConfig" objectSchema @@ -1173,10 +1231,12 @@ data OutlookCalIntegrationConfig = OutlookCalIntegrationConfig instance Default (LockableFeature OutlookCalIntegrationConfig) where def = defLockedFeature +instance ToObjectSchema OutlookCalIntegrationConfig where + objectSchema = pure OutlookCalIntegrationConfig + instance IsFeatureConfig OutlookCalIntegrationConfig where type FeatureSymbol OutlookCalIntegrationConfig = "outlookCalIntegration" featureSingleton = FeatureSingletonOutlookCalIntegrationConfig - objectSchema = pure OutlookCalIntegrationConfig instance ToSchema OutlookCalIntegrationConfig where schema = object "OutlookCalIntegrationConfig" objectSchema @@ -1273,10 +1333,12 @@ instance (FieldF f) => ToSchema (MlsE2EIdConfigB Covered f) where instance Default (LockableFeature MlsE2EIdConfig) where def = defLockedFeature +instance ToObjectSchema MlsE2EIdConfig where + objectSchema = field "config" schema + instance IsFeatureConfig MlsE2EIdConfig where type FeatureSymbol MlsE2EIdConfig = "mlsE2EId" featureSingleton = FeatureSingletonMlsE2EIdConfig - objectSchema = field "config" schema ---------------------------------------------------------------------- -- MlsMigration @@ -1328,10 +1390,12 @@ instance (NestedMaybe f) => ToSchema (MlsMigrationConfigB Covered f) where instance Default (LockableFeature MlsMigrationConfig) where def = defLockedFeature +instance ToObjectSchema MlsMigrationConfig where + objectSchema = field "config" schema + instance IsFeatureConfig MlsMigrationConfig where type FeatureSymbol MlsMigrationConfig = "mlsMigration" featureSingleton = FeatureSingletonMlsMigrationConfig - objectSchema = field "config" schema ---------------------------------------------------------------------- -- EnforceFileDownloadLocationConfig @@ -1378,10 +1442,12 @@ instance (NestedMaybe f) => ToSchema (EnforceFileDownloadLocationConfigB Covered instance Default (LockableFeature EnforceFileDownloadLocationConfig) where def = defLockedFeature +instance ToObjectSchema EnforceFileDownloadLocationConfig where + objectSchema = field "config" schema + instance IsFeatureConfig EnforceFileDownloadLocationConfig where type FeatureSymbol EnforceFileDownloadLocationConfig = "enforceFileDownloadLocation" featureSingleton = FeatureSingletonEnforceFileDownloadLocationConfig - objectSchema = field "config" schema ---------------------------------------------------------------------- -- Guarding the fanout of events when a team member is deleted. @@ -1400,10 +1466,12 @@ data LimitedEventFanoutConfig = LimitedEventFanoutConfig instance Default (LockableFeature LimitedEventFanoutConfig) where def = defUnlockedFeature +instance ToObjectSchema LimitedEventFanoutConfig where + objectSchema = pure LimitedEventFanoutConfig + instance IsFeatureConfig LimitedEventFanoutConfig where type FeatureSymbol LimitedEventFanoutConfig = "limitedEventFanout" featureSingleton = FeatureSingletonLimitedEventFanoutConfig - objectSchema = pure LimitedEventFanoutConfig instance ToSchema LimitedEventFanoutConfig where schema = object "LimitedEventFanoutConfig" objectSchema @@ -1424,31 +1492,385 @@ instance ToSchema DomainRegistrationConfig where instance Default (LockableFeature DomainRegistrationConfig) where def = defLockedFeature +instance ToObjectSchema DomainRegistrationConfig where + objectSchema = pure DomainRegistrationConfig + instance IsFeatureConfig DomainRegistrationConfig where type FeatureSymbol DomainRegistrationConfig = "domainRegistration" featureSingleton = FeatureSingletonDomainRegistrationConfig - objectSchema = pure DomainRegistrationConfig -------------------------------------------------------------------------------- -- Cells feature --- | This feature does not have a PUT endpoint. See Note [unsettable features]. -data CellsConfig = CellsConfig - deriving (Eq, Show, Generic, GSOP.Generic) - deriving (Arbitrary) via (GenericUniform CellsConfig) - deriving (RenderableSymbol) via (RenderableTypeName CellsConfig) - deriving (Default, ParseDbFeature) via (TrivialFeature CellsConfig) +data CellsPropertyStatus = Enabled | Disabled | Enforced + deriving (Show, Eq, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsPropertyStatus + deriving (Arbitrary) via (GenericUniform CellsPropertyStatus) + +instance ToSchema CellsPropertyStatus where + schema = + enum @Text "CellsPropertyStatus" $ + mconcat + [ element "enabled" Enabled, + element "disabled" Disabled, + element "enforced" Enforced + ] + +data CellsProperty = CellsProperty + { enabled :: Bool, + default_ :: CellsPropertyStatus + } + deriving (Show, Eq, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsProperty + deriving (Arbitrary) via (GenericUniform CellsProperty) + +instance ToSchema CellsProperty where + schema = + object "CellsProperty" $ + CellsProperty + <$> (.enabled) .= field "enabled" schema + <*> (.default_) .= field "default" schema + +data CellsUsers = CellsUsers + { externals :: Bool, + guests :: Bool + } + deriving (Show, Eq, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsUsers + deriving (Arbitrary) via (GenericUniform CellsUsers) + +instance ToSchema CellsUsers where + schema = + object "CellsUsers" $ + CellsUsers + <$> (.externals) .= field "externals" schema + <*> (.guests) .= field "guests" schema + +newtype CellsCollaboraStatus = CellsCollaboraStatus {enabled :: Bool} + deriving (Show, Eq, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsCollaboraStatus + deriving (Arbitrary) via (GenericUniform CellsCollaboraStatus) + +instance ToSchema CellsCollaboraStatus where + schema = + object "CellsCollaboraStatus" $ + CellsCollaboraStatus + <$> (.enabled) .= field "enabled" schema + +data CellsPublicLinks = CellsPublicLinks + { enableFiles :: Bool, + enableFolders :: Bool, + enforcePassword :: Bool, + enforceExpirationMax :: Int64, + enforceExpirationDefault :: Int64 + } + deriving (Show, Eq, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsPublicLinks + deriving (Arbitrary) via (GenericUniform CellsPublicLinks) + +instance ToSchema CellsPublicLinks where + schema = + object "CellsPublicLinks" $ + CellsPublicLinks + <$> enableFiles .= field "enableFiles" schema + <*> enableFolders .= field "enableFolders" schema + <*> enforcePassword .= field "enforcePassword" schema + <*> enforceExpirationMax .= field "enforceExpirationMax" schema + <*> enforceExpirationDefault .= field "enforceExpirationDefault" schema + +data CellsRecycle = CellsRecycle + { autoPurgeDays :: Int, + disable :: Bool, + allowSkip :: Bool + } + deriving (Show, Eq, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsRecycle + deriving (Arbitrary) via (GenericUniform CellsRecycle) + +instance ToSchema CellsRecycle where + schema = + object "CellsRecycle" $ + CellsRecycle + <$> autoPurgeDays .= field "autoPurgeDays" schema + <*> disable .= field "disable" schema + <*> allowSkip .= field "allowSkip" schema + +data CellsConfigStorage = CellsConfigStorage + { perFileQuotaBytes :: NumBytes, + recycle :: CellsRecycle + } + deriving (Eq, Show, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsConfigStorage + deriving (Arbitrary) via (GenericUniform CellsConfigStorage) + +instance ToSchema CellsConfigStorage where + schema = + object "CellsConfigStorage" $ + CellsConfigStorage + <$> perFileQuotaBytes .= field "perFileQuotaBytes" schema + <*> recycle .= field "recycle" schema + +data CellsUserMetaTags = CellsUserMetaTags + { defaultValues :: [Text], + allowFreeValues :: Bool + } + deriving (Eq, Show, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsUserMetaTags + deriving (Arbitrary) via (GenericUniform CellsUserMetaTags) + +instance ToSchema CellsUserMetaTags where + schema = + object "CellsUserMetaTags" $ + CellsUserMetaTags + <$> defaultValues .= field "defaultValues" (array schema) + <*> allowFreeValues .= field "allowFreeValues" schema + +newtype CellsNamespaces = CellsNamespaces {usermetaTags :: CellsUserMetaTags} + deriving (Eq, Show, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsNamespaces + deriving (Arbitrary) via (GenericUniform CellsNamespaces) -instance ToSchema CellsConfig where - schema = object "CellsConfig" objectSchema +instance ToSchema CellsNamespaces where + schema = + object "CellsNamespaces" $ + CellsNamespaces + <$> usermetaTags .= field "usermetaTags" schema + +newtype CellsMetadata = CellsMetadata {namespaces :: CellsNamespaces} + deriving (Eq, Show, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsMetadata + deriving (Arbitrary) via (GenericUniform CellsMetadata) + +instance ToSchema CellsMetadata where + schema = + object "CellsMetadata" $ + CellsMetadata + <$> namespaces .= field "namespaces" schema + +data CellsConfigB t f = CellsConfig + { channels :: Wear t f CellsProperty, + groups :: Wear t f CellsProperty, + one2one :: Wear t f CellsProperty, + users :: Wear t f CellsUsers, + collabora :: Wear t f CellsCollaboraStatus, + publicLinks :: Wear t f CellsPublicLinks, + storage :: Wear t f CellsConfigStorage, + metadata :: Wear t f CellsMetadata + } + deriving (Generic, BareB) + +deriving instance FunctorB (CellsConfigB Covered) + +deriving instance ApplicativeB (CellsConfigB Covered) + +deriving instance TraversableB (CellsConfigB Covered) + +type CellsConfig = CellsConfigB Bare Identity + +deriving instance Eq CellsConfig + +deriving instance Show CellsConfig + +deriving via (RenderableTypeName CellsConfig) instance (RenderableSymbol CellsConfig) + +deriving via (GenericUniform CellsConfig) instance (Arbitrary CellsConfig) + +deriving via (BarbieFeature CellsConfigB) instance (ParseDbFeature CellsConfig) + +deriving via (BarbieFeature CellsConfigB) instance (ToSchema CellsConfig) + +instance Default CellsConfig where + def = + CellsConfig + { channels = CellsProperty {enabled = True, default_ = Enabled}, + groups = CellsProperty {enabled = True, default_ = Enabled}, + one2one = CellsProperty {enabled = True, default_ = Enabled}, + users = CellsUsers {externals = True, guests = False}, + collabora = CellsCollaboraStatus {enabled = False}, + publicLinks = + CellsPublicLinks + { enableFiles = True, + enableFolders = True, + enforcePassword = False, + enforceExpirationMax = 0, + enforceExpirationDefault = 0 + }, + storage = + CellsConfigStorage + { perFileQuotaBytes = NumBytes $ BigIntString 100000000, -- 100MB + recycle = + CellsRecycle + { autoPurgeDays = 30, + disable = False, + allowSkip = False + } + }, + metadata = + CellsMetadata + { namespaces = + CellsNamespaces + { usermetaTags = + CellsUserMetaTags + { defaultValues = [], + allowFreeValues = True + } + } + } + } + +instance (FieldF f) => ToSchema (CellsConfigB Covered f) where + schema = + objectWithDocModifier "CellsConfig" (S.schema . S.example ?~ schemaToJSON (def @CellsConfig)) $ + CellsConfig + <$> channels .= fieldF "channels" schema + <*> groups .= fieldF "groups" schema + <*> one2one .= fieldF "one2one" schema + <*> users .= fieldF "users" schema + <*> (.collabora) .= fieldF "collabora" schema + <*> publicLinks .= fieldF "publicLinks" schema + <*> (.storage) .= fieldF "storage" schema + <*> metadata .= fieldF "metadata" schema + +instance ToSchema (Versioned V13 CellsConfig) where + schema = object "CellsConfigV13" objectSchema + +instance ToObjectSchema (Versioned V13 CellsConfig) where + objectSchema = pure $ Versioned def instance Default (LockableFeature CellsConfig) where def = defLockedFeature +instance ToObjectSchema CellsConfig where + objectSchema = field "config" schema + instance IsFeatureConfig CellsConfig where type FeatureSymbol CellsConfig = "cells" featureSingleton = FeatureSingletonCellsConfig - objectSchema = pure CellsConfig + +---------------------------------------------------------------------- +-- Cells Internal + +data CollaboraEdition = No | Code | Cool + deriving (Show, Eq, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CollaboraEdition + deriving (Arbitrary) via (GenericUniform CollaboraEdition) + +instance ToSchema CollaboraEdition where + schema = + enum @Text "CollaboraEdition" $ + mconcat + [ element "NO" No, + element "CODE" Code, + element "COOL" Cool + ] + +newtype CellsCollabora = CellsCollabora + { edition :: CollaboraEdition + } + deriving (Show, Eq, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsCollabora + deriving (Arbitrary) via (GenericUniform CellsCollabora) + +instance ToSchema CellsCollabora where + schema = + object "CellsCollabora" $ + CellsCollabora + <$> edition .= field "edition" schema + +newtype CellsBackend = CellsBackend + { url :: HttpsUrl + } + deriving (Show, Eq, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsBackend + deriving newtype (Arbitrary) + +instance ToSchema CellsBackend where + schema = object "CellsBackend" $ CellsBackend <$> url .= field "url" schema + +newtype NumBytes = NumBytes {unNumBytes :: BigIntString} + deriving newtype (Show, Eq) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema NumBytes + +instance Arbitrary NumBytes where + arbitrary = NumBytes . BigIntString <$> choose (0 :: Integer, 99999999999999999999999999) + +instance ToSchema NumBytes where + schema = + NumBytes + <$> unNumBytes + .= withParser + schema + ( \n@(BigIntString i) -> do + when (i < 0) $ + fail "numBytes must be non-negative" + pure n + ) + +newtype CellsStorage = CellsStorage + { perUserQuotaBytes :: NumBytes + } + deriving (Show, Eq, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema CellsStorage + deriving newtype (Arbitrary) + +instance ToSchema CellsStorage where + schema = + object "CellsStorage" $ + CellsStorage + <$> perUserQuotaBytes .= field "perUserQuotaBytes" schema + +data CellsInternalConfigB t f = CellsInternalConfig + { backend :: Wear t f CellsBackend, + collabora :: Wear t f CellsCollabora, + storage :: Wear t f CellsStorage + } + deriving (Generic, BareB) + +deriving instance FunctorB (CellsInternalConfigB Covered) + +deriving instance ApplicativeB (CellsInternalConfigB Covered) + +deriving instance TraversableB (CellsInternalConfigB Covered) + +type CellsInternalConfig = CellsInternalConfigB Bare Identity + +deriving instance Eq CellsInternalConfig + +deriving instance Show CellsInternalConfig + +deriving via (RenderableTypeName CellsInternalConfig) instance (RenderableSymbol CellsInternalConfig) + +deriving via (GenericUniform CellsInternalConfig) instance (Arbitrary CellsInternalConfig) + +deriving via (BarbieFeature CellsInternalConfigB) instance (ParseDbFeature CellsInternalConfig) + +deriving via (BarbieFeature CellsInternalConfigB) instance (ToSchema CellsInternalConfig) + +instance Default CellsInternalConfig where + def = + CellsInternalConfig + { backend = CellsBackend $ HttpsUrl [URI.QQ.uri|https://cells-beta.wire.com|], + collabora = CellsCollabora Cool, + storage = CellsStorage $ NumBytes $ BigIntString 1000000000000 -- 1 TB + } + +instance (FieldF f) => ToSchema (CellsInternalConfigB Covered f) where + schema = + object "CellsInternalConfig" $ + CellsInternalConfig + <$> backend .= fieldF "backend" schema + <*> (.collabora) .= fieldF "collabora" schema + <*> (.storage) .= fieldF "storage" schema + +instance Default (LockableFeature CellsInternalConfig) where + def = defUnlockedFeature + +instance ToObjectSchema CellsInternalConfig where + objectSchema = field "config" schema + +instance IsFeatureConfig CellsInternalConfig where + type FeatureSymbol CellsInternalConfig = "cellsInternal" + featureSingleton = FeatureSingletonCellsInternalConfig -------------------------------------------------------------------------------- -- Allowed Global Operations feature @@ -1478,11 +1900,13 @@ instance ToSchema AllowedGlobalOperationsConfig where instance Default (LockableFeature AllowedGlobalOperationsConfig) where def = defLockedFeature {status = FeatureStatusEnabled} +instance ToObjectSchema AllowedGlobalOperationsConfig where + objectSchema = field "config" schema + instance IsFeatureConfig AllowedGlobalOperationsConfig where type FeatureSymbol AllowedGlobalOperationsConfig = "allowedGlobalOperations" featureSingleton = FeatureSingletonAllowedGlobalOperationsConfig - objectSchema = field "config" schema -------------------------------------------------------------------------------- -- Asset Audit Log feature @@ -1508,10 +1932,12 @@ instance Default AssetAuditLogConfig where instance Default (LockableFeature AssetAuditLogConfig) where def = defLockedFeature +instance ToObjectSchema AssetAuditLogConfig where + objectSchema = pure AssetAuditLogConfig + instance IsFeatureConfig AssetAuditLogConfig where type FeatureSymbol AssetAuditLogConfig = "assetAuditLog" featureSingleton = FeatureSingletonAssetAuditLogConfig - objectSchema = pure AssetAuditLogConfig -------------------------------------------------------------------------------- -- ConsumableNotifications feature @@ -1529,10 +1955,12 @@ instance ToSchema ConsumableNotificationsConfig where instance Default (LockableFeature ConsumableNotificationsConfig) where def = defLockedFeature +instance ToObjectSchema ConsumableNotificationsConfig where + objectSchema = pure ConsumableNotificationsConfig + instance IsFeatureConfig ConsumableNotificationsConfig where type FeatureSymbol ConsumableNotificationsConfig = "consumableNotifications" featureSingleton = FeatureSingletonConsumableNotificationsConfig - objectSchema = pure ConsumableNotificationsConfig -------------------------------------------------------------------------------- -- Chat Bubbles Feature @@ -1549,12 +1977,13 @@ instance ToSchema ChatBubblesConfig where instance Default (LockableFeature ChatBubblesConfig) where def = defLockedFeature +instance ToObjectSchema ChatBubblesConfig where + objectSchema = pure ChatBubblesConfig + instance IsFeatureConfig ChatBubblesConfig where type FeatureSymbol ChatBubblesConfig = "chatBubbles" featureSingleton = FeatureSingletonChatBubblesConfig - objectSchema = pure ChatBubblesConfig - ------------------------------------------------------------------------------- -- Apps Feature @@ -1570,12 +1999,13 @@ instance ToSchema AppsConfig where instance Default (LockableFeature AppsConfig) where def = defLockedFeature +instance ToObjectSchema AppsConfig where + objectSchema = pure AppsConfig + instance IsFeatureConfig AppsConfig where type FeatureSymbol AppsConfig = "apps" featureSingleton = FeatureSingletonAppsConfig - objectSchema = pure AppsConfig - -------------------------------------------------------------------------------- -- "Simplified User Connection Request QR Code" Feature -- @@ -1594,12 +2024,13 @@ instance ToSchema SimplifiedUserConnectionRequestQRCodeConfig where instance Default (LockableFeature SimplifiedUserConnectionRequestQRCodeConfig) where def = defUnlockedFeature +instance ToObjectSchema SimplifiedUserConnectionRequestQRCodeConfig where + objectSchema = pure SimplifiedUserConnectionRequestQRCodeConfig + instance IsFeatureConfig SimplifiedUserConnectionRequestQRCodeConfig where type FeatureSymbol SimplifiedUserConnectionRequestQRCodeConfig = "simplifiedUserConnectionRequestQRCode" featureSingleton = FeatureSingletonSimplifiedUserConnectionRequestQRCodeConfig - objectSchema = pure SimplifiedUserConnectionRequestQRCodeConfig - -------------------------------------------------------------------------------- -- Stealth Users @@ -1615,11 +2046,62 @@ instance ToSchema StealthUsersConfig where instance Default (LockableFeature StealthUsersConfig) where def = defLockedFeature +instance ToObjectSchema StealthUsersConfig where + objectSchema = pure StealthUsersConfig + instance IsFeatureConfig StealthUsersConfig where type FeatureSymbol StealthUsersConfig = "stealthUsers" featureSingleton = FeatureSingletonStealthUsersConfig - objectSchema = pure StealthUsersConfig +-------------------------------------------------------------------------------- +-- Meetings Feature +-- +-- Controls whether meetings functionality is available. When enabled, users can +-- create and manage meetings. When disabled, meetings endpoints are not accessible. + +data MeetingsConfig = MeetingsConfig + deriving (Eq, Show, Generic, GSOP.Generic) + deriving (Arbitrary) via (GenericUniform MeetingsConfig) + deriving (RenderableSymbol) via (RenderableTypeName MeetingsConfig) + deriving (ParseDbFeature, Default) via TrivialFeature MeetingsConfig + +instance ToSchema MeetingsConfig where + schema = object "MeetingsConfig" objectSchema + +instance Default (LockableFeature MeetingsConfig) where + def = defUnlockedFeature + +instance IsFeatureConfig MeetingsConfig where + type FeatureSymbol MeetingsConfig = "meetings" + featureSingleton = FeatureSingletonMeetingsConfig + +instance ToObjectSchema MeetingsConfig where + objectSchema = pure MeetingsConfig + +-------------------------------------------------------------------------------- +-- MeetingPremium Feature +-- +-- Indicates whether a team has premium meetings features. When enabled, meetings +-- created by team members are not marked as trial. When disabled, meetings are trial. + +data MeetingsPremiumConfig = MeetingsPremiumConfig + deriving (Eq, Show, Generic, GSOP.Generic) + deriving (Arbitrary) via (GenericUniform MeetingsPremiumConfig) + deriving (RenderableSymbol) via (RenderableTypeName MeetingsPremiumConfig) + deriving (ParseDbFeature, Default) via TrivialFeature MeetingsPremiumConfig + +instance ToSchema MeetingsPremiumConfig where + schema = object "MeetingsPremiumConfig" objectSchema + +instance Default (LockableFeature MeetingsPremiumConfig) where + def = defLockedFeature + +instance IsFeatureConfig MeetingsPremiumConfig where + type FeatureSymbol MeetingsPremiumConfig = "meetingsPremium" + featureSingleton = FeatureSingletonMeetingsPremiumConfig + +instance ToObjectSchema MeetingsPremiumConfig where + objectSchema = pure MeetingsPremiumConfig --------------------------------------------------------------------------------- -- FeatureStatus @@ -1713,7 +2195,10 @@ type Features = AppsConfig, SimplifiedUserConnectionRequestQRCodeConfig, AssetAuditLogConfig, - StealthUsersConfig + StealthUsersConfig, + CellsInternalConfig, + MeetingsConfig, + MeetingsPremiumConfig ] -- | list of available features as a record @@ -1895,7 +2380,7 @@ mkAllFeatures m = isCellsFeatureConfigEvent :: forall cfg. (IsFeatureConfig cfg) => Bool isCellsFeatureConfigEvent = - featureName @cfg == featureName @CellsConfig + featureName @cfg `elem` [featureName @CellsConfig, featureName @CellsInternalConfig] -------------------------------------------------------------------------------- -- Team Feature Migration diff --git a/libs/wire-api/src/Wire/API/Team/Member.hs b/libs/wire-api/src/Wire/API/Team/Member.hs index 73a0c5c0dd4..2d75ce2e04d 100644 --- a/libs/wire-api/src/Wire/API/Team/Member.hs +++ b/libs/wire-api/src/Wire/API/Team/Member.hs @@ -70,6 +70,7 @@ module Wire.API.Team.Member rolePermissions, IsPerm (..), HiddenPerm (..), + mkSingleTeamMembersPage, ) where @@ -237,6 +238,15 @@ newtype TeamMembersPage = TeamMembersPage {unTeamMembersPage :: TeamMembersPage' deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON, S.ToSchema) via (Schema TeamMembersPage) +mkSingleTeamMembersPage :: [TeamMemberOptPerms] -> TeamMembersPage +mkSingleTeamMembersPage members = + TeamMembersPage + MultiTablePage + { mtpResults = members, + mtpHasMore = False, + mtpPagingState = MultiTablePagingState TeamMembersTable Nothing + } + instance ToSchema TeamMembersPage where schema = object "TeamMembersPage" $ @@ -511,12 +521,12 @@ permissionsRole (Permissions p p') = permsRole perms = listToMaybe [ role - | role <- [minBound ..], - -- if a there is a role that is strictly less permissive than the perms set that - -- we encounter, we downgrade. this shouldn't happen in real life, but it has - -- happened to very old users on a staging environment, where a user (probably) - -- was create before the current publicly visible permissions had been stabilized. - rolePerms role `Set.isSubsetOf` perms + | role <- [minBound ..], + -- if a there is a role that is strictly less permissive than the perms set that + -- we encounter, we downgrade. this shouldn't happen in real life, but it has + -- happened to very old users on a staging environment, where a user (probably) + -- was create before the current publicly visible permissions had been stabilized. + rolePerms role `Set.isSubsetOf` perms ] -- | Internal function for 'rolePermissions'. (It works iff the two sets in 'Permissions' are diff --git a/libs/wire-api/src/Wire/API/User/Activation.hs b/libs/wire-api/src/Wire/API/User/Activation.hs index 84c993870b4..e6ee35fd35c 100644 --- a/libs/wire-api/src/Wire/API/User/Activation.hs +++ b/libs/wire-api/src/Wire/API/User/Activation.hs @@ -205,8 +205,7 @@ instance ToSchema SendActivationCode where .= maybe_ ( optFieldWithDocModifier "locale" - ( description ?~ "Locale to use for the activation code template." - ) + (description ?~ "Locale to use for the activation code template.") schema ) where 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 1a8c1edf9b5..e2f8eb04087 100644 --- a/libs/wire-api/src/Wire/API/User/Client/Prekey.hs +++ b/libs/wire-api/src/Wire/API/User/Client/Prekey.hs @@ -32,6 +32,7 @@ module Wire.API.User.Client.Prekey ) where +import Cassandra (ColumnType (IntColumn), Cql (ctype, fromCql, toCql), Tagged (..), Value (CqlInt)) import Crypto.Hash (SHA256, hash) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Bits @@ -48,6 +49,12 @@ newtype PrekeyId = PrekeyId {keyId :: Word16} deriving stock (Eq, Ord, Show, Generic) deriving newtype (ToJSON, FromJSON, Arbitrary, S.ToSchema, ToSchema) +instance Cql PrekeyId where + ctype = Tagged IntColumn + toCql = CqlInt . fromIntegral . keyId + fromCql (CqlInt i) = pure $ PrekeyId (fromIntegral i) + fromCql _ = Left "PrekeyId: Int expected" + -------------------------------------------------------------------------------- -- Prekey diff --git a/libs/wire-api/src/Wire/API/User/Identity.hs b/libs/wire-api/src/Wire/API/User/Identity.hs index 841f6dc025d..97a3c503e59 100644 --- a/libs/wire-api/src/Wire/API/User/Identity.hs +++ b/libs/wire-api/src/Wire/API/User/Identity.hs @@ -203,16 +203,16 @@ instance ToJSON UserSSOId where instance FromJSON UserSSOId where parseJSON = A.withObject "UserSSOId" $ \obj -> do - mtenant <- lenientlyParseSAMLIssuer =<< (obj A..:? "tenant") - msubject <- lenientlyParseSAMLNameID =<< (obj A..:? "subject") + mtenant <- mapM lenientlyParseSAMLIssuer =<< (obj A..:? "tenant") + msubject <- mapM lenientlyParseSAMLNameID =<< (obj A..:? "subject") meid <- obj A..:? "scim_external_id" case (mtenant, msubject, meid) of (Just tenant, Just subject, Nothing) -> pure $ UserSSOId (SAML.UserRef tenant subject) (Nothing, Nothing, Just eid) -> pure $ UserScimExternalId eid _ -> fail "either need tenant and subject, or scim_external_id, but not both" -lenientlyParseSAMLIssuer :: Maybe LText -> A.Parser (Maybe SAML.Issuer) -lenientlyParseSAMLIssuer mbtxt = forM mbtxt $ \txt -> do +lenientlyParseSAMLIssuer :: LText -> A.Parser SAML.Issuer +lenientlyParseSAMLIssuer txt = do let asxml :: Either String SAML.Issuer asxml = SAML.decodeElem txt @@ -222,13 +222,12 @@ lenientlyParseSAMLIssuer mbtxt = forM mbtxt $ \txt -> do URI.parseURI URI.laxURIParserOptions (encodeUtf8 . LT.toStrict $ txt) err :: String - err = "lenientlyParseSAMLIssuer: " <> show (asxml, asurl, mbtxt) + err = "lenientlyParseSAMLIssuer: " <> show (asxml, asurl, txt) maybe (fail err) pure $ hush asxml <|> hush asurl -lenientlyParseSAMLNameID :: Maybe LText -> A.Parser (Maybe SAML.NameID) -lenientlyParseSAMLNameID Nothing = pure Nothing -lenientlyParseSAMLNameID (Just txt) = do +lenientlyParseSAMLNameID :: LText -> A.Parser SAML.NameID +lenientlyParseSAMLNameID txt = do let asxml :: Either String SAML.NameID asxml = SAML.decodeElem txt @@ -249,7 +248,7 @@ lenientlyParseSAMLNameID (Just txt) = do maybe (fail err) - (pure . Just) + pure (hush asxml <|> hush asemail <|> hush astxt) -- | For testing. Create a sample 'SAML.UserRef' value with random seeds to make 'Issuer' and diff --git a/libs/wire-api/src/Wire/API/User/IdentityProvider.hs b/libs/wire-api/src/Wire/API/User/IdentityProvider.hs index 3241f924475..b6ffbd71299 100644 --- a/libs/wire-api/src/Wire/API/User/IdentityProvider.hs +++ b/libs/wire-api/src/Wire/API/User/IdentityProvider.hs @@ -60,6 +60,7 @@ import SAML2.WebSSO qualified as SAML import SAML2.WebSSO.Test.Arbitrary () import SAML2.WebSSO.Types.TH (deriveJSONOptions) import Servant.API as Servant hiding (MkLink, URI (..)) +import Wire.API.Routes.Public (ZHostValue) import Wire.API.User.Orphans (samlSchemaOptions) import Wire.API.Util.Aeson (defaultOptsDropChar) import Wire.Arbitrary (Arbitrary, GenericUniform (GenericUniform)) @@ -82,7 +83,8 @@ data WireIdP = WireIdP -- with the @"replaces"@ query parameter, and it is used to decide whether users not -- existing on this IdP can be auto-provisioned (if 'isJust', they can't). _replacedBy :: Maybe SAML.IdPId, - _handle :: IdPHandle + _handle :: IdPHandle, + _domain :: Maybe ZHostValue } deriving (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform WireIdP) diff --git a/libs/wire-api/src/Wire/API/User/Scim.hs b/libs/wire-api/src/Wire/API/User/Scim.hs index 1c83919a1b3..174a4ef6b1b 100644 --- a/libs/wire-api/src/Wire/API/User/Scim.hs +++ b/libs/wire-api/src/Wire/API/User/Scim.hs @@ -425,7 +425,7 @@ data CreateScimToken = CreateScimToken verificationCode :: !(Maybe Code.Value), -- | Optional name for the token name :: Maybe Text, - -- | Optional IdP that created users will "belong" to + -- | Optional SAML IdP that created users will "belong" to; if Nothing, do not use SAML. idp :: Maybe SAML.IdPId } deriving (Eq, Show, Generic) diff --git a/libs/wire-api/src/Wire/API/User/Search.hs b/libs/wire-api/src/Wire/API/User/Search.hs index 3eb2cda6d58..bddf084994a 100644 --- a/libs/wire-api/src/Wire/API/User/Search.hs +++ b/libs/wire-api/src/Wire/API/User/Search.hs @@ -54,6 +54,7 @@ import Data.Schema import Data.Text qualified as T import Data.Text.Ascii (AsciiBase64Url, toText, validateBase64Url) import Data.Text.Encoding qualified as TE +import Data.Typeable (typeRep) import Imports import Servant.API (FromHttpApiData, ToHttpApiData (..)) import Web.Internal.HttpApiData (parseQueryParam) @@ -110,9 +111,9 @@ instance Traversable SearchResult where newResults <- traverse f (searchResults r) pure $ r {searchResults = newResults} -instance (ToSchema a) => ToSchema (SearchResult a) where +instance (ToSchema a, Typeable a) => ToSchema (SearchResult a) where schema = - object "SearchResult" $ + object ("SearchResult_" <> T.pack (show $ typeRep $ Proxy @a)) $ SearchResult <$> searchFound .= fieldWithDocModifier "found" (S.description ?~ "Total number of hits") schema <*> searchReturned .= fieldWithDocModifier "returned" (S.description ?~ "Total number of hits returned") schema diff --git a/libs/wire-api/src/Wire/API/UserEvent.hs b/libs/wire-api/src/Wire/API/UserEvent.hs index a2bf6e4a39c..89e6a600af3 100644 --- a/libs/wire-api/src/Wire/API/UserEvent.hs +++ b/libs/wire-api/src/Wire/API/UserEvent.hs @@ -319,13 +319,11 @@ eventObjectSchema = EventTypeUserLegalholdEnabled -> tag _UserEvent - ( tag _UserLegalHoldEnabled (field "id" schema) - ) + (tag _UserLegalHoldEnabled (field "id" schema)) EventTypeUserLegalholdDisabled -> tag _UserEvent - ( tag _UserLegalHoldDisabled (field "id" schema) - ) + (tag _UserLegalHoldDisabled (field "id" schema)) EventTypeUserLegalholdRequested -> tag _UserEvent diff --git a/libs/wire-api/src/Wire/API/UserGroup/Pagination.hs b/libs/wire-api/src/Wire/API/UserGroup/Pagination.hs index 92e309515e3..9e8ed1bb333 100644 --- a/libs/wire-api/src/Wire/API/UserGroup/Pagination.hs +++ b/libs/wire-api/src/Wire/API/UserGroup/Pagination.hs @@ -18,14 +18,48 @@ module Wire.API.UserGroup.Pagination where import Data.Aeson qualified as A +import Data.Default import Data.OpenApi qualified as S import Data.Schema +import Data.Time.Clock import GHC.Generics import Imports import Wire.API.Pagination +import Wire.API.User.Profile import Wire.API.UserGroup import Wire.Arbitrary as Arbitrary +-- | Request for a paginated list of user groups. +-- +-- (This is not technically API, but since it is used by several +-- different wire-subsystems we've moved it here anyway.) +data UserGroupPageRequest = UserGroupPageRequest + { searchString :: Maybe Text, + managedByFilter :: Maybe ManagedBy, + paginationState :: PaginationState UserGroupId, + sortOrder :: SortOrder, + pageSize :: PageSize, + includeMemberCount :: Bool, + includeChannels :: Bool + } + +instance Default UserGroupPageRequest where + def = + UserGroupPageRequest + { searchString = Nothing, + managedByFilter = Nothing, + paginationState = PaginationSortByCreatedAt Nothing, -- default sort by is 'createdAt', with no state + sortOrder = Desc, + pageSize = def, -- default is 15 + includeMemberCount = True, + includeChannels = False + } + +data PaginationState a + = PaginationSortByName (Maybe (Text, a)) + | PaginationSortByCreatedAt (Maybe (UTCTime, a)) + | PaginationOffset Word + -- | User group without members type UserGroupPage = UserGroupPage_ UserGroupMeta @@ -34,8 +68,6 @@ type UserGroupPageWithMembers = UserGroupPage_ UserGroup -- * User group pages --- - -- | User group pages with different types of user groups. data UserGroupPage_ a = UserGroupPage { page :: [a], diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ConversationCode_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ConversationCode_user.hs index 27c4ed16765..787d45325d6 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ConversationCode_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ConversationCode_user.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE OverloadedLists #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -20,61 +18,21 @@ module Test.Wire.API.Golden.Generated.ConversationCode_user where import Data.Code (Key (Key, asciiKey), Value (Value, asciiValue)) -import Data.Coerce (coerce) -import Data.Misc (HttpsUrl (HttpsUrl)) import Data.Range (unsafeRange) import Data.Text.Ascii (AsciiChars (validate)) -import Imports (Maybe (Just, Nothing), fromRight, undefined) -import URI.ByteString - ( Authority - ( Authority, - authorityHost, - authorityPort, - authorityUserInfo - ), - Host (Host, hostBS), - Query (Query, queryPairs), - Scheme (Scheme, schemeBS), - URIRef - ( URI, - uriAuthority, - uriFragment, - uriPath, - uriQuery, - uriScheme - ), - ) +import Imports (fromRight, undefined) import Wire.API.Conversation.Code (ConversationCode (..)) testObject_ConversationCode_user_1 :: ConversationCode testObject_ConversationCode_user_1 = ConversationCode { conversationKey = Key {asciiKey = unsafeRange (fromRight undefined (validate "M0vnbETaqAgL8tv5Z1_x"))}, - conversationCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "sEG3Y60tIsd9P3"))}, - conversationUri = - Just - ( coerce - URI - { uriScheme = Scheme {schemeBS = "https"}, - uriAuthority = - Just - ( Authority - { authorityUserInfo = Nothing, - authorityHost = Host {hostBS = "example.com"}, - authorityPort = Nothing - } - ), - uriPath = "", - uriQuery = Query {queryPairs = []}, - uriFragment = Nothing - } - ) + conversationCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "sEG3Y60tIsd9P3"))} } testObject_ConversationCode_user_2 :: ConversationCode testObject_ConversationCode_user_2 = ConversationCode { conversationKey = Key {asciiKey = unsafeRange (fromRight undefined (validate "NEN=eLUWHXclTp=_2Nap"))}, - conversationCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "lLz-9vR8ENum0kI-xWJs"))}, - conversationUri = Nothing + conversationCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "lLz-9vR8ENum0kI-xWJs"))} } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_conversation.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_conversation.hs index 39de347d68b..4b1476f5666 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_conversation.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_conversation.hs @@ -83,10 +83,10 @@ testObject_Event_conversation_3 = ( ConversationCodeInfo ( ConversationCode { conversationKey = Key {asciiKey = unsafeRange "CRdONS7988O2QdyndJs1"}, - conversationCode = Value {asciiValue = unsafeRange "7d6713"}, - conversationUri = Just $ HttpsUrl (URI {uriScheme = Scheme {schemeBS = "https"}, uriAuthority = Just (Authority {authorityUserInfo = Nothing, authorityHost = Host {hostBS = "example.com"}, authorityPort = Nothing}), uriPath = "", uriQuery = Query {queryPairs = []}, uriFragment = Nothing}) + conversationCode = Value {asciiValue = unsafeRange "7d6713"} } ) + (HttpsUrl (URI {uriScheme = Scheme {schemeBS = "https"}, uriAuthority = Just (Authority {authorityUserInfo = Nothing, authorityHost = Host {hostBS = "example.com"}, authorityPort = Nothing}), uriPath = "", uriQuery = Query {queryPairs = []}, uriFragment = Nothing})) False ), evtTeam = Nothing diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_user.hs index 36e84bf3489..e315b846ceb 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_user.hs @@ -21,13 +21,14 @@ module Test.Wire.API.Golden.Generated.Event_user where import Data.Domain import Data.Id -import Data.Misc (Milliseconds (Ms, ms)) +import Data.Misc (HttpsUrl (..), Milliseconds (Ms, ms)) import Data.Qualified import Data.Range (unsafeRange) import Data.Set qualified as Set import Data.Text.Ascii (validate) import Data.UUID qualified as UUID (fromString) import Imports +import URI.ByteString import Wire.API.Conversation import Wire.API.Conversation.CellsState import Wire.API.Conversation.Code @@ -308,14 +309,31 @@ testObject_Event_user_14 = Nothing (EdConvCodeUpdate cc) where + testURI :: HttpsUrl + testURI = + HttpsUrl + URI + { uriScheme = Scheme {schemeBS = "https"}, + uriAuthority = + Just + ( Authority + { authorityUserInfo = Nothing, + authorityHost = Host {hostBS = "example.com"}, + authorityPort = Nothing + } + ), + uriPath = "", + uriQuery = Query {queryPairs = []}, + uriFragment = Nothing + } cc = ConversationCodeInfo ( ConversationCode { conversationKey = Key {asciiKey = unsafeRange (fromRight undefined (validate "NEN=eLUWHXclTp=_2Nap"))}, - conversationCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "lLz-9vR8ENum0kI-xWJs"))}, - conversationUri = Nothing + conversationCode = Value {asciiValue = unsafeRange (fromRight undefined (validate "lLz-9vR8ENum0kI-xWJs"))} } ) + testURI False testObject_Event_user_15 :: Event diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUser_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUser_user.hs index 77084a3e648..a910f962d96 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUser_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUser_user.hs @@ -160,8 +160,7 @@ testObject_NewUser_user_8 = newUserIdentity = Just ( EmailIdentity - ( unsafeEmailAddress "some" "example" - ) + (unsafeEmailAddress "some" "example") ), newUserPassword = Just (plainTextPassword8Unsafe "12345678") } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/RTCIceServer_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/RTCIceServer_user.hs index 27d2122fe79..d6a7f3dabce 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/RTCIceServer_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/RTCIceServer_user.hs @@ -62,8 +62,7 @@ testObject_RTCIceServer_user_1 = testObject_RTCIceServer_user_2 :: RTCIceServer testObject_RTCIceServer_user_2 = rtcIceServer - ( NonEmpty.singleton (turnURI SchemeTurn (TurnHostIp (IpAddr (read "108.37.81.160"))) (read "0") (Just TransportTCP)) - ) + (NonEmpty.singleton (turnURI SchemeTurn (TurnHostIp (IpAddr (read "108.37.81.160"))) (read "0") (Just TransportTCP))) ( turnUsername (secondsToNominalDiffTime 3.000000000000) "a8kdffu4" & tuVersion .~ 5 & tuKeyindex .~ 24 diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SimpleMember_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SimpleMember_user.hs index ab173f4d92e..4a5b21046be 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SimpleMember_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SimpleMember_user.hs @@ -31,8 +31,7 @@ testObject_SimpleMember_user_1 = { smQualifiedId = Qualified (Id (fromJust (UUID.fromString "0000003a-0000-0042-0000-007500000037"))) (Domain "faraway.example.com"), smConvRoleName = fromJust - ( parseRoleName "wire_member" - ) + (parseRoleName "wire_member") } testObject_SimpleMember_user_2 :: SimpleMember @@ -41,6 +40,5 @@ testObject_SimpleMember_user_2 = { smQualifiedId = Qualified (Id (fromJust (UUID.fromString "0000003a-0000-0042-0000-007500000037"))) (Domain "faraway.example.com"), smConvRoleName = fromJust - ( parseRoleName "wire_admin" - ) + (parseRoleName "wire_admin") } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs index f62e490ed21..7cb4d14144b 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs @@ -41,6 +41,7 @@ import Test.Wire.API.Golden.Manual.FederationRestriction import Test.Wire.API.Golden.Manual.FederationStatus import Test.Wire.API.Golden.Manual.GetPaginatedConversationIds import Test.Wire.API.Golden.Manual.GroupId +import Test.Wire.API.Golden.Manual.IdP import Test.Wire.API.Golden.Manual.InvitationUserView import Test.Wire.API.Golden.Manual.ListConversations import Test.Wire.API.Golden.Manual.ListUsersById @@ -429,5 +430,12 @@ tests = (testObject_DomainRedirectConfig_2, "testObject_DomainRedirectConfig_2.json"), (testObject_DomainRedirectConfig_4, "testObject_DomainRedirectConfig_4.json") ] + ], + testGroup + "IdP" + $ testObjects + [ (testObject_IdP_1, "testObject_IdP_1.json"), + (testObject_IdP_2, "testObject_IdP_2.json"), + (testObject_IdP_3, "testObject_IdP_3.json") ] ] diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/IdP.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/IdP.hs new file mode 100644 index 00000000000..cae4d041bd5 --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/IdP.hs @@ -0,0 +1,252 @@ +module Test.Wire.API.Golden.Manual.IdP where + +import Data.Id +import Data.List.NonEmpty +import Data.UUID +import Imports +import SAML2.WebSSO.Types +import Text.XML.DSig +import URI.ByteString +import Wire.API.User.IdentityProvider + +testObject_IdP_1 :: IdP +testObject_IdP_1 = + IdPConfig + { _idpId = IdPId {fromIdPId = (fromJust . Data.UUID.fromString) "614c0bb0-1b33-98b6-8600-a1b290bbe1d7"}, + _idpMetadata = + IdPMetadata + { _edIssuer = + Issuer + { _fromIssuer = + URI + { uriScheme = Scheme {schemeBS = "https"}, + uriAuthority = + Just + ( Authority + { authorityUserInfo = Nothing, + authorityHost = Host {hostBS = "liisa.kaisa"}, + authorityPort = Nothing + } + ), + uriPath = "/", + uriQuery = Query {queryPairs = []}, + uriFragment = Nothing + } + }, + _edRequestURI = + URI + { uriScheme = Scheme {schemeBS = "https"}, + uriAuthority = + Just + ( Authority + { authorityUserInfo = Nothing, + authorityHost = Host {hostBS = "johanna.leks"}, + authorityPort = Nothing + } + ), + uriPath = "/aytamah", + uriQuery = Query {queryPairs = []}, + uriFragment = Nothing + }, + _edCertAuthnResponse = + either + error + id + (parseKeyInfo False "MIIDBTCCAe2gAwIBAgIQev76BWqjWZxChmKkGqoAfDANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE4MDIxODAwMDAwMFoXDTIwMDIxOTAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMgmGiRfLh6Fdi99XI2VA3XKHStWNRLEy5Aw/gxFxchnh2kPdk/bejFOs2swcx7yUWqxujjCNRsLBcWfaKUlTnrkY7i9x9noZlMrijgJy/Lk+HH5HX24PQCDf+twjnHHxZ9G6/8VLM2e5ZBeZm+t7M3vhuumEHG3UwloLF6cUeuPdW+exnOB1U1fHBIFOG8ns4SSIoq6zw5rdt0CSI6+l7b1DEjVvPLtJF+zyjlJ1Qp7NgBvAwdiPiRMU4l8IRVbuSVKoKYJoyJ4L3eXsjczoBSTJ6VjV2mygz96DC70MY3avccFrk7tCEC6ZlMRBfY1XPLyldT7tsR3EuzjecSa1M8CAwEAAaMhMB8wHQYDVR0OBBYEFIks1srixjpSLXeiR8zES5cTY6fBMA0GCSqGSIb3DQEBCwUAA4IBAQCKthfK4C31DMuDyQZVS3F7+4Evld3hjiwqu2uGDK+qFZas/D/eDunxsFpiwqC01RIMFFN8yvmMjHphLHiBHWxcBTS+tm7AhmAvWMdxO5lzJLS+UWAyPF5ICROe8Mu9iNJiO5JlCo0Wpui9RbB1C81Xhax1gWHK245ESL6k7YWvyMYWrGqr1NuQcNS0B/AIT1Nsj1WY7efMJQOmnMHkPUTWryVZlthijYyd7P2Gz6rY5a81DAFqhDNJl2pGIAE6HWtSzeUEh3jCsHEkoglKfm4VrGJEuXcALmfCMbdfTvtu4rlsaP2hQad+MG/KJFlenoTK34EMHeBPDCpqNDz8UVNk") + :| [] + }, + _idpExtraInfo = + WireIdP + { _team = (either error id . parseIdFromText) "fc5f3bf8-c296-69e7-27fd-70d483740fe4", + _apiVersion = Nothing, + _oldIssuers = + [ Issuer + { _fromIssuer = + URI + { uriScheme = Scheme {schemeBS = "https"}, + uriAuthority = + Just + ( Authority + { authorityUserInfo = Nothing, + authorityHost = Host {hostBS = "hele.johanna"}, + authorityPort = Nothing + } + ), + uriPath = "/", + uriQuery = Query {queryPairs = []}, + uriFragment = Nothing + } + }, + Issuer + { _fromIssuer = + URI + { uriScheme = Scheme {schemeBS = "https"}, + uriAuthority = + Just + ( Authority + { authorityUserInfo = Nothing, + authorityHost = Host {hostBS = "ulli.jannis"}, + authorityPort = Nothing + } + ), + uriPath = "/", + uriQuery = Query {queryPairs = []}, + uriFragment = Nothing + } + }, + Issuer + { _fromIssuer = + URI + { uriScheme = Scheme {schemeBS = "https"}, + uriAuthority = + Just + ( Authority + { authorityUserInfo = Nothing, + authorityHost = Host {hostBS = "reet.loviise"}, + authorityPort = Nothing + } + ), + uriPath = "/", + uriQuery = Query {queryPairs = []}, + uriFragment = Nothing + } + } + ], + _replacedBy = Just (IdPId {fromIdPId = (fromJust . Data.UUID.fromString) "fc5f3bf8-c296-69e7-27fd-70d483740fe4"}), + _handle = IdPHandle {unIdPHandle = "614c0bb0-1b33-98b6-8600-a1b290bbe1d7"}, + _domain = Just "wire.com" + } + } + +testObject_IdP_2 :: IdP +testObject_IdP_2 = + IdPConfig + { _idpId = IdPId {fromIdPId = (fromJust . Data.UUID.fromString) "614c0bb0-1b33-98b6-8600-a1b290bbe1d7"}, + _idpMetadata = + IdPMetadata + { _edIssuer = + Issuer + { _fromIssuer = + URI + { uriScheme = Scheme {schemeBS = "https"}, + uriAuthority = + Just + ( Authority + { authorityUserInfo = Nothing, + authorityHost = Host {hostBS = "liisa.kaisa"}, + authorityPort = Nothing + } + ), + uriPath = "/", + uriQuery = Query {queryPairs = []}, + uriFragment = Nothing + } + }, + _edRequestURI = + URI + { uriScheme = Scheme {schemeBS = "https"}, + uriAuthority = + Just + ( Authority + { authorityUserInfo = Nothing, + authorityHost = Host {hostBS = "johanna.leks"}, + authorityPort = Nothing + } + ), + uriPath = "/aytamah", + uriQuery = Query {queryPairs = []}, + uriFragment = Nothing + }, + _edCertAuthnResponse = + either + error + id + (parseKeyInfo False "MIIDBTCCAe2gAwIBAgIQev76BWqjWZxChmKkGqoAfDANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE4MDIxODAwMDAwMFoXDTIwMDIxOTAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMgmGiRfLh6Fdi99XI2VA3XKHStWNRLEy5Aw/gxFxchnh2kPdk/bejFOs2swcx7yUWqxujjCNRsLBcWfaKUlTnrkY7i9x9noZlMrijgJy/Lk+HH5HX24PQCDf+twjnHHxZ9G6/8VLM2e5ZBeZm+t7M3vhuumEHG3UwloLF6cUeuPdW+exnOB1U1fHBIFOG8ns4SSIoq6zw5rdt0CSI6+l7b1DEjVvPLtJF+zyjlJ1Qp7NgBvAwdiPiRMU4l8IRVbuSVKoKYJoyJ4L3eXsjczoBSTJ6VjV2mygz96DC70MY3avccFrk7tCEC6ZlMRBfY1XPLyldT7tsR3EuzjecSa1M8CAwEAAaMhMB8wHQYDVR0OBBYEFIks1srixjpSLXeiR8zES5cTY6fBMA0GCSqGSIb3DQEBCwUAA4IBAQCKthfK4C31DMuDyQZVS3F7+4Evld3hjiwqu2uGDK+qFZas/D/eDunxsFpiwqC01RIMFFN8yvmMjHphLHiBHWxcBTS+tm7AhmAvWMdxO5lzJLS+UWAyPF5ICROe8Mu9iNJiO5JlCo0Wpui9RbB1C81Xhax1gWHK245ESL6k7YWvyMYWrGqr1NuQcNS0B/AIT1Nsj1WY7efMJQOmnMHkPUTWryVZlthijYyd7P2Gz6rY5a81DAFqhDNJl2pGIAE6HWtSzeUEh3jCsHEkoglKfm4VrGJEuXcALmfCMbdfTvtu4rlsaP2hQad+MG/KJFlenoTK34EMHeBPDCpqNDz8UVNk") + :| [] + }, + _idpExtraInfo = + WireIdP + { _team = (either error id . parseIdFromText) "fc5f3bf8-c296-69e7-27fd-70d483740fe4", + _apiVersion = Just WireIdPAPIV2, + _oldIssuers = [], + _replacedBy = Nothing, + _handle = IdPHandle {unIdPHandle = "614c0bb0-1b33-98b6-8600-a1b290bbe1d7"}, + _domain = Nothing + } + } + +testObject_IdP_3 :: IdP +testObject_IdP_3 = + let rightOrError :: Either String b -> b + rightOrError = either error id + certs = + rightOrError + <$> (parseKeyInfo False "MIIDBTCCAe2gAwIBAgIQev76BWqjWZxChmKkGqoAfDANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE4MDIxODAwMDAwMFoXDTIwMDIxOTAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMgmGiRfLh6Fdi99XI2VA3XKHStWNRLEy5Aw/gxFxchnh2kPdk/bejFOs2swcx7yUWqxujjCNRsLBcWfaKUlTnrkY7i9x9noZlMrijgJy/Lk+HH5HX24PQCDf+twjnHHxZ9G6/8VLM2e5ZBeZm+t7M3vhuumEHG3UwloLF6cUeuPdW+exnOB1U1fHBIFOG8ns4SSIoq6zw5rdt0CSI6+l7b1DEjVvPLtJF+zyjlJ1Qp7NgBvAwdiPiRMU4l8IRVbuSVKoKYJoyJ4L3eXsjczoBSTJ6VjV2mygz96DC70MY3avccFrk7tCEC6ZlMRBfY1XPLyldT7tsR3EuzjecSa1M8CAwEAAaMhMB8wHQYDVR0OBBYEFIks1srixjpSLXeiR8zES5cTY6fBMA0GCSqGSIb3DQEBCwUAA4IBAQCKthfK4C31DMuDyQZVS3F7+4Evld3hjiwqu2uGDK+qFZas/D/eDunxsFpiwqC01RIMFFN8yvmMjHphLHiBHWxcBTS+tm7AhmAvWMdxO5lzJLS+UWAyPF5ICROe8Mu9iNJiO5JlCo0Wpui9RbB1C81Xhax1gWHK245ESL6k7YWvyMYWrGqr1NuQcNS0B/AIT1Nsj1WY7efMJQOmnMHkPUTWryVZlthijYyd7P2Gz6rY5a81DAFqhDNJl2pGIAE6HWtSzeUEh3jCsHEkoglKfm4VrGJEuXcALmfCMbdfTvtu4rlsaP2hQad+MG/KJFlenoTK34EMHeBPDCpqNDz8UVNk") + :| [(parseKeyInfo False "MIIDpDCCAoygAwIBAgIGAWSx7x1HMA0GCSqGSIb3DQEBCwUAMIGSMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEUMBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi01MDA1MDgxHDAaBgkqhkiG9w0BCQEWDWluZm9Ab2t0YS5jb20wHhcNMTgwNzE5MDk0NTM1WhcNMjgwNzE5MDk0NjM0WjCBkjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNVBAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtNTAwNTA4MRwwGgYJKoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhUaQm/3dgPws1A5IjFK9ZQpj170vIqENuDG0tapAzkvk6+9vyhduGckHTeZF3k5MMlW9iix2Eg0qa1oS/Wrq/aBf7+BH6y1MJlQnaKQ3hPL+OFvYzbnrN8k2uC2LivP7Y90dXwtN3P63rA4QSyDPYEMvdKSubUKX/HNsUg4I2PwHmpfWBNgoMkqe0bxQILBv+84L62IYSd6k77XXnCFb/usHpG/gY6sJsTQ2aFl9FuJ51uf67AOj8RzPXstgtUaXbdJI0kAqKIb3j9Zv3mpPCy/GHnyB3PMalvtc1uaz1ZnwO2eliqhwB6/8W6CPutFo1Bhq1glQIX+1OD7906iORwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB0h6vKAywJwH3g0RnocOpBvT42QW57TZ3Wzm9gbg6dQL0rB+NHDx2V0VIh51E3YHL1os9W09MreM7I74D/fX27r1Q3+qAsL1v3CN8WIVh9eYitBCtF7DwZmL2UXTia+GWPrabO14qAztFmTXfqNuCZej7gJd/K2r0KBiZtZ6o58WBREW2F70a6nN6Nk1yjzBkDTJMMf8OMXHphTaalMBXojN9W6HEDpGBE0qY7c70PqvfUEzd8wHWcDxo6+3jajajelk0V4rg7Cqxccr+WwjYtENEuQypNG2mbI52iPZked0QWKy0WzhSMw5wjJ+QDG31vJInAB2769C2KmhPDyNhU")] + in IdPConfig + { _idpId = IdPId {fromIdPId = (fromJust . Data.UUID.fromString) "614c0bb0-1b33-98b6-8600-a1b290bbe1d7"}, + _idpMetadata = + IdPMetadata + { _edIssuer = + Issuer + { _fromIssuer = + URI + { uriScheme = Scheme {schemeBS = "https"}, + uriAuthority = + Just + ( Authority + { authorityUserInfo = Nothing, + authorityHost = Host {hostBS = "liisa.kaisa"}, + authorityPort = Nothing + } + ), + uriPath = "/", + uriQuery = Query {queryPairs = []}, + uriFragment = Nothing + } + }, + _edRequestURI = + URI + { uriScheme = Scheme {schemeBS = "https"}, + uriAuthority = + Just + ( Authority + { authorityUserInfo = Nothing, + authorityHost = Host {hostBS = "johanna.leks"}, + authorityPort = Nothing + } + ), + uriPath = "/aytamah", + uriQuery = Query {queryPairs = []}, + uriFragment = Nothing + }, + _edCertAuthnResponse = certs + }, + _idpExtraInfo = + WireIdP + { _team = (either error id . parseIdFromText) "fc5f3bf8-c296-69e7-27fd-70d483740fe4", + _apiVersion = Just WireIdPAPIV1, + _oldIssuers = + [ Issuer + { _fromIssuer = + URI + { uriScheme = Scheme {schemeBS = "https"}, + uriAuthority = + Just + ( Authority + { authorityUserInfo = Nothing, + authorityHost = Host {hostBS = "hele.johanna"}, + authorityPort = Nothing + } + ), + uriPath = "/", + uriQuery = Query {queryPairs = []}, + uriFragment = Nothing + } + } + ], + _replacedBy = Nothing, + _handle = IdPHandle {unIdPHandle = "614c0bb0-1b33-98b6-8600-a1b290bbe1d7"}, + _domain = Nothing + } + } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/LoginId_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/LoginId_user.hs index c739d7d034b..ead2775ee40 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/LoginId_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/LoginId_user.hs @@ -30,8 +30,7 @@ testObject_LoginId_user_1 = testObject_LoginId_user_2 :: LoginId testObject_LoginId_user_2 = LoginByEmail - ( unsafeEmailAddress "some" "example" - ) + (unsafeEmailAddress "some" "example") testObject_LoginId_user_3 :: LoginId testObject_LoginId_user_3 = LoginByHandle (fromJust (parseHandle "7a8gg3v98")) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/SubConversation.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/SubConversation.hs index eda0dcb51aa..8ae7df9b026 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/SubConversation.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/SubConversation.hs @@ -46,8 +46,7 @@ domain = Domain "golden.example.com" convId :: Qualified ConvId convId = Qualified - ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001")) - ) + (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))) domain testObject_PublicSubConversation_1 :: PublicSubConversation @@ -82,7 +81,6 @@ testObject_PublicSubConversation_2 = user :: Qualified UserId user = Qualified - ( Id (fromJust (UUID.fromString "00000000-0000-0007-0000-000a00000002")) - ) + (Id (fromJust (UUID.fromString "00000000-0000-0007-0000-000a00000002"))) domain cid = ClientId 0xdeadbeef diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs index e24036faaa1..6a6ea459187 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs @@ -160,14 +160,12 @@ testObject_UserEvent_12 = testObject_UserEvent_13 :: Event testObject_UserEvent_13 = PropertyEvent - ( PropertySet (PropertyKey "a") (toJSON (39 :: Int)) - ) + (PropertySet (PropertyKey "a") (toJSON (39 :: Int))) testObject_UserEvent_14 :: Event testObject_UserEvent_14 = PropertyEvent - ( PropertyDeleted (PropertyKey "a") - ) + (PropertyDeleted (PropertyKey "a")) testObject_UserEvent_15 :: Event testObject_UserEvent_15 = PropertyEvent PropertiesCleared diff --git a/libs/wire-api/test/golden/testObject_ConversationCode_user_1.json b/libs/wire-api/test/golden/testObject_ConversationCode_user_1.json index 784e20ffcef..2fd637f6d0b 100644 --- a/libs/wire-api/test/golden/testObject_ConversationCode_user_1.json +++ b/libs/wire-api/test/golden/testObject_ConversationCode_user_1.json @@ -1,5 +1,4 @@ { "code": "sEG3Y60tIsd9P3", - "key": "M0vnbETaqAgL8tv5Z1_x", - "uri": "https://example.com" + "key": "M0vnbETaqAgL8tv5Z1_x" } diff --git a/libs/wire-api/test/golden/testObject_Event_user_14.json b/libs/wire-api/test/golden/testObject_Event_user_14.json index 67fcad8fce1..624aef30e99 100644 --- a/libs/wire-api/test/golden/testObject_Event_user_14.json +++ b/libs/wire-api/test/golden/testObject_Event_user_14.json @@ -3,7 +3,8 @@ "data": { "code": "lLz-9vR8ENum0kI-xWJs", "has_password": false, - "key": "NEN=eLUWHXclTp=_2Nap" + "key": "NEN=eLUWHXclTp=_2Nap", + "uri": "https://example.com" }, "from": "0000114a-0000-7da8-0000-40cb00007fcf", "qualified_conversation": { diff --git a/libs/wire-api/test/golden/testObject_IdP_1.json b/libs/wire-api/test/golden/testObject_IdP_1.json new file mode 100644 index 00000000000..6d5614ceb20 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_IdP_1.json @@ -0,0 +1,22 @@ +{ + "extraInfo": { + "apiVersion": null, + "domain": "wire.com", + "handle": "614c0bb0-1b33-98b6-8600-a1b290bbe1d7", + "oldIssuers": [ + "https://hele.johanna/", + "https://ulli.jannis/", + "https://reet.loviise/" + ], + "replacedBy": "fc5f3bf8-c296-69e7-27fd-70d483740fe4", + "team": "fc5f3bf8-c296-69e7-27fd-70d483740fe4" + }, + "id": "614c0bb0-1b33-98b6-8600-a1b290bbe1d7", + "metadata": { + "certAuthnResponse": [ + "MIIDBTCCAe2gAwIBAgIQev76BWqjWZxChmKkGqoAfDANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE4MDIxODAwMDAwMFoXDTIwMDIxOTAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMgmGiRfLh6Fdi99XI2VA3XKHStWNRLEy5Aw/gxFxchnh2kPdk/bejFOs2swcx7yUWqxujjCNRsLBcWfaKUlTnrkY7i9x9noZlMrijgJy/Lk+HH5HX24PQCDf+twjnHHxZ9G6/8VLM2e5ZBeZm+t7M3vhuumEHG3UwloLF6cUeuPdW+exnOB1U1fHBIFOG8ns4SSIoq6zw5rdt0CSI6+l7b1DEjVvPLtJF+zyjlJ1Qp7NgBvAwdiPiRMU4l8IRVbuSVKoKYJoyJ4L3eXsjczoBSTJ6VjV2mygz96DC70MY3avccFrk7tCEC6ZlMRBfY1XPLyldT7tsR3EuzjecSa1M8CAwEAAaMhMB8wHQYDVR0OBBYEFIks1srixjpSLXeiR8zES5cTY6fBMA0GCSqGSIb3DQEBCwUAA4IBAQCKthfK4C31DMuDyQZVS3F7+4Evld3hjiwqu2uGDK+qFZas/D/eDunxsFpiwqC01RIMFFN8yvmMjHphLHiBHWxcBTS+tm7AhmAvWMdxO5lzJLS+UWAyPF5ICROe8Mu9iNJiO5JlCo0Wpui9RbB1C81Xhax1gWHK245ESL6k7YWvyMYWrGqr1NuQcNS0B/AIT1Nsj1WY7efMJQOmnMHkPUTWryVZlthijYyd7P2Gz6rY5a81DAFqhDNJl2pGIAE6HWtSzeUEh3jCsHEkoglKfm4VrGJEuXcALmfCMbdfTvtu4rlsaP2hQad+MG/KJFlenoTK34EMHeBPDCpqNDz8UVNk" + ], + "issuer": "https://liisa.kaisa/", + "requestURI": "https://johanna.leks/aytamah" + } +} diff --git a/libs/wire-api/test/golden/testObject_IdP_2.json b/libs/wire-api/test/golden/testObject_IdP_2.json new file mode 100644 index 00000000000..e6ae1cacd05 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_IdP_2.json @@ -0,0 +1,18 @@ +{ + "extraInfo": { + "apiVersion": "WireIdPAPIV2", + "domain": null, + "handle": "614c0bb0-1b33-98b6-8600-a1b290bbe1d7", + "oldIssuers": [], + "replacedBy": null, + "team": "fc5f3bf8-c296-69e7-27fd-70d483740fe4" + }, + "id": "614c0bb0-1b33-98b6-8600-a1b290bbe1d7", + "metadata": { + "certAuthnResponse": [ + "MIIDBTCCAe2gAwIBAgIQev76BWqjWZxChmKkGqoAfDANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE4MDIxODAwMDAwMFoXDTIwMDIxOTAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMgmGiRfLh6Fdi99XI2VA3XKHStWNRLEy5Aw/gxFxchnh2kPdk/bejFOs2swcx7yUWqxujjCNRsLBcWfaKUlTnrkY7i9x9noZlMrijgJy/Lk+HH5HX24PQCDf+twjnHHxZ9G6/8VLM2e5ZBeZm+t7M3vhuumEHG3UwloLF6cUeuPdW+exnOB1U1fHBIFOG8ns4SSIoq6zw5rdt0CSI6+l7b1DEjVvPLtJF+zyjlJ1Qp7NgBvAwdiPiRMU4l8IRVbuSVKoKYJoyJ4L3eXsjczoBSTJ6VjV2mygz96DC70MY3avccFrk7tCEC6ZlMRBfY1XPLyldT7tsR3EuzjecSa1M8CAwEAAaMhMB8wHQYDVR0OBBYEFIks1srixjpSLXeiR8zES5cTY6fBMA0GCSqGSIb3DQEBCwUAA4IBAQCKthfK4C31DMuDyQZVS3F7+4Evld3hjiwqu2uGDK+qFZas/D/eDunxsFpiwqC01RIMFFN8yvmMjHphLHiBHWxcBTS+tm7AhmAvWMdxO5lzJLS+UWAyPF5ICROe8Mu9iNJiO5JlCo0Wpui9RbB1C81Xhax1gWHK245ESL6k7YWvyMYWrGqr1NuQcNS0B/AIT1Nsj1WY7efMJQOmnMHkPUTWryVZlthijYyd7P2Gz6rY5a81DAFqhDNJl2pGIAE6HWtSzeUEh3jCsHEkoglKfm4VrGJEuXcALmfCMbdfTvtu4rlsaP2hQad+MG/KJFlenoTK34EMHeBPDCpqNDz8UVNk" + ], + "issuer": "https://liisa.kaisa/", + "requestURI": "https://johanna.leks/aytamah" + } +} diff --git a/libs/wire-api/test/golden/testObject_IdP_3.json b/libs/wire-api/test/golden/testObject_IdP_3.json new file mode 100644 index 00000000000..e7619ad16e9 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_IdP_3.json @@ -0,0 +1,21 @@ +{ + "extraInfo": { + "apiVersion": "WireIdPAPIV1", + "domain": null, + "handle": "614c0bb0-1b33-98b6-8600-a1b290bbe1d7", + "oldIssuers": [ + "https://hele.johanna/" + ], + "replacedBy": null, + "team": "fc5f3bf8-c296-69e7-27fd-70d483740fe4" + }, + "id": "614c0bb0-1b33-98b6-8600-a1b290bbe1d7", + "metadata": { + "certAuthnResponse": [ + "MIIDBTCCAe2gAwIBAgIQev76BWqjWZxChmKkGqoAfDANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE4MDIxODAwMDAwMFoXDTIwMDIxOTAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMgmGiRfLh6Fdi99XI2VA3XKHStWNRLEy5Aw/gxFxchnh2kPdk/bejFOs2swcx7yUWqxujjCNRsLBcWfaKUlTnrkY7i9x9noZlMrijgJy/Lk+HH5HX24PQCDf+twjnHHxZ9G6/8VLM2e5ZBeZm+t7M3vhuumEHG3UwloLF6cUeuPdW+exnOB1U1fHBIFOG8ns4SSIoq6zw5rdt0CSI6+l7b1DEjVvPLtJF+zyjlJ1Qp7NgBvAwdiPiRMU4l8IRVbuSVKoKYJoyJ4L3eXsjczoBSTJ6VjV2mygz96DC70MY3avccFrk7tCEC6ZlMRBfY1XPLyldT7tsR3EuzjecSa1M8CAwEAAaMhMB8wHQYDVR0OBBYEFIks1srixjpSLXeiR8zES5cTY6fBMA0GCSqGSIb3DQEBCwUAA4IBAQCKthfK4C31DMuDyQZVS3F7+4Evld3hjiwqu2uGDK+qFZas/D/eDunxsFpiwqC01RIMFFN8yvmMjHphLHiBHWxcBTS+tm7AhmAvWMdxO5lzJLS+UWAyPF5ICROe8Mu9iNJiO5JlCo0Wpui9RbB1C81Xhax1gWHK245ESL6k7YWvyMYWrGqr1NuQcNS0B/AIT1Nsj1WY7efMJQOmnMHkPUTWryVZlthijYyd7P2Gz6rY5a81DAFqhDNJl2pGIAE6HWtSzeUEh3jCsHEkoglKfm4VrGJEuXcALmfCMbdfTvtu4rlsaP2hQad+MG/KJFlenoTK34EMHeBPDCpqNDz8UVNk", + "MIIDpDCCAoygAwIBAgIGAWSx7x1HMA0GCSqGSIb3DQEBCwUAMIGSMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEUMBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi01MDA1MDgxHDAaBgkqhkiG9w0BCQEWDWluZm9Ab2t0YS5jb20wHhcNMTgwNzE5MDk0NTM1WhcNMjgwNzE5MDk0NjM0WjCBkjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNVBAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtNTAwNTA4MRwwGgYJKoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhUaQm/3dgPws1A5IjFK9ZQpj170vIqENuDG0tapAzkvk6+9vyhduGckHTeZF3k5MMlW9iix2Eg0qa1oS/Wrq/aBf7+BH6y1MJlQnaKQ3hPL+OFvYzbnrN8k2uC2LivP7Y90dXwtN3P63rA4QSyDPYEMvdKSubUKX/HNsUg4I2PwHmpfWBNgoMkqe0bxQILBv+84L62IYSd6k77XXnCFb/usHpG/gY6sJsTQ2aFl9FuJ51uf67AOj8RzPXstgtUaXbdJI0kAqKIb3j9Zv3mpPCy/GHnyB3PMalvtc1uaz1ZnwO2eliqhwB6/8W6CPutFo1Bhq1glQIX+1OD7906iORwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB0h6vKAywJwH3g0RnocOpBvT42QW57TZ3Wzm9gbg6dQL0rB+NHDx2V0VIh51E3YHL1os9W09MreM7I74D/fX27r1Q3+qAsL1v3CN8WIVh9eYitBCtF7DwZmL2UXTia+GWPrabO14qAztFmTXfqNuCZej7gJd/K2r0KBiZtZ6o58WBREW2F70a6nN6Nk1yjzBkDTJMMf8OMXHphTaalMBXojN9W6HEDpGBE0qY7c70PqvfUEzd8wHWcDxo6+3jajajelk0V4rg7Cqxccr+WwjYtENEuQypNG2mbI52iPZked0QWKy0WzhSMw5wjJ+QDG31vJInAB2769C2KmhPDyNhU" + ], + "issuer": "https://liisa.kaisa/", + "requestURI": "https://johanna.leks/aytamah" + } +} 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 91f136c22bb..8d7c6e1d2b0 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 @@ -26,6 +26,7 @@ import Imports import Test.Tasty qualified as T import Test.Tasty.QuickCheck (Arbitrary, counterexample, testProperty, (.&&.), (===)) import Type.Reflection (typeRep) +import Wire.API.App qualified as App import Wire.API.Asset qualified as Asset import Wire.API.BackgroundJobs qualified as BackgroundJobs import Wire.API.Call.Config qualified as Call.Config @@ -86,7 +87,8 @@ import Wire.API.Wrapped qualified as Wrapped tests :: T.TestTree tests = T.localOption (T.Timeout (60 * 1000000) "60s") . T.testGroup "JSON roundtrip tests" $ - [ testRoundTrip @Asset.AssetToken, + [ testRoundTrip @App.Category, + testRoundTrip @Asset.AssetToken, testRoundTrip @Asset.NewAssetToken, testRoundTrip @Asset.AssetRetention, testRoundTrip @Asset.AssetSettings, diff --git a/libs/wire-api/test/unit/Test/Wire/API/Routes/Version/Wai.hs b/libs/wire-api/test/unit/Test/Wire/API/Routes/Version/Wai.hs index b0f253cba72..7583d717378 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Routes/Version/Wai.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Routes/Version/Wai.hs @@ -20,7 +20,7 @@ module Test.Wire.API.Routes.Version.Wai where import Data.Proxy import Data.Set qualified as Set import Data.String.Conversions -import Data.Text as T +import Data.Text as T hiding (show) import Imports import Network.HTTP.Types.Status (status200, status400) import Network.Wai diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 80c4e211369..0d0a1666608 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -631,6 +631,7 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Manual.FederationStatus Test.Wire.API.Golden.Manual.GetPaginatedConversationIds Test.Wire.API.Golden.Manual.GroupId + Test.Wire.API.Golden.Manual.IdP Test.Wire.API.Golden.Manual.InvitationUserView Test.Wire.API.Golden.Manual.ListConversations Test.Wire.API.Golden.Manual.ListUsersById 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 8087bfa00f8..54e3e907ead 100644 --- a/libs/wire-message-proto-lens/wire-message-proto-lens.cabal +++ b/libs/wire-message-proto-lens/wire-message-proto-lens.cabal @@ -19,6 +19,14 @@ custom-setup , Cabal >=3.12 , proto-lens-setup +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +flag nix-dev-env + description: In a Nix dev environment. + default: False + manual: True + library exposed-modules: Proto.Mls @@ -81,7 +89,9 @@ library base , proto-lens-runtime - build-tool-depends: proto-lens-protoc:proto-lens-protoc + if !flag(nix-dev-env) + build-tool-depends: proto-lens-protoc:proto-lens-protoc + default-language: GHC2021 autogen-modules: Proto.Otr diff --git a/libs/wire-subsystems/default.nix b/libs/wire-subsystems/default.nix index 3bb1a6658b0..65e5959917d 100644 --- a/libs/wire-subsystems/default.nix +++ b/libs/wire-subsystems/default.nix @@ -8,6 +8,7 @@ , amazonka , amazonka-core , amazonka-ses +, amazonka-sqs , amqp , async , attoparsec @@ -35,6 +36,7 @@ , extended , extra , file-embed +, galley-types , gitignoreSource , hashable , HaskellNet @@ -75,6 +77,7 @@ , postgresql-error-codes , profunctors , prometheus-client +, proto-lens , QuickCheck , quickcheck-instances , random @@ -101,9 +104,11 @@ , time-out , time-units , tinylog +, tls , token-bucket , transformers , types-common +, types-common-journal , unliftio , unordered-containers , uri-bytestring @@ -128,6 +133,7 @@ mkDerivation { amazonka amazonka-core amazonka-ses + amazonka-sqs amqp async attoparsec @@ -155,6 +161,7 @@ mkDerivation { extended extra file-embed + galley-types hashable HaskellNet HaskellNet-SSL @@ -192,6 +199,7 @@ mkDerivation { postgresql-error-codes profunctors prometheus-client + proto-lens QuickCheck raw-strings-qq resource-pool @@ -214,9 +222,11 @@ mkDerivation { time-out time-units tinylog + tls token-bucket transformers types-common + types-common-journal unliftio unordered-containers uri-bytestring @@ -237,6 +247,7 @@ mkDerivation { amazonka amazonka-core amazonka-ses + amazonka-sqs amqp async attoparsec @@ -263,6 +274,7 @@ mkDerivation { extended extra file-embed + galley-types hashable HaskellNet HaskellNet-SSL @@ -298,6 +310,7 @@ mkDerivation { polysemy-wire-zoo profunctors prometheus-client + proto-lens QuickCheck quickcheck-instances random @@ -327,6 +340,7 @@ mkDerivation { token-bucket transformers types-common + types-common-journal unliftio unordered-containers uri-bytestring diff --git a/libs/wire-subsystems/postgres-migrations/20251127115917-app-fields-category-description-creator.sql b/libs/wire-subsystems/postgres-migrations/20251127115917-app-fields-category-description-creator.sql new file mode 100644 index 00000000000..ec8f60f154d --- /dev/null +++ b/libs/wire-subsystems/postgres-migrations/20251127115917-app-fields-category-description-creator.sql @@ -0,0 +1,4 @@ +ALTER TABLE apps + ADD COLUMN category text DEFAULT 'other' NOT NULL, + ADD COLUMN description text DEFAULT '' NOT NULL, + ADD COLUMN creator uuid NOT NULL; diff --git a/libs/wire-subsystems/src/Wire/AWS.hs b/libs/wire-subsystems/src/Wire/AWS.hs index d26bafff38b..78b7e74eff6 100644 --- a/libs/wire-subsystems/src/Wire/AWS.hs +++ b/libs/wire-subsystems/src/Wire/AWS.hs @@ -14,31 +14,158 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE TemplateHaskell #-} module Wire.AWS where -import Amazonka (Env, runResourceT) -import Amazonka.Core.Lens.Internal qualified as AWS -import Amazonka.Send as AWS -import Amazonka.Types qualified as AWS -import Control.Lens +import Amazonka qualified as AWS +import Amazonka.SQS qualified as SQS +import Amazonka.SQS.Lens qualified as SQS +import Control.Exception.Lens +import Control.Lens hiding ((.=)) +import Control.Monad.Catch +import Control.Monad.Trans.Resource +import Control.Retry (exponentialBackoff, limitRetries, retrying) +import Data.ByteString.Base64 qualified as B64 +import Data.ByteString.Builder (toLazyByteString) +import Data.ProtoLens.Encoding (encodeMessage) +import Data.Text.Encoding (decodeLatin1) +import Data.UUID (toText) +import Data.UUID.V4 (nextRandom) import Imports import Network.HTTP.Client -import Polysemy -import Polysemy.Input + ( HttpException (..), + HttpExceptionContent (..), + Manager, + ) +import Network.TLS qualified as TLS +import Polysemy (Embed, Member, Sem, embed) +import Polysemy.Input (Input, input) +import Proto.TeamEvents qualified as E +import System.Logger qualified as Logger +import System.Logger.Class (Logger, MonadLogger (..)) +import Util.Options (AWSEndpoint (..), awsHost, awsPort, awsSecure) +newtype QueueUrl = QueueUrl Text + deriving (Show) + +data Error where + GeneralError :: (Show e, AWS.AsError e) => e -> Error + +deriving instance Show Error + +deriving instance Typeable Error + +instance Exception Error + +data Env = Env + { _awsEnv :: !AWS.Env, + _logger :: !Logger, + _eventQueue :: !QueueUrl + } + +makeLenses ''Env + +newtype Amazon a = Amazon + { unAmazon :: ReaderT Env (ResourceT IO) a + } + deriving + ( Functor, + Applicative, + Monad, + MonadIO, + MonadThrow, + MonadCatch, + MonadMask, + MonadReader Env, + MonadResource, + MonadUnliftIO + ) + +instance MonadLogger Amazon where + log l m = view logger >>= \g -> Logger.log g l m + +mkEnv :: Logger -> Manager -> AWSEndpoint -> Text -> IO Env +mkEnv lgr mgr endpoint qname = do + let g = Logger.clone (Just "aws") lgr + e <- mkAwsEnv g + q <- getQueueUrl e qname + pure (Env e g (QueueUrl q)) + where + sqs e = AWS.setEndpoint (e ^. awsSecure) (e ^. awsHost) (e ^. awsPort) SQS.defaultService + mkAwsEnv g = do + baseEnv <- + AWS.newEnv AWS.discover + <&> AWS.configureService (sqs endpoint) + pure $ + baseEnv + { AWS.logger = awsLogger g, + AWS.retryCheck = retryCheck, + AWS.manager = mgr + } + awsLogger g l = Logger.log g (mapLevel l) . Logger.msg . toLazyByteString + mapLevel AWS.Info = Logger.Info + mapLevel AWS.Debug = Logger.Trace + mapLevel AWS.Trace = Logger.Trace + mapLevel AWS.Error = Logger.Debug + retryCheck _ InvalidUrlException {} = False + retryCheck n (HttpExceptionRequest _ ex) = case ex of + _ | n >= 3 -> False + NoResponseDataReceived -> True + ConnectionTimeout -> True + ConnectionClosed -> True + ConnectionFailure _ -> True + InternalException x -> case fromException x of + Just TLS.HandshakeFailed {} -> True + _ -> False + _ -> False + getQueueUrl :: AWS.Env -> Text -> IO Text + getQueueUrl e q = do + x <- + runResourceT $ + trying AWS._Error $ + AWS.send e (SQS.newGetQueueUrl q) + either + (throwM . GeneralError) + (pure . view SQS.getQueueUrlResponse_queueUrl) + x + +execute :: (MonadIO m) => Env -> Amazon a -> m a +execute e m = liftIO $ runResourceT (runReaderT (unAmazon m) e) + +enqueue :: E.TeamEvent -> Amazon () +enqueue ev = do + QueueUrl url <- view eventQueue + dedup <- liftIO nextRandom + amaznkaEnv <- view awsEnv + let body = decodeLatin1 $ B64.encode $ encodeMessage ev + req = + SQS.newSendMessage url body + & SQS.sendMessage_messageGroupId ?~ "team.events" + & SQS.sendMessage_messageDeduplicationId ?~ toText dedup + res <- retrying (limitRetries 5 <> exponentialBackoff 1000000) (const (pure . canRetry)) $ const (sendCatchEnv amaznkaEnv req) + either (throwM . GeneralError) (const (pure ())) res + +-- Polysemy-style helper used by existing code sendCatch :: - ( Member (Input Amazonka.Env) r, - Member (Embed IO) r, - AWS.AWSRequest req, - Typeable req, - Typeable (AWS.AWSResponse req) + ( Member (Embed IO) r, + Member (Input AWS.Env) r, + AWS.AWSRequest req ) => req -> Sem r (Either AWS.Error (AWS.AWSResponse req)) sendCatch req = do env <- input - embed . AWS.trying AWS._Error . runResourceT . AWS.send env $ req + embed $ runResourceT (trying AWS._Error (AWS.send env req)) + +-- Amazon monad variant +sendCatchEnv :: + (AWS.AWSRequest r) => + AWS.Env -> + r -> + Amazon (Either AWS.Error (AWS.AWSResponse r)) +sendCatchEnv e = trying AWS._Error . AWS.send e canRetry :: Either AWS.Error a -> Bool canRetry (Right _) = False diff --git a/libs/wire-subsystems/src/Wire/AppStore.hs b/libs/wire-subsystems/src/Wire/AppStore.hs index e756a242f68..bc347da1662 100644 --- a/libs/wire-subsystems/src/Wire/AppStore.hs +++ b/libs/wire-subsystems/src/Wire/AppStore.hs @@ -21,34 +21,48 @@ module Wire.AppStore where import Data.Aeson import Data.Id +import Data.Range import Data.UUID import Imports import Polysemy +import Wire.API.App import Wire.API.PostgresMarshall data StoredApp = StoredApp { id :: UserId, teamId :: TeamId, - meta :: Object + meta :: Object, + category :: Category, + description :: Range 0 300 Text, + creator :: UserId } deriving (Eq, Ord, Show) -instance PostgresMarshall StoredApp (UUID, UUID, Value) where +-- The `PostgresMarshall` instances are here in this module -- as +-- having them elsewhere would make them orphan instances of +-- `StoredApp`. +instance PostgresMarshall StoredApp (UUID, UUID, Value, Text, Text, UUID) where postgresMarshall app = ( postgresMarshall app.id, postgresMarshall app.teamId, - postgresMarshall app.meta + postgresMarshall app.meta, + postgresMarshall (categoryToText app.category), + postgresMarshall (fromRange app.description), + postgresMarshall app.creator ) -instance PostgresUnmarshall (UUID, UUID, Value) StoredApp where - postgresUnmarshall (uid, teamId, meta) = +instance PostgresUnmarshall (UUID, UUID, Value, Text, Text, UUID) StoredApp where + postgresUnmarshall (uid, teamId, meta, category, description, creator) = StoredApp <$> postgresUnmarshall uid <*> postgresUnmarshall teamId <*> postgresUnmarshall meta + <*> (postgresUnmarshall =<< maybe (Left $ "Category " <> category <> " not found") Right (categoryFromText category)) + <*> (maybe (Left "description out of bounds") Right . checked @0 @300 =<< postgresUnmarshall description) + <*> postgresUnmarshall creator data AppStore m a where CreateApp :: StoredApp -> AppStore m () - GetApp :: UserId -> AppStore m (Maybe StoredApp) + GetApp :: UserId -> TeamId -> AppStore m (Maybe StoredApp) makeSem ''AppStore diff --git a/libs/wire-subsystems/src/Wire/AppStore/Postgres.hs b/libs/wire-subsystems/src/Wire/AppStore/Postgres.hs index dc6b4c14dd3..d457003e3c9 100644 --- a/libs/wire-subsystems/src/Wire/AppStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/AppStore/Postgres.hs @@ -42,7 +42,7 @@ interpretAppStoreToPostgres :: interpretAppStoreToPostgres = interpret $ \case CreateApp app -> createAppImpl app - GetApp userId -> getAppImpl userId + GetApp userId teamId -> getAppImpl userId teamId createAppImpl :: ( Member (Input Pool) r, @@ -55,8 +55,8 @@ createAppImpl app = runStatement app $ lmapPG [resultlessStatement| - insert into apps (user_id, team_id, metadata) - values ($1 :: uuid, $2 :: uuid, $3 :: json) |] + insert into apps (user_id, team_id, metadata, category, description, creator) + values ($1 :: uuid, $2 :: uuid, $3 :: json, $4 :: text, $5 :: text, $6 :: uuid) |] getAppImpl :: ( Member (Input Pool) r, @@ -64,9 +64,10 @@ getAppImpl :: Member (Error UsageError) r ) => UserId -> + TeamId -> Sem r (Maybe StoredApp) -getAppImpl uid = - runStatement uid $ +getAppImpl uid tid = + runStatement (uid, tid) $ dimapPG - [maybeStatement| select (user_id :: uuid), (team_id :: uuid), (metadata :: json) - from apps where user_id = ($1 :: uuid) |] + [maybeStatement| select (user_id :: uuid), (team_id :: uuid), (metadata :: json), (category :: text), (description :: text), (creator :: uuid) + from apps where user_id = ($1 :: uuid) and team_id = ($2 :: uuid) |] diff --git a/libs/wire-subsystems/src/Wire/AppSubsystem.hs b/libs/wire-subsystems/src/Wire/AppSubsystem.hs index 38112be32ec..de5ae18a24a 100644 --- a/libs/wire-subsystems/src/Wire/AppSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/AppSubsystem.hs @@ -26,7 +26,7 @@ import Imports import Network.HTTP.Types.Status import Network.Wai.Utilities.Error qualified as Wai import Polysemy -import Wire.API.App +import Wire.API.App qualified as Apps import Wire.API.User import Wire.API.User.Auth import Wire.Error @@ -35,17 +35,23 @@ data AppSubsystemConfig = AppSubsystemConfig { defaultLocale :: Locale } -data AppSubsystemError = AppSubsystemErrorNoPerm | AppSubsystemErrorNoUser | AppSubsystemErrorNoApp +data AppSubsystemError + = AppSubsystemErrorNoPerm + | AppSubsystemErrorNoUser -- The user having created the app not found + | AppSubsystemErrorAppUserNotFound -- The user used to "enact" the app not found + | AppSubsystemErrorNoApp appSubsystemErrorToHttpError :: AppSubsystemError -> HttpError appSubsystemErrorToHttpError = StdError . \case AppSubsystemErrorNoPerm -> Wai.mkError status403 "app-no-permission" "User does not have permission to create or manage apps" AppSubsystemErrorNoUser -> Wai.mkError status403 "create-app-no-user" "App owner not found" + AppSubsystemErrorAppUserNotFound -> Wai.mkError status403 "app-user-not-found" "App user not found" AppSubsystemErrorNoApp -> Wai.mkError status404 "app-not-found" "App not found" data AppSubsystem m a where - CreateApp :: Local UserId -> TeamId -> NewApp -> AppSubsystem m CreatedApp + CreateApp :: Local UserId -> TeamId -> Apps.NewApp -> AppSubsystem m Apps.CreatedApp + GetApp :: Local UserId -> TeamId -> UserId -> AppSubsystem m Apps.GetApp RefreshAppCookie :: Local UserId -> TeamId -> diff --git a/libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs index 11d01611f33..b3f22d1e1e2 100644 --- a/libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs @@ -31,7 +31,7 @@ import Polysemy.Input import Polysemy.TinyLog (TinyLog) import Polysemy.TinyLog qualified as Log import System.Logger.Message qualified as Log -import Wire.API.App +import Wire.API.App qualified as Apps import Wire.API.Event.Team import Wire.API.Team.Member qualified as T import Wire.API.User @@ -49,6 +49,7 @@ import Wire.TeamSubsystem import Wire.TeamSubsystem.Util import Wire.UserStore (UserStore) import Wire.UserStore qualified as Store +import Wire.UserSubsystem (UserSubsystem, internalUpdateSearchIndex) runAppSubsystem :: ( Member UserStore r, @@ -61,12 +62,14 @@ runAppSubsystem :: Member Now r, Member TeamSubsystem r, Member NotificationSubsystem r, - Member AuthenticationSubsystem r + Member AuthenticationSubsystem r, + Member UserSubsystem r ) => Sem (AppSubsystem ': r) a -> Sem r a runAppSubsystem = interpret \case CreateApp lusr tid new -> createAppImpl lusr tid new + GetApp lusr tid uid -> getAppImpl lusr tid uid RefreshAppCookie lusr tid appId -> runError $ refreshAppCookieImpl lusr tid appId createAppImpl :: @@ -80,16 +83,16 @@ createAppImpl :: Member Now r, Member TeamSubsystem r, Member NotificationSubsystem r, - Member AuthenticationSubsystem r + Member AuthenticationSubsystem r, + Member UserSubsystem r ) => Local UserId -> TeamId -> - NewApp -> - Sem r CreatedApp -createAppImpl lusr tid new = do - creator <- Store.getUser (tUnqualified lusr) >>= note AppSubsystemErrorNoUser - - mem <- getTeamMember creator.id tid >>= note AppSubsystemErrorNoPerm + Apps.NewApp -> + Sem r Apps.CreatedApp +createAppImpl lusr tid (Apps.NewApp new password6) = do + verifyUserPasswordError lusr password6 + (creator, mem) <- ensureTeamMember lusr tid note AppSubsystemErrorNoPerm $ guard (T.hasPermission mem T.CreateApp) u <- appNewStoredUser creator new @@ -97,7 +100,10 @@ createAppImpl lusr tid new = do StoredApp { id = u.id, teamId = tid, - meta = new.meta + meta = new.meta, + category = new.category, + description = new.description, + creator = tUnqualified lusr } Log.info $ @@ -108,17 +114,57 @@ createAppImpl lusr tid new = do -- create app and user entries Store.createApp app Store.createUser u Nothing + internalUpdateSearchIndex u.id -- generate a team event generateTeamEvents creator.id tid [EdAppCreate u.id] c :: Cookie (Token U) <- newCookie u.id Nothing PersistentCookie (Just "app") pure - CreatedApp + Apps.CreatedApp { user = newStoredUserToUser (tUntagged (qualifyAs lusr u)), cookie = mkSomeToken c.cookieValue } +-- | Check that @lusr@ is member of team with @tid@. +ensureTeamMember :: + ( Member UserStore r, + Member (Error AppSubsystemError) r, + Member GalleyAPIAccess r + ) => + Local UserId -> + TeamId -> + Sem r (StoredUser, T.TeamMember) +ensureTeamMember lusr tid = do + storedUser <- Store.getUser (tUnqualified lusr) >>= note AppSubsystemErrorNoUser + teamMember <- getTeamMember storedUser.id tid >>= note AppSubsystemErrorNoPerm + pure (storedUser, teamMember) + +getAppImpl :: + ( Member AppStore r, + Member (Error AppSubsystemError) r, + Member GalleyAPIAccess r, + Member UserStore r + ) => + Local UserId -> + TeamId -> + UserId -> + Sem r Apps.GetApp +getAppImpl lusr tid uid = do + void $ ensureTeamMember lusr tid + storedApp <- Store.getApp uid tid >>= note AppSubsystemErrorNoApp + u <- Store.getUser uid >>= note AppSubsystemErrorAppUserNotFound + pure $ + Apps.GetApp + { name = u.name, + pict = fromMaybe (Pict []) u.pict, + assets = fromMaybe [] u.assets, + accentId = u.accentId, + meta = storedApp.meta, + category = storedApp.category, + description = storedApp.description + } + refreshAppCookieImpl :: ( Member AuthenticationSubsystem r, Member AppStore r, @@ -133,8 +179,7 @@ refreshAppCookieImpl :: refreshAppCookieImpl (tUnqualified -> uid) tid appId = do mem <- getTeamMember uid tid >>= note AppSubsystemErrorNoPerm note AppSubsystemErrorNoPerm $ guard (T.hasPermission mem T.ManageApps) - app <- Store.getApp appId >>= note AppSubsystemErrorNoApp - note AppSubsystemErrorNoApp $ guard (app.teamId == tid) + void $ Store.getApp appId tid >>= note AppSubsystemErrorNoApp c :: Cookie (Token U) <- newCookieLimited appId Nothing PersistentCookie (Just "app") @@ -146,7 +191,7 @@ appNewStoredUser :: Member (Input AppSubsystemConfig) r ) => StoredUser -> - NewApp -> + Apps.GetApp -> Sem r NewStoredUser appNewStoredUser creator new = do uid <- liftIO nextRandom diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/RabbitMQ.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/RabbitMQ.hs index 78b3b9aa8b1..fecd5ca6bd2 100644 --- a/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/RabbitMQ.hs +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsPublisher/RabbitMQ.hs @@ -38,8 +38,7 @@ interpretBackgroundJobsPublisherRabbitMQ requestId channelMVar = publishJob requestId channel jobId jobPayload publishJob :: - ( Member (Embed IO) r - ) => + (Member (Embed IO) r) => RequestId -> Q.Channel -> JobId -> diff --git a/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs b/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs index e9055a1f399..7353ad6092f 100644 --- a/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/BackgroundJobsRunner/Interpreter.hs @@ -143,14 +143,6 @@ runSyncUserGroupAndChannel (SyncUserGroupAndChannel {..}) = do action def -localBotsAndUsers :: (Foldable f) => f LocalMember -> ([BotMember], [LocalMember]) -localBotsAndUsers = foldMap botOrUser - where - botOrUser m = case m.service of - -- we drop invalid bots here, which shouldn't happen - Just _ -> (toList (newBotMember m), []) - Nothing -> ([], [m]) - runSyncUserGroup :: ( Member UserGroupStore r, Member BackgroundJobsPublisher r, diff --git a/libs/wire-subsystems/src/Wire/BrigAPIAccess.hs b/libs/wire-subsystems/src/Wire/BrigAPIAccess.hs index c9f9aa41688..34b90fe5b00 100644 --- a/libs/wire-subsystems/src/Wire/BrigAPIAccess.hs +++ b/libs/wire-subsystems/src/Wire/BrigAPIAccess.hs @@ -161,7 +161,7 @@ data BrigAPIAccess m a where GetAccountsBy :: GetBy -> BrigAPIAccess m [User] CreateGroupInternal :: ManagedBy -> TeamId -> Maybe UserId -> NewUserGroup -> BrigAPIAccess m (Either Wai.Error UserGroup) GetGroupInternal :: TeamId -> UserGroupId -> Bool -> BrigAPIAccess m (Maybe UserGroup) - GetGroupsInternal :: TeamId -> Maybe Scim.Filter -> BrigAPIAccess m UserGroupPageWithMembers + GetGroupsInternal :: TeamId -> Maybe Scim.Filter -> Maybe ManagedBy -> Word -> Maybe Word -> BrigAPIAccess m UserGroupPageWithMembers UpdateGroup :: UpdateGroupInternalRequest -> BrigAPIAccess m (Either Wai.Error ()) DeleteGroupInternal :: ManagedBy -> TeamId -> UserGroupId -> BrigAPIAccess m (Either DeleteGroupManagedError ()) diff --git a/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs b/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs index 08df77e2173..bca65344eef 100644 --- a/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs @@ -124,8 +124,8 @@ interpretBrigAccess brigEndpoint = getAccountsBy localGetBy CreateGroupInternal managedBy teamId creatorUserId newGroup -> createGroupInternal managedBy teamId creatorUserId newGroup - GetGroupsInternal tid mbFilter -> - getGroupsInternal tid mbFilter + GetGroupsInternal tid mbFilter mbManagedBy startIndex mbCount -> + getGroupsInternal tid mbFilter mbManagedBy startIndex mbCount GetGroupInternal tid gid includeChannels -> getGroupInternal tid gid includeChannels UpdateGroup req -> @@ -605,8 +605,11 @@ getGroupsInternal :: (Member Rpc r, Member (Input Endpoint) r, Member (Error ParseException) r) => TeamId -> Maybe Scim.Filter -> + Maybe ManagedBy -> + Word -> + Maybe Word -> Sem r UserGroupPageWithMembers -getGroupsInternal tid mbFilter = do +getGroupsInternal tid mbFilter mbManagedBy startIndex mbCount = do maybeDisplayName :: Maybe Text <- case mbFilter of Just filter' -> case filter' of FilterAttrCompare (AttrPath _schema "displayName" Nothing) OpCo (ValString str) -> pure $ Just str @@ -617,6 +620,9 @@ getGroupsInternal tid mbFilter = do method GET . paths ["i", "user-groups", toByteString' tid] . maybe id (queryItem "nameContains" . Text.encodeUtf8) maybeDisplayName + . maybe id (queryItem "managedBy" . toByteString') mbManagedBy + . queryItem "startIndex" (toByteString' startIndex) + . maybe id (queryItem "count" . toByteString') mbCount . expect2xx decodeBodyOrThrow "brig" r diff --git a/libs/wire-subsystems/src/Wire/ConversationStore.hs b/libs/wire-subsystems/src/Wire/ConversationStore.hs index 56f319982db..2a529d40cd5 100644 --- a/libs/wire-subsystems/src/Wire/ConversationStore.hs +++ b/libs/wire-subsystems/src/Wire/ConversationStore.hs @@ -231,3 +231,10 @@ data PostgresMigrationOpts = PostgresMigrationOpts instance FromJSON PostgresMigrationOpts where parseJSON = withObject "PostgresMigrationOpts" $ \o -> PostgresMigrationOpts <$> o .: "conversation" + +getConvOrSubGroupInfo :: + (Member ConversationStore r) => + ConvOrSubConvId -> + Sem r (Maybe GroupInfoData) +getConvOrSubGroupInfo (Conv c) = getGroupInfo c +getConvOrSubGroupInfo (SubConv c s) = getSubConversationGroupInfo c s diff --git a/libs/wire-subsystems/src/Wire/ConversationStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/ConversationStore/Cassandra.hs index cab006f3464..66f3943b90e 100644 --- a/libs/wire-subsystems/src/Wire/ConversationStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/ConversationStore/Cassandra.hs @@ -260,8 +260,7 @@ localConversations :: Sem r [StoredConversation] localConversations client = collectAndLog - <=< ( runEmbedded (runClient client) . embed . UnliftIO.pooledMapConcurrentlyN 8 localConversation' - ) + <=< (runEmbedded (runClient client) . embed . UnliftIO.pooledMapConcurrentlyN 8 localConversation') where collectAndLog cs = case partitionEithers cs of (errs, convs) -> traverse_ (warn . Log.msg) errs $> convs diff --git a/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs b/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs index 2a968a106c1..6cae2fc9cf6 100644 --- a/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs +++ b/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs @@ -22,6 +22,7 @@ module Wire.ConversationStore.Migration where import Cassandra import Cassandra.Settings hiding (pageSize) import Control.Error (lastMay) +import Data.Aeson (FromJSON) import Data.Conduit import Data.Conduit.Internal (zipSources) import Data.Conduit.List qualified as C @@ -36,6 +37,7 @@ import Data.Time.Calendar.OrdinalDate (fromOrdinalDate) import Data.Tuple.Extra import Data.Vector (Vector) import Data.Vector qualified as Vector +import GHC.Generics (Generically (..)) import Hasql.Pool qualified as Hasql import Hasql.Statement qualified as Hasql import Hasql.TH @@ -52,6 +54,7 @@ import Polysemy.Time import Polysemy.TinyLog import Prometheus qualified import System.Logger qualified as Log +import UnliftIO.Exception qualified as UnliftIO import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.CellsState import Wire.API.Conversation.Protocol @@ -70,6 +73,8 @@ import Wire.ConversationStore.Migration.Cleanup import Wire.ConversationStore.Migration.Types import Wire.ConversationStore.MigrationLock import Wire.Postgres +import Wire.Sem.Concurrency (Concurrency, ConcurrencySafety (..), unsafePooledMapConcurrentlyN_) +import Wire.Sem.Concurrency.IO (unsafelyPerformConcurrency) import Wire.Sem.Logger (mapLogger) import Wire.Sem.Logger.TinyLog (loggerToTinyLog) import Wire.Sem.Paging.Cassandra @@ -78,28 +83,45 @@ import Wire.Util -- * Top level logic -type EffectStack = [State Int, Input ClientState, Input Hasql.Pool, Async, Race, TinyLog, Embed IO, Final IO] +type EffectStack = [State Int, Input ClientState, Input Hasql.Pool, Async, Race, TinyLog, Embed IO, Concurrency 'Unsafe, Final IO] -migrateConvsLoop :: ClientState -> Hasql.Pool -> Log.Logger -> Prometheus.Counter -> Prometheus.Counter -> IO () -migrateConvsLoop cassClient pgPool logger migCounter migFinished = - migrationLoop cassClient pgPool logger "conversations" migFinished $ migrateAllConversations migCounter +data MigrationOptions = MigrationOptions + { pageSize :: Int32, + parallelism :: Int + } + deriving (Show, Eq, Generic) + deriving (FromJSON) via Generically MigrationOptions -migrateUsersLoop :: ClientState -> Hasql.Pool -> Log.Logger -> Prometheus.Counter -> Prometheus.Counter -> IO () -migrateUsersLoop cassClient pgPool logger migCounter migFinished = - migrationLoop cassClient pgPool logger "users" migFinished $ migrateAllUsers migCounter +migrateConvsLoop :: MigrationOptions -> ClientState -> Hasql.Pool -> Log.Logger -> Prometheus.Counter -> Prometheus.Counter -> Prometheus.Counter -> IO () +migrateConvsLoop migOpts cassClient pgPool logger migCounter migFinished migFailed = + migrationLoop cassClient pgPool logger "conversations" migFinished migFailed $ migrateAllConversations migOpts migCounter -migrationLoop :: ClientState -> Hasql.Pool -> Log.Logger -> ByteString -> Prometheus.Counter -> ConduitT () Void (Sem EffectStack) () -> IO () -migrationLoop cassClient pgPool logger name migFinished migration = do - go 0 - Prometheus.incCounter migFinished +migrateUsersLoop :: MigrationOptions -> ClientState -> Hasql.Pool -> Log.Logger -> Prometheus.Counter -> Prometheus.Counter -> Prometheus.Counter -> IO () +migrateUsersLoop migOpts cassClient pgPool logger migCounter migFinished migFailed = + migrationLoop cassClient pgPool logger "users" migFinished migFailed $ migrateAllUsers migOpts migCounter + +migrationLoop :: ClientState -> Hasql.Pool -> Log.Logger -> ByteString -> Prometheus.Counter -> Prometheus.Counter -> ConduitT () Void (Sem EffectStack) () -> IO () +migrationLoop cassClient pgPool logger name migFinished migFailed migration = do + go 0 `UnliftIO.catch` handleIOError where + handleIOError :: SomeException -> IO () + handleIOError exc = do + Prometheus.incCounter migFailed + Log.err logger $ + Log.msg (Log.val "migration failed, it won't restart unless the background-worker is restarted.") + . Log.field "migration" name + . Log.field "error" (displayException exc) + UnliftIO.throwIO exc + go :: Int -> IO () go nIter = do runMigration >>= \case - 0 -> + 0 -> do Log.info logger $ Log.msg (Log.val "finished migration") . Log.field "attempt" nIter + . Log.field "migration" name + Prometheus.incCounter migFinished n -> do Log.info logger $ Log.msg (Log.val "finished migration with errors") @@ -117,6 +139,7 @@ migrationLoop cassClient pgPool logger name migFinished migration = do interpreter :: ClientState -> Hasql.Pool -> Log.Logger -> ByteString -> Sem EffectStack a -> IO (Int, a) interpreter cassClient pgPool logger name = runFinal + . unsafelyPerformConcurrency . embedToFinal . loggerToTinyLog logger . mapLogger (Log.field "migration" name .) @@ -127,9 +150,6 @@ interpreter cassClient pgPool logger name = . runInputConst cassClient . runState 0 -pageSize :: Int32 -pageSize = 10000 - migrateAllConversations :: ( Member (Input Hasql.Pool) r, Member (Embed IO) r, @@ -137,15 +157,17 @@ migrateAllConversations :: Member TinyLog r, Member Async r, Member Race r, - Member (State Int) r + Member (State Int) r, + Member (Concurrency Unsafe) r ) => + MigrationOptions -> Prometheus.Counter -> ConduitM () Void (Sem r) () -migrateAllConversations migCounter = do +migrateAllConversations migOpts migCounter = do lift $ info $ Log.msg (Log.val "migrateAllConversations") - withCount (paginateSem select (paramsP LocalQuorum () pageSize) x5) - .| logRetrievedPage - .| C.mapM_ (mapM_ (handleErrors (migrateConversation migCounter) "conv")) + withCount (paginateSem select (paramsP LocalQuorum () migOpts.pageSize) x5) + .| logRetrievedPage migOpts.pageSize + .| C.mapM_ (unsafePooledMapConcurrentlyN_ migOpts.parallelism (handleErrors (migrateConversation migCounter) "conv")) where select :: PrepQuery R () (Identity ConvId) select = "select conv from conversation" @@ -157,21 +179,23 @@ migrateAllUsers :: Member TinyLog r, Member Async r, Member Race r, - Member (State Int) r + Member (State Int) r, + Member (Concurrency 'Unsafe) r ) => + MigrationOptions -> Prometheus.Counter -> ConduitM () Void (Sem r) () -migrateAllUsers migCounter = do +migrateAllUsers migOpts migCounter = do lift $ info $ Log.msg (Log.val "migrateAllUsers") - withCount (paginateSem select (paramsP LocalQuorum () pageSize) x5) - .| logRetrievedPage - .| C.mapM_ (mapM_ (handleErrors (migrateUser migCounter) "user")) + withCount (paginateSem select (paramsP LocalQuorum () migOpts.pageSize) x5) + .| logRetrievedPage migOpts.pageSize + .| C.mapM_ (unsafePooledMapConcurrentlyN_ migOpts.parallelism (handleErrors (migrateUser migCounter) "user")) where select :: PrepQuery R () (Identity UserId) select = "select distinct user from user_remote_conv" -logRetrievedPage :: (Member TinyLog r) => ConduitM (Int32, [Identity (Id a)]) [Id a] (Sem r) () -logRetrievedPage = +logRetrievedPage :: (Member TinyLog r) => Int32 -> ConduitM (Int32, [Identity (Id a)]) [Id a] (Sem r) () +logRetrievedPage pageSize = C.mapM ( \(i, rows) -> do let estimatedRowsSoFar = (i - 1) * pageSize + fromIntegral (length rows) diff --git a/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs b/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs index 4a668eaa809..ff1c89490cf 100644 --- a/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs @@ -35,6 +35,7 @@ import GHC.Records (HasField) import Hasql.Decoders qualified as HD import Hasql.Pipeline qualified as Pipeline import Hasql.Pool qualified as Hasql +import Hasql.Session qualified as HasqlSession import Hasql.Statement qualified as Hasql import Hasql.TH import Hasql.Transaction (Transaction) @@ -203,8 +204,8 @@ getConversationImpl cid = case mConvRow of Nothing -> pure Nothing Just convRow -> do - localMembers <- Transaction.statement cid selectLocalMembersStmt - remoteMembers <- Transaction.statement cid selectRemoteMembersStmt + localMembers <- dedupMembers cid <$> Transaction.statement cid selectLocalMembersStmt + remoteMembers <- dedupMembers cid <$> Transaction.statement cid selectRemoteMembersStmt pure $ toConv cid localMembers remoteMembers (Just convRow) selectConvMetadata :: Hasql.Statement (ConvId) (Maybe ConvRow) @@ -271,7 +272,13 @@ getConversationsImpl cids = do (otr_archived :: boolean?), (otr_archived_ref :: text?), (hidden :: boolean?), (hidden_ref :: text?), (conversation_role :: text?) FROM conversation_member WHERE conv = ANY ($1 :: uuid[]) - OR conv IN (SELECT parent_conv FROM conversation WHERE id = ANY ($1 :: uuid[])) + + UNION ALL + + SELECT (conv :: uuid), ("user" :: uuid), (service :: uuid?), (provider :: uuid?), (otr_muted_status :: integer?), (otr_muted_ref :: text?), + (otr_archived :: boolean?), (otr_archived_ref :: text?), (hidden :: boolean?), (hidden_ref :: text?), (conversation_role :: text?) + FROM conversation_member + WHERE conv IN (SELECT parent_conv FROM conversation WHERE id = ANY ($1 :: uuid[])) |] selectAllRemoteMembers :: Hasql.Statement [ConvId] [RemoteMemberRow] selectAllRemoteMembers = @@ -279,7 +286,12 @@ getConversationsImpl cids = do [vectorStatement|SELECT (conv :: uuid), (user_remote_domain :: text), (user_remote_id :: uuid), (conversation_role :: text) FROM local_conversation_remote_member WHERE conv = ANY ($1 :: uuid[]) - OR conv IN (SELECT parent_conv FROM conversation WHERE id = ANY ($1 :: uuid[])) + + UNION ALL + + SELECT (conv :: uuid), (user_remote_domain :: text), (user_remote_id :: uuid), (conversation_role :: text) + FROM local_conversation_remote_member + WHERE conv IN (SELECT parent_conv FROM conversation WHERE id = ANY ($1 :: uuid[])) |] findMembers :: (HasField "id_" a b, Eq b) => ConvId -> Maybe ConvId -> [(ConvId, a)] -> [a] @@ -646,7 +658,13 @@ createBotMemberImpl serviceRef botId convId = do getLocalMemberImpl :: (PGConstraints r) => ConvId -> UserId -> Sem r (Maybe LocalMember) getLocalMemberImpl convId userId = do - mRow <- runStatement (convId, userId) selectMember + mRow <- + runSession $ do + mDirectMember <- HasqlSession.statement (convId, userId) selectMember + case mDirectMember of + Nothing -> HasqlSession.statement (convId, userId) selectParentMember + Just mem -> pure (Just mem) + pure $ snd . mkLocalMember <$> mRow where selectMember :: Hasql.Statement (ConvId, UserId) (Maybe (ConvId, UserId, Maybe ServiceId, Maybe ProviderId, Maybe MutedStatus, Maybe Text, Maybe Bool, Maybe Text, Maybe Bool, Maybe Text, Maybe RoleName)) @@ -655,30 +673,44 @@ getLocalMemberImpl convId userId = do [maybeStatement|SELECT (conv :: uuid), ("user" :: uuid), (service :: uuid?), (provider :: uuid?), (otr_muted_status :: integer?), (otr_muted_ref :: text?), (otr_archived :: boolean?), (otr_archived_ref :: text?), (hidden :: boolean?), (hidden_ref :: text?), (conversation_role :: text?) FROM conversation_member - WHERE (conv = ($1 :: uuid) - OR conv IN (SELECT parent_conv FROM conversation WHERE id = ($1 :: uuid))) + WHERE conv = ($1 :: uuid) + AND "user" = ($2 :: uuid) + |] + + selectParentMember :: Hasql.Statement (ConvId, UserId) (Maybe (ConvId, UserId, Maybe ServiceId, Maybe ProviderId, Maybe MutedStatus, Maybe Text, Maybe Bool, Maybe Text, Maybe Bool, Maybe Text, Maybe RoleName)) + selectParentMember = + dimapPG + [maybeStatement|SELECT (conv :: uuid), ("user" :: uuid), (service :: uuid?), (provider :: uuid?), (otr_muted_status :: integer?), (otr_muted_ref :: text?), + (otr_archived :: boolean?), (otr_archived_ref :: text?), (hidden :: boolean?), (hidden_ref :: text?), (conversation_role :: text?) + FROM conversation_member + WHERE conv IN (SELECT parent_conv FROM conversation WHERE id = ($1 :: uuid)) AND "user" = ($2 :: uuid) - ORDER BY CASE - WHEN conv = ($1 :: uuid) THEN 1 - ELSE 2 - END - LIMIT 1 |] getLocalMembersImpl :: (PGConstraints r) => ConvId -> Sem r [LocalMember] getLocalMembersImpl convId = - runStatement convId selectLocalMembersStmt + dedupMembers convId <$> runStatement convId selectLocalMembersStmt type LocalMemberRow = (ConvId, UserId, Maybe ServiceId, Maybe ProviderId, Maybe MutedStatus, Maybe Text, Maybe Bool, Maybe Text, Maybe Bool, Maybe Text, Maybe RoleName) -selectLocalMembersStmt :: Hasql.Statement ConvId [LocalMember] +-- | A user can be member of a conv and its parent. If the user is only part of +-- the one of these, we return that member object. If the user is part of both, +-- we return the one corresponding to the conversation being queried. +dedupMembers :: (HasField "id_" mem a, Ord a) => ConvId -> [(ConvId, mem)] -> [mem] +dedupMembers convId memsWithConvIds = + let sortFunction (cid1, mem1) (cid2, mem2) + | cid1 == cid2 = EQ + | cid1 == convId = LT + | cid2 == convId = GT + | otherwise = compare (cid1, mem1.id_) (cid2, mem2.id_) + orderedMems = snd <$> sortBy sortFunction memsWithConvIds + in nubBy ((==) `on` (.id_)) orderedMems + +-- | The members must be deduped using 'dedupMembers' before use +selectLocalMembersStmt :: Hasql.Statement ConvId [(ConvId, LocalMember)] selectLocalMembersStmt = - dedupMembers <$> select + mkLocalMember <$$> select where - dedupMembers rows = - let localMembers = mkLocalMember <$> rows - in map snd $ nubBy ((==) `on` ((.id_) . snd)) localMembers - select :: Hasql.Statement ConvId [LocalMemberRow] select = dimapPG @@ -686,11 +718,13 @@ selectLocalMembersStmt = (otr_archived :: boolean?), (otr_archived_ref :: text?), (hidden :: boolean?), (hidden_ref :: text?), (conversation_role :: text?) FROM conversation_member WHERE conv = ($1 :: uuid) - OR conv IN (SELECT parent_conv FROM conversation WHERE id = ($1 :: uuid)) - ORDER BY CASE - WHEN conv = ($1 :: uuid) THEN 1 - ELSE 2 - END + + UNION ALL + + SELECT (conv :: uuid), ("user" :: uuid), (service :: uuid?), (provider :: uuid?), (otr_muted_status :: integer?), (otr_muted_ref :: text?), + (otr_archived :: boolean?), (otr_archived_ref :: text?), (hidden :: boolean?), (hidden_ref :: text?), (conversation_role :: text?) + FROM conversation_member + WHERE conv IN (SELECT parent_conv FROM conversation WHERE id = ($1 :: uuid)) |] mkLocalMember :: LocalMemberRow -> (ConvId, LocalMember) @@ -712,8 +746,15 @@ mkLocalMember (cid, uid, mServiceId, mProviderId, msOtrMutedStatus, msOtrMutedRe type RemoteMemberRow = (ConvId, Domain, UserId, RoleName) getRemoteMemberImpl :: (PGConstraints r) => ConvId -> Remote UserId -> Sem r (Maybe RemoteMember) -getRemoteMemberImpl convId (tUntagged -> Qualified uid domain) = - snd . mkRemoteMember <$$> runStatement (convId, domain, uid) selectMember +getRemoteMemberImpl convId (tUntagged -> Qualified uid domain) = do + mRow <- + runSession $ do + mDirectMember <- HasqlSession.statement (convId, domain, uid) selectMember + case mDirectMember of + Nothing -> HasqlSession.statement (convId, domain, uid) selectParentMember + Just mem -> pure (Just mem) + + pure $ snd . mkRemoteMember <$> mRow where selectMember :: Hasql.Statement (ConvId, Domain, UserId) (Maybe RemoteMemberRow) selectMember = @@ -722,38 +763,40 @@ getRemoteMemberImpl convId (tUntagged -> Qualified uid domain) = FROM local_conversation_remote_member WHERE user_remote_domain = ($2 :: text) AND user_remote_id = ($3 :: uuid) - AND (conv = ($1 :: uuid) - OR conv IN (SELECT parent_conv FROM conversation WHERE id = ($1 :: uuid))) - ORDER BY CASE - WHEN conv = ($1 :: uuid) THEN 1 - ELSE 2 - END - LIMIT 1 + AND conv = ($1 :: uuid) + |] + + selectParentMember :: Hasql.Statement (ConvId, Domain, UserId) (Maybe RemoteMemberRow) + selectParentMember = + dimapPG + [maybeStatement|SELECT (conv :: uuid), (user_remote_domain :: text), (user_remote_id :: uuid), (conversation_role :: text) + FROM local_conversation_remote_member + WHERE user_remote_domain = ($2 :: text) + AND user_remote_id = ($3 :: uuid) + AND conv IN (SELECT parent_conv FROM conversation WHERE id = ($1 :: uuid)) |] getRemoteMembersImpl :: (PGConstraints r) => ConvId -> Sem r [RemoteMember] getRemoteMembersImpl convId = - runStatement convId selectRemoteMembersStmt + dedupMembers convId <$> runStatement convId selectRemoteMembersStmt -selectRemoteMembersStmt :: Hasql.Statement ConvId [RemoteMember] +-- | The members must be deduped using 'dedupMembers' before use +selectRemoteMembersStmt :: Hasql.Statement ConvId [(ConvId, RemoteMember)] selectRemoteMembersStmt = - dedupMembers <$> select + mkRemoteMember <$$> select where - dedupMembers rows = - let localMembers = mkRemoteMember <$> rows - in map snd $ nubBy ((==) `on` ((.id_) . snd)) localMembers - select :: Hasql.Statement ConvId [(ConvId, Domain, UserId, RoleName)] select = dimapPG [vectorStatement|SELECT (conv :: uuid), (user_remote_domain :: text), (user_remote_id :: uuid), (conversation_role :: text) FROM local_conversation_remote_member - WHERE (conv = ($1 :: uuid) - OR conv IN (SELECT parent_conv FROM conversation WHERE id = ($1 :: uuid))) - ORDER BY CASE - WHEN conv = ($1 :: uuid) THEN 1 - ELSE 2 - END + WHERE conv = ($1 :: uuid) + + UNION ALL + + SELECT (conv :: uuid), (user_remote_domain :: text), (user_remote_id :: uuid), (conversation_role :: text) + FROM local_conversation_remote_member + WHERE conv IN (SELECT parent_conv FROM conversation WHERE id = ($1 :: uuid)) |] mkRemoteMember :: (ConvId, Domain, UserId, RoleName) -> (ConvId, RemoteMember) @@ -1233,15 +1276,15 @@ searchConversationsImpl req = (sortOrderOperator req.sortOrder) -- the pagination cursor must match the ORDER BY. Therefore the comparison is case-insensitive. (mkClause "lower(name)" (Text.toLower lastName) <> mkClause "id" lastId) - | lastName <- toList req.lastName, - lastId <- toList req.lastId + | lastName <- toList req.lastName, + lastId <- toList req.lastId ] <> toList (like "name" <$> req.searchString) <> discoverableClause ) -- keep ordering consistent with the outer query, therefore case-insensitive <> orderBy [("lower(name)", req.sortOrder), ("id", req.sortOrder)] - <> limit (pageSizeToInt32 req.pageSize) + <> limit (fromIntegral @_ @Int32 $ pageSizeToWord req.pageSize) <> literal ")" discoverableClause diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs index 2cc7001a013..089e6d14c76 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Interpreter.hs @@ -22,6 +22,7 @@ import Data.Id import Data.Json.Util (ToJSONObject (toJSONObject)) import Data.Qualified import Data.Singletons (Sing) +import Galley.Types.Teams (FeatureDefaults) import Imports import Network.AMQP qualified as Q import Polysemy @@ -29,11 +30,13 @@ import Polysemy.Error import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Action import Wire.API.Conversation.CellsState (CellsState (..)) +import Wire.API.Conversation.Protocol (ProtocolTag) import Wire.API.Event.Conversation import Wire.API.Federation.API (makeConversationUpdateBundle, sendBundle) import Wire.API.Federation.API.Galley.Notifications (ConversationUpdate (..)) import Wire.API.Federation.Error (FederationError) -import Wire.API.Push.V2 qualified as PushV2 +import Wire.API.MLS.Keys (MLSKeysByPurpose, MLSPrivateKeys) +import Wire.API.Team.Feature (LegalholdConfig) import Wire.BackendNotificationQueueAccess (BackendNotificationQueueAccess, enqueueNotificationsConcurrently) import Wire.ConversationSubsystem import Wire.ExternalAccess (ExternalAccess, deliverAsync) @@ -42,6 +45,13 @@ import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now import Wire.StoredConversation +data ConversationSubsystemConfig = ConversationSubsystemConfig + { mlsKeys :: Maybe (MLSKeysByPurpose MLSPrivateKeys), + federationProtocols :: Maybe [ProtocolTag], + legalholdDefaults :: FeatureDefaults LegalholdConfig, + maxConvSize :: Word16 + } + interpretConversationSubsystem :: ( Member (Error FederationError) r, Member BackendNotificationQueueAccess r, @@ -130,6 +140,3 @@ pushConversationEvent conn st e lusers bots = do recipients = map userRecipient (tUnqualified users), isCellsEvent = shouldPushToCells st e } - - userRecipient :: UserId -> Recipient - userRecipient u = Recipient {recipientUserId = u, recipientClients = PushV2.RecipientClientsAll} diff --git a/libs/wire-subsystems/src/Wire/DomainVerificationChallengeStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/DomainVerificationChallengeStore/Cassandra.hs index 03ba7a61013..44ed929a560 100644 --- a/libs/wire-subsystems/src/Wire/DomainVerificationChallengeStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/DomainVerificationChallengeStore/Cassandra.hs @@ -34,8 +34,7 @@ import Wire.DomainVerificationChallengeStore interpretDomainVerificationChallengeStoreToCassandra :: forall r. - ( Member (Embed IO) r - ) => + (Member (Embed IO) r) => ClientState -> Timeout -> InterpreterFor DomainVerificationChallengeStore r diff --git a/libs/wire-subsystems/src/Wire/EnterpriseLoginSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/EnterpriseLoginSubsystem/Interpreter.hs index 1f203564ed9..54539dcd495 100644 --- a/libs/wire-subsystems/src/Wire/EnterpriseLoginSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/EnterpriseLoginSubsystem/Interpreter.hs @@ -559,8 +559,7 @@ sendAuditMail url subject mBefore mAfter = do url <> " called;\nOld value:\n" <> fromLazyText - ( LT.decodeUtf8 (encodeDomainRegistrationPretty mBefore) - ) + (LT.decodeUtf8 (encodeDomainRegistrationPretty mBefore)) <> "\nNew value:\n" <> fromLazyText ( LT.decodeUtf8 diff --git a/libs/wire-subsystems/src/Wire/Error.hs b/libs/wire-subsystems/src/Wire/Error.hs index d5505e9ae2e..b526bb6c9da 100644 --- a/libs/wire-subsystems/src/Wire/Error.hs +++ b/libs/wire-subsystems/src/Wire/Error.hs @@ -35,6 +35,7 @@ import Network.HTTP.Types import Network.Wai import Network.Wai.Utilities import Network.Wai.Utilities.Error qualified as Wai +import Network.Wai.Utilities.Exception import Network.Wai.Utilities.JSONResponse import Network.Wai.Utilities.Server import Servant (ServerError (..)) @@ -86,9 +87,9 @@ postgresUsageErrorToHttpError err = case err of -- return "404 not found", not "database crashed"? -- The problem is that the SessionError is not typed to easily be parsed -- To prevent foreign key errors we should check the foreign key constraints before inserting - StdError (Wai.mkError status500 "server-error" (LT.pack $ "postgres: " <> show err)) - ConnectionUsageError _ -> StdError (Wai.mkError status500 "server-error" (LT.pack $ "postgres: " <> show err)) - AcquisitionTimeoutUsageError -> StdError (Wai.mkError status500 "server-error" (LT.pack $ "postgres: " <> show err)) + StdError (Wai.mkError status500 "server-error" (LT.pack $ "postgres: " <> displayExceptionNoBacktrace err)) + ConnectionUsageError _ -> StdError (Wai.mkError status500 "server-error" (LT.pack $ "postgres: " <> displayExceptionNoBacktrace err)) + AcquisitionTimeoutUsageError -> StdError (Wai.mkError status500 "server-error" (LT.pack $ "postgres: " <> displayExceptionNoBacktrace err)) -- | Extract the wai error from an HttpError and convert into a -- servant error. `RichError` extra data is discarded! diff --git a/libs/wire-subsystems/src/Wire/ExternalAccess/External.hs b/libs/wire-subsystems/src/Wire/ExternalAccess/External.hs index cd0e67465ff..768f37f141f 100644 --- a/libs/wire-subsystems/src/Wire/ExternalAccess/External.hs +++ b/libs/wire-subsystems/src/Wire/ExternalAccess/External.hs @@ -188,7 +188,7 @@ deliver env pp = mapM (Async.async . exec) pp >>= foldM evaluate [] . zip (map f field "provider" (toByteString (s ^. serviceRefProvider)) ~~ field "service" (toByteString (s ^. serviceRefId)) ~~ field "bot" (toByteString (botMemId b)) - ~~ field "error" (show ex) + ~~ field "error" (displayException ex) ~~ msg (val "External delivery failure") pure gone Nothing -> do diff --git a/libs/wire-subsystems/src/Wire/FederationAPIAccess.hs b/libs/wire-subsystems/src/Wire/FederationAPIAccess.hs index 673e7f1d611..4d457295ec5 100644 --- a/libs/wire-subsystems/src/Wire/FederationAPIAccess.hs +++ b/libs/wire-subsystems/src/Wire/FederationAPIAccess.hs @@ -33,20 +33,16 @@ data FederationAPIAccess (fedM :: Component -> Type -> Type) m a where Remote x -> fedM c a -> FederationAPIAccess fedM m (Either FederationError a) - RunFederatedConcurrently :: - forall (c :: Component) f a m x fedM. - (KnownComponent c, Foldable f) => - f (Remote x) -> - (Remote x -> fedM c a) -> - FederationAPIAccess fedM m [Either (Remote x, FederationError) (Remote a)] - -- | An action similar to 'RunFederatedConcurrently', but the input is - -- bucketed by domain before the RPCs are sent to the remote backends. - RunFederatedBucketed :: - forall (c :: Component) f a m x fedM. + RunFederatedConcurrentlyEither :: (KnownComponent c, Foldable f, Functor f) => f (Remote x) -> (Remote [x] -> fedM c a) -> FederationAPIAccess fedM m [Either (Remote [x], FederationError) (Remote a)] + RunFederatedConcurrentlyBucketsEither :: + (KnownComponent c, Foldable f) => + f (Remote x) -> + (Remote x -> fedM c a) -> + FederationAPIAccess fedM m [Either (Remote x, FederationError) (Remote a)] IsFederationConfigured :: FederationAPIAccess fedM m Bool makeSem ''FederationAPIAccess @@ -61,3 +57,18 @@ runFederated :: fedM c a -> Sem r a runFederated rx c = runFederatedEither rx c >>= fromEither + +runFederatedConcurrently :: + forall c fedM f x a r. + ( Member (FederationAPIAccess fedM) r, + Member (Error FederationError) r, + KnownComponent c, + Foldable f, + Functor f + ) => + f (Remote x) -> + (Remote [x] -> fedM c a) -> + Sem r [Remote a] +runFederatedConcurrently rx c = do + results <- runFederatedConcurrentlyEither rx c + fromEither $ mapLeft snd $ sequence results diff --git a/libs/wire-subsystems/src/Wire/FederationAPIAccess/Interpreter.hs b/libs/wire-subsystems/src/Wire/FederationAPIAccess/Interpreter.hs index 737cee6529e..47e1030779b 100644 --- a/libs/wire-subsystems/src/Wire/FederationAPIAccess/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/FederationAPIAccess/Interpreter.hs @@ -81,8 +81,8 @@ interpretFederationAPIAccessGeneral runFedM isFederationConfigured = interpret $ \case RunFederatedEither remote rpc -> runFederatedEither runFedM remote rpc - RunFederatedConcurrently remotes rpc -> runFederatedConcurrently runFedM remotes rpc - RunFederatedBucketed remotes rpc -> runFederatedBucketed runFedM remotes rpc + RunFederatedConcurrentlyEither remotes rpc -> runFederatedConcurrently runFedM remotes rpc + RunFederatedConcurrentlyBucketsEither remotes rpc -> runFederatedBucketed runFedM remotes rpc IsFederationConfigured -> isFederationConfigured runFederatedEither :: @@ -95,25 +95,25 @@ runFederatedEither runFedM (tDomain -> remoteDomain) rpc = runFederatedConcurrently :: ( Foldable f, - Member (Concurrency 'Unsafe) r + Member (Concurrency 'Unsafe) r, + Functor f ) => FederatedActionRunner fedM r -> f (Remote a) -> - (Remote a -> fedM c b) -> - Sem r [Either (Remote a, FederationError) (Remote b)] + (Remote [a] -> fedM c b) -> + Sem r [Either (Remote [a], FederationError) (Remote b)] runFederatedConcurrently runFedM xs rpc = - unsafePooledForConcurrentlyN 8 (toList xs) $ \r -> + unsafePooledForConcurrentlyN 8 (bucketRemote xs) $ \r -> bimap (r,) (qualifyAs r) <$> runFederatedEither runFedM r (rpc r) runFederatedBucketed :: ( Foldable f, - Functor f, Member (Concurrency 'Unsafe) r ) => FederatedActionRunner fedM r -> f (Remote a) -> - (Remote [a] -> fedM c b) -> - Sem r [Either (Remote [a], FederationError) (Remote b)] + (Remote a -> fedM c b) -> + Sem r [Either (Remote a, FederationError) (Remote b)] runFederatedBucketed runFedM xs rpc = - unsafePooledForConcurrentlyN 8 (bucketRemote xs) $ \r -> + unsafePooledForConcurrentlyN 8 (toList xs) $ \r -> bimap (r,) (qualifyAs r) <$> runFederatedEither runFedM r (rpc r) diff --git a/libs/wire-subsystems/src/Wire/FederationConfigStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/FederationConfigStore/Cassandra.hs index 2038fc697ed..fcae3c1261a 100644 --- a/libs/wire-subsystems/src/Wire/FederationConfigStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/FederationConfigStore/Cassandra.hs @@ -47,8 +47,7 @@ import Wire.FederationConfigStore -- In the future the config file will be removed and the database will be the only source of truth. interpretFederationDomainConfig :: forall r a. - ( Member (Embed IO) r - ) => + (Member (Embed IO) r) => ClientState -> Maybe FederationStrategy -> Map Domain FederationDomainConfig -> diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs index fb2544f7066..52b53465d5a 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs @@ -1,19 +1,3 @@ --- 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 #-} -- This file is part of the Wire Server implementation. @@ -99,7 +83,7 @@ data GalleyAPIAccess m a where UserId -> TeamId -> GalleyAPIAccess m (Maybe Team.TeamMember) - GetTeamMembers :: + GetTeamMembersWithLimit :: TeamId -> Maybe (Range 1 Team.HardTruncationLimit Int32) -> GalleyAPIAccess m Team.TeamMemberList @@ -107,6 +91,10 @@ data GalleyAPIAccess m a where TeamId -> [UserId] -> GalleyAPIAccess m Team.TeamMemberInfoList + SelectTeamMembers :: + TeamId -> + [UserId] -> + GalleyAPIAccess m [Team.TeamMember] GetTeamId :: UserId -> GalleyAPIAccess m (Maybe TeamId) @@ -129,6 +117,11 @@ data GalleyAPIAccess m a where Team.TeamStatus -> Maybe Currency.Alpha -> GalleyAPIAccess m () + FinalizeDeleteTeam :: + Local UserId -> + Maybe ConnId -> + TeamId -> + GalleyAPIAccess m () MemberIsTeamOwner :: TeamId -> UserId -> diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs index bcce4a24488..86b070bd836 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs @@ -78,8 +78,9 @@ interpretGalleyAPIAccessToRpc disabledVersions galleyEndpoint = AddTeamMember id' id'' a b -> addTeamMember id' id'' a b CreateTeam id' bnt id'' -> createTeam id' bnt id'' GetTeamMember id' id'' -> getTeamMember id' id'' - GetTeamMembers tid maxResults -> getTeamMembers tid maxResults + GetTeamMembersWithLimit tid maxResults -> getTeamMembersWithLimit tid maxResults SelectTeamMemberInfos tid uids -> selectTeamMemberInfos tid uids + SelectTeamMembers tid uids -> selectTeamMembers tid uids GetTeamId id' -> getTeamId id' GetTeam id' -> getTeam id' GetTeamName id' -> getTeamName id' @@ -88,6 +89,7 @@ interpretGalleyAPIAccessToRpc disabledVersions galleyEndpoint = GetFeatureConfigForTeam tid -> getFeatureConfigForTeam tid GetUserLegalholdStatus id' tid -> getUserLegalholdStatus id' tid ChangeTeamStatus id' ts m_al -> changeTeamStatus id' ts m_al + FinalizeDeleteTeam lusr mconn tid -> finalizeDeleteTeam lusr mconn tid MemberIsTeamOwner id' id'' -> memberIsTeamOwner id' id'' GetAllTeamFeaturesForUser m_id' -> getAllTeamFeaturesForUser m_id' GetVerificationCodeEnabled id' -> getVerificationCodeEnabled id' @@ -338,7 +340,7 @@ getTeamMember u tid = do -- | TODO: is now truncated. this is (only) used for team suspension / unsuspension, which -- 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 :: +getTeamMembersWithLimit :: ( Member (Error ParseException) r, Member Rpc r, Member (Input Endpoint) r, @@ -347,7 +349,7 @@ getTeamMembers :: TeamId -> Maybe (Range 1 HardTruncationLimit Int32) -> Sem r TeamMemberList -getTeamMembers tid maxResults = do +getTeamMembersWithLimit tid maxResults = do debug $ remote "galley" . msg (val "Get team members") galleyRequest req >>= decodeBodyOrThrow "galley" where @@ -378,6 +380,25 @@ selectTeamMemberInfos tid uids = do . lbytes (encode bdy) . expect2xx +selectTeamMembers :: + ( Member (Error ParseException) r, + Member Rpc r, + Member (Input Endpoint) r + ) => + TeamId -> + [UserId] -> + Sem r [TeamMember] +selectTeamMembers tid uids = do + let bdy = UserIds uids + galleyRequest (req bdy) >>= decodeBodyOrThrow "galley" + where + req bdy = + method POST + . paths ["i", "teams", toByteString' tid, "members", "get-by-ids"] + . header "Content-Type" "application/json" + . lbytes (encode bdy) + . expect2xx + getTeamAdmins :: ( Member (Error ParseException) r, Member Rpc r, @@ -577,6 +598,24 @@ changeTeamStatus tid s cur = do . expect2xx . lbytes (encode $ Team.TeamStatusUpdate s cur) +finalizeDeleteTeam :: + ( Member Rpc r, + Member (Input Endpoint) r + ) => + Local UserId -> + Maybe ConnId -> + TeamId -> + Sem r () +finalizeDeleteTeam lusr mconn tid = do + void $ galleyRequest req + where + req = + method POST + . paths ["i", "teams", toByteString' tid, "finalize-delete"] + . zUser (tUnqualified lusr) + . maybe id (header "Z-Connection" . fromConnId) mconn + . expect2xx + getTeamExposeInvitationURLsToTeamAdmin :: ( Member Rpc r, Member (Input Endpoint) r, diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs index 05241eadcdc..320199e48d1 100644 --- a/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs @@ -76,8 +76,7 @@ interpretIndexedUserStoreES cfg = GetTeamSize tid -> getTeamSizeImpl cfg tid getTeamSizeImpl :: - ( Member (Embed IO) r - ) => + (Member (Embed IO) r) => IndexedUserStoreConfig -> TeamId -> Sem r TeamSize diff --git a/services/galley/src/Galley/Effects/LegalHoldStore.hs b/libs/wire-subsystems/src/Wire/LegalHoldStore.hs similarity index 51% rename from services/galley/src/Galley/Effects/LegalHoldStore.hs rename to libs/wire-subsystems/src/Wire/LegalHoldStore.hs index 0cea5e4298b..66f675cb49c 100644 --- a/services/galley/src/Galley/Effects/LegalHoldStore.hs +++ b/libs/wire-subsystems/src/Wire/LegalHoldStore.hs @@ -1,46 +1,7 @@ {-# LANGUAGE TemplateHaskell #-} --- 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.LegalHoldStore where -module Galley.Effects.LegalHoldStore - ( -- * LegalHold store effect - LegalHoldStore (..), - - -- * Store actions - createSettings, - getSettings, - removeSettings, - insertPendingPrekeys, - selectPendingPrekeys, - dropPendingPrekeys, - setUserLegalHoldStatus, - setTeamLegalholdWhitelisted, - unsetTeamLegalholdWhitelisted, - isTeamLegalholdWhitelisted, - validateServiceKey, - - -- * Intra actions - makeVerifiedRequest, - makeVerifiedRequestFreshManager, - ) -where - -import Brig.Types.Team.LegalHold import Data.ByteString.Lazy.Char8 qualified as LC8 import Data.Id import Data.LegalHold @@ -49,6 +10,7 @@ import Imports import Network.HTTP.Client qualified as Http import Polysemy import Wire.API.Provider.Service +import Wire.API.Team.LegalHold.Internal import Wire.API.User.Client.Prekey data LegalHoldStore m a where @@ -62,7 +24,6 @@ data LegalHoldStore m a where SetTeamLegalholdWhitelisted :: TeamId -> LegalHoldStore m () UnsetTeamLegalholdWhitelisted :: TeamId -> LegalHoldStore m () IsTeamLegalholdWhitelisted :: TeamId -> LegalHoldStore m Bool - -- intra actions MakeVerifiedRequestFreshManager :: Fingerprint Rsa -> HttpsUrl -> diff --git a/libs/wire-subsystems/src/Wire/LegalHoldStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/LegalHoldStore/Cassandra.hs new file mode 100644 index 00000000000..e0d473b5b79 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/LegalHoldStore/Cassandra.hs @@ -0,0 +1,148 @@ +module Wire.LegalHoldStore.Cassandra (interpretLegalHoldStoreToCassandra, validateServiceKey) where + +import Cassandra +import Control.Exception (catch) +import Data.ByteString.Conversion.To +import Data.ByteString.Lazy.Char8 qualified as LC8 +import Data.Id +import Data.LegalHold +import Data.Misc +import Galley.Types.Teams (FeatureDefaults (..)) +import Imports +import OpenSSL.EVP.Digest qualified as SSL +import OpenSSL.EVP.PKey qualified as SSL +import OpenSSL.PEM qualified as SSL +import OpenSSL.RSA qualified as SSL +import Polysemy +import Polysemy.Input +import Polysemy.TinyLog +import Ssl.Util qualified as SSL +import Wire.API.Provider.Service +import Wire.API.Team.Feature (LegalholdConfig) +import Wire.API.Team.LegalHold.Internal +import Wire.API.User.Client.Prekey +import Wire.LegalHoldStore (LegalHoldStore (..)) +import Wire.LegalHoldStore.Cassandra.Queries qualified as Q +import Wire.LegalHoldStore.Env (LegalHoldEnv (..)) +import Wire.TeamStore.Cassandra.Queries qualified as QTS +import Wire.Util (embedClientInput, logEffect) + +interpretLegalHoldStoreToCassandra :: + ( Member (Embed IO) r, + Member (Input ClientState) r, + Member (Input LegalHoldEnv) r, + Member TinyLog r + ) => + FeatureDefaults LegalholdConfig -> + Sem (LegalHoldStore ': r) a -> + Sem r a +interpretLegalHoldStoreToCassandra lh = interpret $ \case + CreateSettings s -> do + logEffect "LegalHoldStore.CreateSettings" + embedClientInput $ createSettings s + GetSettings tid -> do + logEffect "LegalHoldStore.GetSettings" + embedClientInput $ getSettings tid + RemoveSettings tid -> do + logEffect "LegalHoldStore.RemoveSettings" + embedClientInput $ removeSettings tid + InsertPendingPrekeys uid pkeys -> do + logEffect "LegalHoldStore.InsertPendingPrekeys" + embedClientInput $ insertPendingPrekeys uid pkeys + SelectPendingPrekeys uid -> do + logEffect "LegalHoldStore.SelectPendingPrekeys" + embedClientInput $ selectPendingPrekeys uid + DropPendingPrekeys uid -> do + logEffect "LegalHoldStore.DropPendingPrekeys" + embedClientInput $ dropPendingPrekeys uid + SetUserLegalHoldStatus tid uid st -> do + logEffect "LegalHoldStore.SetUserLegalHoldStatus" + embedClientInput $ setUserLegalHoldStatus tid uid st + SetTeamLegalholdWhitelisted tid -> do + logEffect "LegalHoldStore.SetTeamLegalholdWhitelisted" + embedClientInput $ setTeamLegalholdWhitelisted tid + UnsetTeamLegalholdWhitelisted tid -> do + logEffect "LegalHoldStore.UnsetTeamLegalholdWhitelisted" + embedClientInput $ unsetTeamLegalholdWhitelisted tid + IsTeamLegalholdWhitelisted tid -> do + logEffect "LegalHoldStore.IsTeamLegalholdWhitelisted" + embedClientInput $ isTeamLegalholdWhitelisted lh tid + MakeVerifiedRequestFreshManager fpr url r -> do + logEffect "LegalHoldStore.MakeVerifiedRequestFreshManager" + env <- input + embed @IO $ makeVerifiedRequestFreshManager env fpr url r + MakeVerifiedRequest fpr url r -> do + logEffect "LegalHoldStore.MakeVerifiedRequest" + env <- input + embed @IO $ makeVerifiedRequest env fpr url r + ValidateServiceKey sk -> do + logEffect "LegalHoldStore.ValidateServiceKey" + embed @IO $ validateServiceKey sk + +createSettings :: (MonadClient m) => LegalHoldService -> m () +createSettings (LegalHoldService tid url fpr tok key) = + retry x1 $ write Q.insertLegalHoldSettings (params LocalQuorum (url, fpr, tok, key, tid)) + +getSettings :: (MonadClient m) => TeamId -> m (Maybe LegalHoldService) +getSettings tid = fmap toLegalHoldService <$> retry x1 (query1 Q.selectLegalHoldSettings (params LocalQuorum (Identity tid))) + where + toLegalHoldService (httpsUrl, fingerprint, tok, key) = LegalHoldService tid httpsUrl fingerprint tok key + +removeSettings :: (MonadClient m) => TeamId -> m () +removeSettings tid = retry x5 (write Q.removeLegalHoldSettings (params LocalQuorum (Identity tid))) + +insertPendingPrekeys :: (MonadClient m) => UserId -> [Prekey] -> m () +insertPendingPrekeys uid keys = retry x5 . batch $ do + forM_ keys $ \(Prekey keyId key) -> addPrepQuery Q.insertPendingPrekeys (uid, keyId, key) + +selectPendingPrekeys :: (MonadClient m) => UserId -> m (Maybe ([Prekey], LastPrekey)) +selectPendingPrekeys uid = pickLastKey . fmap fromTuple <$> retry x1 (query Q.selectPendingPrekeys (params LocalQuorum (Identity uid))) + where + fromTuple (keyId, key) = Prekey keyId key + pickLastKey allPrekeys = case unsnoc allPrekeys of + Nothing -> Nothing + Just (keys, lst) -> pure (keys, lastPrekey . prekeyKey $ lst) + +dropPendingPrekeys :: (MonadClient m) => UserId -> m () +dropPendingPrekeys uid = retry x5 (write Q.dropPendingPrekeys (params LocalQuorum (Identity uid))) + +setUserLegalHoldStatus :: (MonadClient m) => TeamId -> UserId -> UserLegalHoldStatus -> m () +setUserLegalHoldStatus tid uid status = retry x5 (write Q.updateUserLegalHoldStatus (params LocalQuorum (status, tid, uid))) + +setTeamLegalholdWhitelisted :: (MonadClient m) => TeamId -> m () +setTeamLegalholdWhitelisted tid = retry x5 (write Q.insertLegalHoldWhitelistedTeam (params LocalQuorum (Identity tid))) + +unsetTeamLegalholdWhitelisted :: (MonadClient m) => TeamId -> m () +unsetTeamLegalholdWhitelisted tid = retry x5 (write Q.removeLegalHoldWhitelistedTeam (params LocalQuorum (Identity tid))) + +isTeamLegalholdWhitelisted :: FeatureDefaults LegalholdConfig -> TeamId -> Client Bool +isTeamLegalholdWhitelisted FeatureLegalHoldDisabledPermanently _ = pure False +isTeamLegalholdWhitelisted FeatureLegalHoldDisabledByDefault _ = pure False +isTeamLegalholdWhitelisted FeatureLegalHoldWhitelistTeamsAndImplicitConsent tid = + isJust <$> (runIdentity <$$> retry x5 (query1 QTS.selectLegalHoldWhitelistedTeam (params LocalQuorum (Identity tid)))) + +validateServiceKey :: (MonadIO m) => ServiceKeyPEM -> m (Maybe (ServiceKey, Fingerprint Rsa)) +validateServiceKey pem = + liftIO $ + readPublicKey >>= \pk -> + case SSL.toPublicKey =<< pk of + Nothing -> pure Nothing + Just pk' -> do + Just sha <- SSL.getDigestByName "SHA256" + let size = SSL.rsaSize (pk' :: SSL.RSAPubKey) + if size < minRsaKeySize + then pure Nothing + else do + fpr <- Fingerprint <$> SSL.rsaFingerprint sha pk' + let bits = fromIntegral size * 8 + let key = ServiceKey RsaServiceKey bits pem + pure (Just (key, fpr)) + where + readPublicKey = + ( do + pk <- SSL.readPublicKey (LC8.unpack (toByteString pem)) + pure (Just pk) + ) + `catch` (\(_ :: SomeException) -> pure Nothing) + minRsaKeySize :: Int + minRsaKeySize = 256 diff --git a/libs/wire-subsystems/src/Wire/LegalHoldStore/Cassandra/Queries.hs b/libs/wire-subsystems/src/Wire/LegalHoldStore/Cassandra/Queries.hs new file mode 100644 index 00000000000..97c072a8903 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/LegalHoldStore/Cassandra/Queries.hs @@ -0,0 +1,75 @@ +module Wire.LegalHoldStore.Cassandra.Queries where + +import Cassandra as C +import Data.Functor.Identity (Identity) +import Data.Id +import Data.LegalHold +import Data.Misc +import Data.Text (Text) +import Text.RawString.QQ +import Wire.API.Provider.Service +import Wire.API.User.Client.Prekey + +insertLegalHoldSettings :: PrepQuery W (HttpsUrl, Fingerprint Rsa, ServiceToken, ServiceKey, TeamId) () +insertLegalHoldSettings = + [r| + update legalhold_service + set base_url = ?, + fingerprint = ?, + auth_token = ?, + pubkey = ? + where team_id = ? + |] + +selectLegalHoldSettings :: PrepQuery R (Identity TeamId) (HttpsUrl, Fingerprint Rsa, ServiceToken, ServiceKey) +selectLegalHoldSettings = + [r| + select base_url, fingerprint, auth_token, pubkey + from legalhold_service + where team_id = ? + |] + +removeLegalHoldSettings :: PrepQuery W (Identity TeamId) () +removeLegalHoldSettings = "delete from legalhold_service where team_id = ?" + +insertPendingPrekeys :: PrepQuery W (UserId, PrekeyId, Text) () +insertPendingPrekeys = + [r| + insert into legalhold_pending_prekeys (user, key, data) values (?, ?, ?) + |] + +dropPendingPrekeys :: PrepQuery W (Identity UserId) () +dropPendingPrekeys = + [r| + delete from legalhold_pending_prekeys + where user = ? + |] + +selectPendingPrekeys :: PrepQuery R (Identity UserId) (PrekeyId, Text) +selectPendingPrekeys = + [r| + select key, data + from legalhold_pending_prekeys + where user = ? + order by key asc + |] + +updateUserLegalHoldStatus :: PrepQuery W (UserLegalHoldStatus, TeamId, UserId) () +updateUserLegalHoldStatus = + [r| + update team_member + set legalhold_status = ? + where team = ? and user = ? + |] + +insertLegalHoldWhitelistedTeam :: PrepQuery W (Identity TeamId) () +insertLegalHoldWhitelistedTeam = + [r| + insert into legalhold_whitelisted (team) values (?) + |] + +removeLegalHoldWhitelistedTeam :: PrepQuery W (Identity TeamId) () +removeLegalHoldWhitelistedTeam = + [r| + delete from legalhold_whitelisted where team = ? + |] diff --git a/libs/wire-subsystems/src/Wire/LegalHoldStore/Env.hs b/libs/wire-subsystems/src/Wire/LegalHoldStore/Env.hs new file mode 100644 index 00000000000..17926ebce6c --- /dev/null +++ b/libs/wire-subsystems/src/Wire/LegalHoldStore/Env.hs @@ -0,0 +1,11 @@ +module Wire.LegalHoldStore.Env where + +import Data.ByteString.Lazy.Char8 qualified as LC8 +import Data.Misc +import Imports +import Network.HTTP.Client qualified as Http + +data LegalHoldEnv = LegalHoldEnv + { makeVerifiedRequest :: Fingerprint Rsa -> HttpsUrl -> (Http.Request -> Http.Request) -> IO (Http.Response LC8.ByteString), + makeVerifiedRequestFreshManager :: Fingerprint Rsa -> HttpsUrl -> (Http.Request -> Http.Request) -> IO (Http.Response LC8.ByteString) + } diff --git a/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs b/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs index 2ffe5d13b22..750d90885a9 100644 --- a/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs @@ -20,6 +20,7 @@ module Wire.NotificationSubsystem where import Control.Concurrent.Async (Async) +import Control.Lens (view) import Data.Aeson import Data.Default import Data.Id @@ -28,7 +29,11 @@ import Polysemy import Wire.API.Event.Conversation import Wire.API.Federation.API.Galley.Notifications (ConversationUpdate) import Wire.API.Push.V2 hiding (Push (..), Recipient, newPush) +import Wire.API.Push.V2 qualified as PushV2 +import Wire.API.Team.Member +import Wire.API.Team.Member qualified as Mem import Wire.Arbitrary +import Wire.StoredConversation (LocalMember (..)) data Recipient = Recipient { recipientUserId :: UserId, @@ -37,6 +42,16 @@ data Recipient = Recipient deriving stock (Show, Ord, Eq, Generic) deriving (Arbitrary) via GenericUniform Recipient +userRecipient :: UserId -> Recipient +userRecipient u = Recipient u PushV2.RecipientClientsAll + +membersToRecipients :: Maybe UserId -> [TeamMember] -> [Recipient] +membersToRecipients Nothing = map (userRecipient . view Mem.userId) +membersToRecipients (Just u) = map userRecipient . filter (/= u) . map (view Mem.userId) + +localMemberToRecipient :: LocalMember -> Recipient +localMemberToRecipient = userRecipient . (.id_) + data Push = Push { conn :: Maybe ConnId, transient :: Bool, diff --git a/libs/wire-subsystems/src/Wire/PaginationState.hs b/libs/wire-subsystems/src/Wire/PaginationState.hs index 97baeb76c65..d5bf5562c08 100644 --- a/libs/wire-subsystems/src/Wire/PaginationState.hs +++ b/libs/wire-subsystems/src/Wire/PaginationState.hs @@ -18,33 +18,18 @@ module Wire.PaginationState ( PaginationState (..), paginationClause, - mkPaginationState, ) where -import Data.Time.Clock import Imports hiding (sortBy) -import Wire.API.Pagination +import Wire.API.UserGroup.Pagination import Wire.Postgres -data PaginationState a - = PaginationSortByName (Maybe (Text, a)) - | PaginationSortByCreatedAt (Maybe (UTCTime, a)) - paginationClause :: (PostgresValue a) => PaginationState a -> Maybe Clause paginationClause s = case s of PaginationSortByName (Just (name, x)) -> Just (mkClause "name" name <> mkClause "id" x) PaginationSortByCreatedAt (Just (createdAt, x)) -> Just (mkClause "created_at" createdAt <> mkClause "id" x) + PaginationOffset _ -> Nothing _ -> Nothing - -mkPaginationState :: - SortBy -> - Maybe Text -> - Maybe UTCTime -> - Maybe a -> - PaginationState a -mkPaginationState sortBy name createdAt x = case sortBy of - SortByName -> PaginationSortByName $ (,) <$> name <*> x - SortByCreatedAt -> PaginationSortByCreatedAt $ (,) <$> createdAt <*> x diff --git a/libs/wire-subsystems/src/Wire/Postgres.hs b/libs/wire-subsystems/src/Wire/Postgres.hs index 4629e56dbdd..84cca14b183 100644 --- a/libs/wire-subsystems/src/Wire/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/Postgres.hs @@ -38,6 +38,7 @@ module Wire.Postgres -- * Runners runStatement, + runSession, runTransaction, runPipeline, parseCount, @@ -56,6 +57,7 @@ module Wire.Postgres clause1, orderBy, limit, + offset, buildStatement, -- * Type classes @@ -91,14 +93,21 @@ type PGConstraints r = Member (Error Hasql.UsageError) r ) +runSession :: + (PGConstraints r) => + Session a -> + Sem r a +runSession sess = do + pool <- input + liftIO (use pool sess) >>= either throw pure + runStatement :: (PGConstraints r) => a -> Statement a b -> Sem r b -runStatement a stmt = do - pool <- input - liftIO (use pool (statement a stmt)) >>= either throw pure +runStatement a stmt = + runSession $ statement a stmt runTransaction :: (PGConstraints r) => @@ -106,17 +115,15 @@ runTransaction :: Mode -> Transaction a -> Sem r a -runTransaction isolationLevel mode t = do - pool <- input - liftIO (use pool $ Transaction.transaction isolationLevel mode t) >>= either throw pure +runTransaction isolationLevel mode t = + runSession $ Transaction.transaction isolationLevel mode t runPipeline :: (PGConstraints r) => Pipeline a -> Sem r a -runPipeline p = do - pool <- input - liftIO (use pool $ pipeline p) >>= either throw pure +runPipeline p = + runSession $ pipeline p class PostgresValue a where postgresType :: Text @@ -275,6 +282,10 @@ limit :: forall a. (PostgresValue a) => a -> QueryFragment limit n = paramLiteral (valueEncoder n) $ \i -> "limit " <> argPattern (postgresType @a) i +offset :: forall a. (PostgresValue a) => a -> QueryFragment +offset n = paramLiteral (valueEncoder n) $ \i -> + "offset " <> argPattern (postgresType @a) i + buildStatement :: QueryFragment -> Dec.Result b -> Statement () b buildStatement frag dec = Statement diff --git a/services/galley/src/Galley/Effects/ProposalStore.hs b/libs/wire-subsystems/src/Wire/ProposalStore.hs similarity index 97% rename from services/galley/src/Galley/Effects/ProposalStore.hs rename to libs/wire-subsystems/src/Wire/ProposalStore.hs index cf549d576c3..130a2b2a0af 100644 --- a/services/galley/src/Galley/Effects/ProposalStore.hs +++ b/libs/wire-subsystems/src/Wire/ProposalStore.hs @@ -17,7 +17,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Galley.Effects.ProposalStore where +module Wire.ProposalStore where import Imports import Polysemy diff --git a/services/galley/src/Galley/Cassandra/Proposal.hs b/libs/wire-subsystems/src/Wire/ProposalStore/Cassandra.hs similarity index 75% rename from services/galley/src/Galley/Cassandra/Proposal.hs rename to libs/wire-subsystems/src/Wire/ProposalStore/Cassandra.hs index c7675b6be32..a2c971f820f 100644 --- a/services/galley/src/Galley/Cassandra/Proposal.hs +++ b/libs/wire-subsystems/src/Wire/ProposalStore/Cassandra.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 Galley.Cassandra.Proposal +module Wire.ProposalStore.Cassandra ( interpretProposalStoreToCassandra, ProposalOrigin (..), ) @@ -23,18 +23,16 @@ where import Cassandra import Data.Timeout -import Galley.Cassandra.Store -import Galley.Cassandra.Util -import Galley.Effects.ProposalStore import Imports import Polysemy import Polysemy.Input -import Polysemy.TinyLog import Wire.API.MLS.Epoch import Wire.API.MLS.Group import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation import Wire.ConversationStore.Cassandra.Instances () +import Wire.ProposalStore +import Wire.Util (embedClient) -- | Proposals in the database expire after this timeout defaultTTL :: Timeout @@ -42,28 +40,27 @@ defaultTTL = 28 # Day interpretProposalStoreToCassandra :: ( Member (Embed IO) r, - Member (Input ClientState) r, - Member TinyLog r + Member (Input ClientState) r ) => Sem (ProposalStore ': r) a -> Sem r a interpretProposalStoreToCassandra = interpret $ \case StoreProposal groupId epoch ref origin raw -> do - logEffect "ProposalStore.StoreProposal" - embedClient . retry x5 $ + client <- input + embedClient client . retry x5 $ write (storeQuery defaultTTL) (params LocalQuorum (groupId, epoch, ref, origin, raw)) GetProposal groupId epoch ref -> do - logEffect "ProposalStore.GetProposal" - embedClient (runIdentity <$$> retry x1 (query1 getQuery (params LocalQuorum (groupId, epoch, ref)))) + client <- input + embedClient client (runIdentity <$$> retry x1 (query1 getQuery (params LocalQuorum (groupId, epoch, ref)))) GetAllPendingProposalRefs groupId epoch -> do - logEffect "ProposalStore.GetAllPendingProposalRefs" - embedClient (runIdentity <$$> retry x1 (query getAllPendingRef (params LocalQuorum (groupId, epoch)))) + client <- input + embedClient client (runIdentity <$$> retry x1 (query getAllPendingRef (params LocalQuorum (groupId, epoch)))) GetAllPendingProposals groupId epoch -> do - logEffect "ProposalStore.GetAllPendingProposals" - embedClient $ retry x1 (query getAllPending (params LocalQuorum (groupId, epoch))) + client <- input + embedClient client $ retry x1 (query getAllPending (params LocalQuorum (groupId, epoch))) DeleteAllProposals groupId -> do - logEffect "ProposalStore.DeleteAllProposals" - embedClient $ retry x5 (write deleteAllProposalsForGroup (params LocalQuorum (Identity groupId))) + client <- input + embedClient client $ retry x5 (write deleteAllProposalsForGroup (params LocalQuorum (Identity groupId))) storeQuery :: Timeout -> PrepQuery W (GroupId, Epoch, ProposalRef, ProposalOrigin, RawMLS Proposal) () storeQuery ttl = diff --git a/libs/wire-subsystems/src/Wire/ScimSubsystem.hs b/libs/wire-subsystems/src/Wire/ScimSubsystem.hs index 3f6d95da351..50ce62f3a6c 100644 --- a/libs/wire-subsystems/src/Wire/ScimSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/ScimSubsystem.hs @@ -20,6 +20,7 @@ module Wire.ScimSubsystem where import Data.Id +import Data.Int import Data.Maybe import Polysemy import Web.Scim.Class.Group qualified as SCG @@ -32,6 +33,6 @@ data ScimSubsystem m a where ScimGetUserGroup :: TeamId -> UserGroupId -> ScimSubsystem m (SCG.StoredGroup SparTag) ScimUpdateUserGroup :: TeamId -> UserGroupId -> SCG.Group -> ScimSubsystem m (SCG.StoredGroup SparTag) ScimDeleteUserGroup :: TeamId -> SCG.GroupId SparTag -> ScimSubsystem m () - ScimGetUserGroups :: TeamId -> Maybe Scim.Filter -> ScimSubsystem m (Scim.ListResponse (SCG.StoredGroup SparTag)) + ScimGetUserGroups :: TeamId -> Maybe Scim.Filter -> Maybe Int -> Maybe Int -> ScimSubsystem m (Scim.ListResponse (SCG.StoredGroup SparTag)) makeSem ''ScimSubsystem diff --git a/libs/wire-subsystems/src/Wire/ScimSubsystem/Error.hs b/libs/wire-subsystems/src/Wire/ScimSubsystem/Error.hs new file mode 100644 index 00000000000..1be5ccd0852 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/ScimSubsystem/Error.hs @@ -0,0 +1,47 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 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.ScimSubsystem.Error where + +import Data.Aeson +import Data.ByteString.Lazy as LBS +import Data.Id +import Data.Text as T +import Data.Text.Encoding as T +import Imports +import Network.Wai.Utilities.Error qualified as Wai +import Web.Scim.Schema.Error + +data ScimSubsystemError + = ScimSubsystemBadGroupName Text + | ScimSubsystemGroupNotFound UserGroupId + | ScimSubsystemUserNotFound UserId + | ScimSubsystemInvalidGroupMemberId Text + | ScimSubsystemGroupMembersNotFound [UserId] + | ScimSubsystemForbidden UserGroupId + | ScimSubsystemInternal Wai.Error + deriving (Show, Eq) + +scimSubsystemErrorToScimError :: ScimSubsystemError -> ScimError +scimSubsystemErrorToScimError = \case + ScimSubsystemBadGroupName bad -> badRequest InvalidValue (Just bad) + ScimSubsystemGroupNotFound bad -> notFound "Group" (idToText bad) + ScimSubsystemUserNotFound bad -> notFound "User" (idToText bad) + ScimSubsystemInvalidGroupMemberId bad -> badRequest InvalidValue (Just $ "Invalid group member ID: " <> bad) + ScimSubsystemGroupMembersNotFound bads -> badRequest InvalidValue (Just $ "These users do not exist, are not in your team, or not \"managed_by\" = \"scim\": " <> T.intercalate ", " (idToText <$> bads)) + ScimSubsystemForbidden bad -> forbidden ("Group is not managed by SCIM: " <> idToText bad) + ScimSubsystemInternal waiErr -> serverError (T.decodeUtf8 $ LBS.toStrict $ encode waiErr) diff --git a/libs/wire-subsystems/src/Wire/ScimSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/ScimSubsystem/Interpreter.hs index 96132f3f6a6..e3edf0d19da 100644 --- a/libs/wire-subsystems/src/Wire/ScimSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/ScimSubsystem/Interpreter.hs @@ -15,29 +15,32 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Wire.ScimSubsystem.Interpreter where +module Wire.ScimSubsystem.Interpreter + ( module Wire.ScimSubsystem.Error, + module Wire.ScimSubsystem.Interpreter, + ) +where import Data.Default import Data.Id import Data.Json.Util import Data.Set qualified as Set import Data.Text qualified as Text -import Data.UUID qualified as UUID import Data.Vector qualified as V import Imports import Network.HTTP.Types.Status (notFound404) import Network.URI (parseURI) -import Network.Wai.Utilities.Error qualified as Wai +import Network.Wai.Utilities.Error qualified as Error import Polysemy import Polysemy.Error import Polysemy.Input import Web.Scim.Class.Group qualified as SCG import Web.Scim.Filter qualified as Scim import Web.Scim.Schema.Common qualified as Common -import Web.Scim.Schema.Error import Web.Scim.Schema.ListResponse qualified as Scim import Web.Scim.Schema.Meta qualified as Meta import Web.Scim.Schema.ResourceType qualified as RT +import Web.Scim.Schema.Schema qualified as Scim import Wire.API.Routes.Internal.Brig import Wire.API.User import Wire.API.User.Scim (SparTag) @@ -46,7 +49,7 @@ import Wire.API.UserGroup.Pagination import Wire.BrigAPIAccess (BrigAPIAccess) import Wire.BrigAPIAccess qualified as BrigAPI import Wire.ScimSubsystem -import Wire.UserGroupSubsystem.Interpreter (UserGroupSubsystemError (..)) +import Wire.ScimSubsystem.Error data ScimSubsystemConfig = ScimSubsystemConfig { scimBaseUri :: Common.URI @@ -61,21 +64,10 @@ interpretScimSubsystem :: interpretScimSubsystem = interpret $ \case ScimCreateUserGroup teamId scimGroup -> createScimGroupImpl teamId scimGroup ScimGetUserGroup tid gid -> scimGetUserGroupImpl tid gid - ScimGetUserGroups tid mbFilter -> scimGetUserGroupsImpl tid mbFilter + ScimGetUserGroups tid mbFilter startIndex mbCount -> scimGetUserGroupsImpl tid mbFilter startIndex mbCount ScimUpdateUserGroup teamId userGroupId scimGroup -> scimUpdateUserGroupImpl teamId userGroupId scimGroup ScimDeleteUserGroup teamId groupId -> deleteScimGroupImpl teamId groupId -data ScimSubsystemError - = ScimSubsystemError ScimError -- TODO: replace this with custom constructors. (also, where are ScimSubsystemInvalidGroupMemberId etc. translated back into ScimErrors?) - | ScimSubsystemInvalidGroupMemberId Text - | ScimSubsystemGroupMembersNotFound [UserId] - | ScimSubsystemInternal Wai.Error - | ScimSubsystemInternalError UserGroupSubsystemError - deriving (Show, Eq) - -scimThrow :: (Member (Error ScimSubsystemError) r) => ScimError -> Sem r a -scimThrow = throw . ScimSubsystemError - createScimGroupImpl :: forall r. ( Member (Input ScimSubsystemConfig) r, @@ -98,12 +90,12 @@ createScimGroupImpl teamId grp = do ugName <- userGroupNameFromText grp.displayName - & either (scimThrow . badRequest InvalidValue . Just) pure + & either (throw . ScimSubsystemBadGroupName) pure ugMemberIds <- let go :: SCG.Member -> Sem r UserId go m = parseIdFromText m.value - & either (scimThrow . badRequest InvalidValue . Just . Text.pack) pure + & either (throw . ScimSubsystemInvalidGroupMemberId . Text.pack) pure in go `mapM` grp.members let newGroup = NewUserGroup {name = ugName, members = V.fromList ugMemberIds} @@ -127,7 +119,7 @@ scimGetUserGroupImpl tid gid = do let includeChannels = False -- SCIM has no notion of channels. maybe groupNotFound returnStoredGroup =<< BrigAPI.getGroupInternal tid gid includeChannels where - groupNotFound = scimThrow $ notFound "Group" $ UUID.toText $ toUUID gid + groupNotFound = throw (ScimSubsystemGroupNotFound gid) returnStoredGroup g = do ScimSubsystemConfig scimBaseUri <- input pure $ toStoredGroup scimBaseUri g @@ -139,11 +131,23 @@ scimGetUserGroupsImpl :: ) => TeamId -> Maybe Scim.Filter -> + Maybe Int -> + Maybe Int -> Sem r (Scim.ListResponse (SCG.StoredGroup SparTag)) -scimGetUserGroupsImpl tid mbFilter = do - UserGroupPage {page} :: UserGroupPageWithMembers <- BrigAPI.getGroupsInternal tid mbFilter +scimGetUserGroupsImpl tid mbFilter mbStartIndex mbCount = do + let startIndex = fromIntegral (max 0 $ maybe 0 (\n -> n - 1) mbStartIndex) :: Word + mbCount' = fromIntegral . (max 0) <$> mbCount + UserGroupPage {page, total} :: UserGroupPageWithMembers <- BrigAPI.getGroupsInternal tid mbFilter (Just ManagedByScim) startIndex mbCount' ScimSubsystemConfig scimBaseUri <- input - pure . Scim.fromList $ toStoredGroup scimBaseUri <$> page + let page' = map (toStoredGroup scimBaseUri) page + pure $ + Scim.ListResponse + { schemas = [Scim.ListResponse20], + totalResults = total, + itemsPerPage = length page', + startIndex = fromIntegral startIndex + 1, + resources = page' + } scimUpdateUserGroupImpl :: forall r. @@ -159,11 +163,11 @@ scimUpdateUserGroupImpl teamId gid grp = do let includeChannels = False ug <- BrigAPI.getGroupInternal teamId gid includeChannels - >>= note (ScimSubsystemError $ notFound "Group" (UUID.toText $ gid.toUUID)) + >>= note (ScimSubsystemGroupNotFound gid) when (ug.managedBy /= ManagedByScim) do - scimThrow $ notFound "Group" (UUID.toText $ gid.toUUID) + throw (ScimSubsystemGroupNotFound gid) - ugName <- either (scimThrow . badRequest InvalidValue . Just) pure $ userGroupNameFromText grp.displayName + ugName <- either (throw . ScimSubsystemBadGroupName) pure $ userGroupNameFromText grp.displayName reqMemberIds <- for grp.members parseMember let currentSet = Set.fromList (toList (runIdentity ug.members)) @@ -179,18 +183,18 @@ scimUpdateUserGroupImpl teamId gid grp = do throw (ScimSubsystemGroupMembersNotFound notInTeamOrNotScim) case missing of [] -> pure () - (u : _) -> scimThrow $ notFound "User" (idToText u) + (u : _) -> throw (ScimSubsystemUserNotFound u) -- replace the members of the user group; propagate Brig errors BrigAPI.updateGroup (UpdateGroupInternalRequest teamId gid (Just ugName) (Just reqMemberIds)) >>= \case Right () -> pure () Left err -> if err.code == notFound404 - then scimThrow $ notFound "Group" (UUID.toText $ gid.toUUID) + then throw (ScimSubsystemGroupNotFound gid) else throw (ScimSubsystemInternal err) ScimSubsystemConfig scimBaseUri <- input - maybe (scimThrow $ notFound "Group" (UUID.toText $ gid.toUUID)) (pure . toStoredGroup scimBaseUri) + maybe (throw $ ScimSubsystemGroupNotFound gid) (pure . toStoredGroup scimBaseUri) =<< BrigAPI.getGroupInternal teamId gid includeChannels deleteScimGroupImpl :: @@ -205,7 +209,7 @@ deleteScimGroupImpl teamId groupId = do eResult <- BrigAPI.deleteGroupInternal ManagedByScim teamId groupId case eResult of Right () -> pure () - Left BrigAPI.DeleteGroupManagedManagedByMismatch -> scimThrow (forbidden "Cannot delete group not managed by SCIM") + Left BrigAPI.DeleteGroupManagedManagedByMismatch -> throw (ScimSubsystemForbidden groupId) toStoredGroup :: Common.URI -> UserGroup -> SCG.StoredGroup SparTag toStoredGroup scimBaseUri ug = Meta.WithMeta meta (Common.WithId ug.id_ sg) @@ -234,7 +238,7 @@ toStoredGroup scimBaseUri ug = Meta.WithMeta meta (Common.WithId ug.id_ sg) typ = "User", ref = Common.uriToText . mkLocation $ "/Users/" <> idToString uid } - | uid <- toList (runIdentity ug.members) + | uid <- toList (runIdentity ug.members) ] } diff --git a/libs/wire-subsystems/src/Wire/SparAPIAccess.hs b/libs/wire-subsystems/src/Wire/SparAPIAccess.hs index 7435ac2af55..b2df76bd01a 100644 --- a/libs/wire-subsystems/src/Wire/SparAPIAccess.hs +++ b/libs/wire-subsystems/src/Wire/SparAPIAccess.hs @@ -1,19 +1,3 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2025 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 #-} -- This file is part of the Wire Server implementation. @@ -37,9 +21,12 @@ module Wire.SparAPIAccess where import Data.Id import Polysemy +import Wire.API.User import Wire.API.User.IdentityProvider data SparAPIAccess m a where GetIdentityProviders :: TeamId -> SparAPIAccess m IdPList + DeleteTeam :: TeamId -> SparAPIAccess m () + LookupScimUserInfo :: UserId -> SparAPIAccess m ScimUserInfo makeSem ''SparAPIAccess diff --git a/libs/wire-subsystems/src/Wire/SparAPIAccess/Rpc.hs b/libs/wire-subsystems/src/Wire/SparAPIAccess/Rpc.hs index 5ded03c9398..fc08de1b81d 100644 --- a/libs/wire-subsystems/src/Wire/SparAPIAccess/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/SparAPIAccess/Rpc.hs @@ -30,6 +30,7 @@ import Polysemy.Input import Polysemy.TinyLog import System.Logger.Message import Util.Options +import Wire.API.User (ScimUserInfo) import Wire.API.User.IdentityProvider import Wire.ParseException import Wire.Rpc @@ -47,6 +48,8 @@ interpretSparAPIAccessToRpc sparEndpoint = interpret $ runInputConst sparEndpoint . \case GetIdentityProviders tid -> getIdentityProvidersImpl tid + DeleteTeam tid -> deleteTeamImpl tid + LookupScimUserInfo uid -> lookupScimUserInfoImpl uid sparRequest :: (Member Rpc r, Member (Input Endpoint) r) => @@ -75,6 +78,36 @@ getIdentityProvidersImpl tid = do method GET . paths ["i", "identity-providers", toByteString' tid] +-- | Notify Spar that a team is being deleted. +deleteTeamImpl :: + ( Member (Input Endpoint) r, + Member Rpc r + ) => + TeamId -> + Sem r () +deleteTeamImpl tid = do + void $ sparRequest delReq + where + delReq = + method DELETE + . paths ["i", "teams", toByteString' tid] + . expect2xx + +-- | Get the SCIM user info for a user. +lookupScimUserInfoImpl :: + ( Member (Error ParseException) r, + Member (Input Endpoint) r, + Member Rpc r + ) => + UserId -> + Sem r ScimUserInfo +lookupScimUserInfoImpl uid = do + decodeBodyOrThrow "spar" =<< sparRequest postReq + where + postReq = + method POST + . paths ["i", "scim", "userinfo", toByteString' uid] + -- FUTUREWORK: This is duplicated in Wire/GalleyAPIAccess/Rpc. Move to a common module. decodeBodyOrThrow :: forall a r. (Typeable a, FromJSON a, Member (Error ParseException) r) => Text -> Response (Maybe BL.ByteString) -> Sem r a decodeBodyOrThrow ctx r = either (throw . ParseException ctx) pure (responseJsonEither r) diff --git a/libs/wire-subsystems/src/Wire/StoredConversation.hs b/libs/wire-subsystems/src/Wire/StoredConversation.hs index 85ac05ac2cf..3de3e3a8f46 100644 --- a/libs/wire-subsystems/src/Wire/StoredConversation.hs +++ b/libs/wire-subsystems/src/Wire/StoredConversation.hs @@ -28,6 +28,7 @@ import Data.Qualified import Data.Set qualified as Set import Data.Time (UTCTime) import Data.UUID.Tagged qualified as U +import Galley.Types.Teams (isTeamMember) import Imports import Wire.API.Conversation import Wire.API.Conversation.CellsState @@ -37,6 +38,7 @@ import Wire.API.MLS.CipherSuite import Wire.API.MLS.Group.Serialisation qualified as MLS import Wire.API.MLS.SubConversation import Wire.API.Provider.Service +import Wire.API.Team.Member import Wire.API.User import Wire.UserList @@ -363,3 +365,18 @@ botMemId m = BotId $ m.fromBotMember.id_ botMemService :: BotMember -> ServiceRef botMemService m = fromJust $ m.fromBotMember.service + +localBotsAndUsers :: (Foldable f) => f LocalMember -> ([BotMember], [LocalMember]) +localBotsAndUsers = foldMap botOrUser + where + botOrUser m = case m.service of + -- we drop invalid bots here, which shouldn't happen + Just _ -> (toList (newBotMember m), []) + Nothing -> ([], [m]) + +nonTeamMembers :: [LocalMember] -> [TeamMember] -> [LocalMember] +nonTeamMembers cm tm = filter (not . isMemberOfTeam . (.id_)) cm + where + -- FUTUREWORK: remote members: teams and their members are always on the same backend + isMemberOfTeam = \case + uid -> isTeamMember uid tm diff --git a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs index b28e38dfc52..7af4b25c9a5 100644 --- a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs @@ -101,8 +101,7 @@ getAllTeamCollaboratorsImpl zUser team = do Store.getAllTeamCollaborators team internalGetTeamCollaboratorsWithIdsImpl :: - ( Member Store.TeamCollaboratorsStore r - ) => + (Member Store.TeamCollaboratorsStore r) => Set TeamId -> Set UserId -> Sem r [TeamCollaborator] @@ -110,8 +109,7 @@ internalGetTeamCollaboratorsWithIdsImpl = do Store.getTeamCollaboratorsWithIds internalUpdateTeamCollaboratorImpl :: - ( Member Store.TeamCollaboratorsStore r - ) => + (Member Store.TeamCollaboratorsStore r) => UserId -> TeamId -> Set CollaboratorPermission -> @@ -120,8 +118,7 @@ internalUpdateTeamCollaboratorImpl user team perms = do Store.updateTeamCollaborator user team perms internalRemoveTeamCollaboratorImpl :: - ( Member Store.TeamCollaboratorsStore r - ) => + (Member Store.TeamCollaboratorsStore r) => UserId -> TeamId -> Sem r () diff --git a/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs index 58e01782d4b..2f6ff578364 100644 --- a/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs @@ -27,6 +27,7 @@ import Data.Set qualified as Set import Data.Text.Ascii qualified as AsciiText import Data.Text.Encoding qualified as Text import Imports +import Network.Wai.Utilities.Exception (displayExceptionNoBacktrace) import Polysemy import Polysemy.Error import Polysemy.Input (Input, input, runInputConst) @@ -265,7 +266,7 @@ logInvitationRequest context action = runError action >>= \case Left e -> do Log.warn $ - msg @String ("Failed to create invitation: " <> show e) + msg @String ("Failed to create invitation: " <> displayExceptionNoBacktrace e) . context throw e Right res@(_, code) -> do diff --git a/services/galley/src/Galley/Intra/Journal.hs b/libs/wire-subsystems/src/Wire/TeamJournal.hs similarity index 81% rename from services/galley/src/Galley/Intra/Journal.hs rename to libs/wire-subsystems/src/Wire/TeamJournal.hs index 51221065ac6..c8e6ba64a00 100644 --- a/services/galley/src/Galley/Intra/Journal.hs +++ b/libs/wire-subsystems/src/Wire/TeamJournal.hs @@ -1,6 +1,8 @@ +{-# LANGUAGE TemplateHaskell #-} + -- This file is part of the Wire Server implementation. -- --- Copyright (C) 2022 Wire Swiss GmbH +-- Copyright (C) 2025 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,14 +17,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Galley.Intra.Journal - ( teamActivate, - teamUpdate, - teamDelete, - teamSuspend, - evData, - ) -where +module Wire.TeamJournal where import Control.Lens import Data.Currency qualified as Currency @@ -31,15 +26,20 @@ import Data.Proto.Id import Data.ProtoLens (defMessage) import Data.Text (pack) import Data.Time.Clock.POSIX -import Galley.Effects.TeamStore -import Galley.Types.Teams import Imports hiding (head) import Numeric.Natural import Polysemy -import Proto.TeamEvents (TeamEvent'EventData, TeamEvent'EventType (..)) +import Proto.TeamEvents (TeamEvent, TeamEvent'EventData, TeamEvent'EventType (..)) import Proto.TeamEvents_Fields qualified as T +import Wire.API.Team (TeamCreationTime (..)) import Wire.Sem.Now import Wire.Sem.Now qualified as Now +import Wire.TeamStore + +data TeamJournal m a where + EnqueueTeamEvent :: TeamEvent -> TeamJournal m () + +makeSem ''TeamJournal -- Note [journaling] -- ~~~~~~~~~~~~~~~~~ @@ -48,7 +48,8 @@ import Wire.Sem.Now qualified as Now teamActivate :: ( Member Now r, - Member TeamStore r + Member TeamStore r, + Member TeamJournal r ) => TeamId -> Natural -> @@ -60,8 +61,8 @@ teamActivate tid teamSize cur time = do journalEvent TeamEvent'TEAM_ACTIVATE tid (Just $ evData teamSize owners cur) time teamUpdate :: - ( Member TeamStore r, - Member Now r + ( Member Now r, + Member TeamJournal r ) => TeamId -> Natural -> @@ -71,24 +72,24 @@ teamUpdate tid teamSize billingUserIds = journalEvent TeamEvent'TEAM_UPDATE tid (Just $ evData teamSize billingUserIds Nothing) Nothing teamDelete :: - ( Member TeamStore r, - Member Now r + ( Member Now r, + Member TeamJournal r ) => TeamId -> Sem r () teamDelete tid = journalEvent TeamEvent'TEAM_DELETE tid Nothing Nothing teamSuspend :: - ( Member TeamStore r, - Member Now r + ( Member Now r, + Member TeamJournal r ) => TeamId -> Sem r () teamSuspend tid = journalEvent TeamEvent'TEAM_SUSPEND tid Nothing Nothing journalEvent :: - ( Member TeamStore r, - Member Now r + ( Member Now r, + Member TeamJournal r ) => TeamEvent'EventType -> TeamId -> @@ -98,7 +99,7 @@ journalEvent :: journalEvent typ tid dat tim = do -- writetime is in microseconds in cassandra 3.11 now <- round . utcTimeToPOSIXSeconds <$> Now.get - let ts = maybe now ((`div` 1000000) . view tcTime) tim + let ts = maybe now ((`div` 1000000) . _tcTime) tim ev = defMessage & T.eventType .~ typ diff --git a/services/galley/src/Galley/Intra/Effects.hs b/libs/wire-subsystems/src/Wire/TeamJournal/Aws.hs similarity index 53% rename from services/galley/src/Galley/Intra/Effects.hs rename to libs/wire-subsystems/src/Wire/TeamJournal/Aws.hs index 502612df75e..b9eb3e79911 100644 --- a/services/galley/src/Galley/Intra/Effects.hs +++ b/libs/wire-subsystems/src/Wire/TeamJournal/Aws.hs @@ -1,6 +1,6 @@ -- This file is part of the Wire Server implementation. -- --- Copyright (C) 2022 Wire Swiss GmbH +-- Copyright (C) 2025 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,29 +15,22 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Galley.Intra.Effects where +module Wire.TeamJournal.Aws + ( interpretTeamJournal, + ) +where -import Galley.Cassandra.Util -import Galley.Effects.SparAccess (SparAccess (..)) -import Galley.Env -import Galley.Intra.Spar -import Galley.Monad import Imports import Polysemy -import Polysemy.Input -import Polysemy.TinyLog +import Wire.AWS qualified as WA +import Wire.TeamJournal (TeamJournal (..)) -interpretSparAccess :: - ( Member (Embed IO) r, - Member (Input Env) r, - Member TinyLog r - ) => - Sem (SparAccess ': r) a -> +interpretTeamJournal :: + (Member (Embed IO) r) => + Maybe WA.Env -> + Sem (TeamJournal ': r) a -> Sem r a -interpretSparAccess = interpret $ \case - DeleteTeam tid -> do - logEffect "SparAccess.DeleteTeam" - embedApp $ deleteTeam tid - LookupScimUserInfo uid -> do - logEffect "SparAccess.LookupScimUserInfo" - embedApp $ lookupScimUserInfo uid +interpretTeamJournal mEnv = interpret $ \case + EnqueueTeamEvent ev -> case mEnv of + Nothing -> pure () + Just e -> embed $ WA.execute e (WA.enqueue ev) diff --git a/services/galley/src/Galley/Effects/TeamStore.hs b/libs/wire-subsystems/src/Wire/TeamStore.hs similarity index 68% rename from services/galley/src/Galley/Effects/TeamStore.hs rename to libs/wire-subsystems/src/Wire/TeamStore.hs index 96300b755ef..bf45dd0cdf5 100644 --- a/services/galley/src/Galley/Effects/TeamStore.hs +++ b/libs/wire-subsystems/src/Wire/TeamStore.hs @@ -2,7 +2,7 @@ -- This file is part of the Wire Server implementation. -- --- Copyright (C) 2022 Wire Swiss GmbH +-- Copyright (C) 2025 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 @@ -17,82 +17,21 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Galley.Effects.TeamStore - ( -- * Team store effect - TeamStore (..), - - -- * Teams - - -- ** Create teams - createTeam, - - -- ** Read teams - getTeam, - getTeamName, - getTeamBinding, - getTeamsBindings, - getTeamCreationTime, - listTeams, - selectTeams, - getUserTeams, - getUsersTeams, - getOneUserTeam, - lookupBindingTeam, - - -- ** Update teams - setTeamData, - setTeamStatus, - - -- ** Delete teams - deleteTeam, - - -- * Team Members - - -- ** Create team members - createTeamMember, - - -- ** Read team members - getTeamMember, - getTeamMembersWithLimit, - getTeamMembers, - getBillingTeamMembers, - getTeamAdmins, - selectTeamMembers, - selectTeamMemberInfos, - selectTeamMembersPaginated, - - -- ** Update team members - setTeamMemberPermissions, - - -- ** Delete team members - deleteTeamMember, - - -- * Configuration - fanoutLimit, - getLegalHoldFlag, - - -- * Events - enqueueTeamEvent, - ) -where +module Wire.TeamStore where import Data.Id import Data.Range -import Galley.Types.Teams import Imports import Polysemy -import Proto.TeamEvents qualified 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.Feature import Wire.API.Team.Member (HardTruncationLimit, TeamMember, TeamMemberList) import Wire.API.Team.Member.Info (TeamMemberInfo) import Wire.API.Team.Permission import Wire.ListItems import Wire.Sem.Paging -import Wire.Sem.Paging.Cassandra (CassandraPaging) data TeamStore m a where CreateTeamMember :: TeamId -> TeamMember -> TeamStore m () @@ -116,12 +55,6 @@ data TeamStore m a where GetTeamMembers :: TeamId -> TeamStore m [TeamMember] SelectTeamMembers :: TeamId -> [UserId] -> TeamStore m [TeamMember] SelectTeamMemberInfos :: TeamId -> [UserId] -> TeamStore m [TeamMemberInfo] - SelectTeamMembersPaginated :: - TeamId -> - [UserId] -> - Maybe (PagingState CassandraPaging TeamMember) -> - PagingBounds CassandraPaging TeamMember -> - TeamStore m (Page CassandraPaging TeamMember) -- FUTUREWORK(mangoiv): this should be a single 'TeamId' (@'Maybe' 'TeamId'@), there's no way -- a user could be part of multiple teams GetUserTeams :: UserId -> TeamStore m [TeamId] @@ -133,9 +66,6 @@ data TeamStore m a where DeleteTeam :: TeamId -> TeamStore m () SetTeamData :: TeamId -> TeamUpdateData -> TeamStore m () SetTeamStatus :: TeamId -> TeamStatus -> TeamStore m () - FanoutLimit :: TeamStore m (Range 1 HardTruncationLimit Int32) - GetLegalHoldFlag :: TeamStore m (FeatureDefaults LegalholdConfig) - EnqueueTeamEvent :: E.TeamEvent -> TeamStore m () makeSem ''TeamStore diff --git a/libs/wire-subsystems/src/Wire/TeamStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/TeamStore/Cassandra.hs new file mode 100644 index 00000000000..5a5f52468da --- /dev/null +++ b/libs/wire-subsystems/src/Wire/TeamStore/Cassandra.hs @@ -0,0 +1,333 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 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.TeamStore.Cassandra + ( interpretTeamStoreToCassandra, + ) +where + +import Cassandra +import Cassandra.Util +import Control.Lens hiding ((<|)) +import Control.Monad.Catch () +import Data.ByteString.Conversion (toByteString') +import Data.Id as Id +import Data.Json.Util (UTCTimeMillis (..), toUTCTimeMillis) +import Data.LegalHold (UserLegalHoldStatus (..), defUserLegalHoldStatus) +import Data.Map.Strict qualified as Map +import Data.Range +import Data.Set qualified as Set +import Data.Text.Encoding +import Data.UUID.V4 (nextRandom) +import Imports hiding (Set, max) +import Polysemy +import Polysemy.Input +import Polysemy.TinyLog +import UnliftIO qualified +import Wire.API.Routes.Internal.Galley.TeamsIntra +import Wire.API.Team +import Wire.API.Team.Member +import Wire.API.Team.Member.Info (TeamMemberInfo (TeamMemberInfo)) +import Wire.API.Team.Member.Info qualified as Info +import Wire.API.Team.Permission (Perm (SetBilling), Permissions, self) +import Wire.ConversationStore (ConversationStore) +import Wire.ConversationStore qualified as E +import Wire.ConversationStore.Cassandra.Instances () +import Wire.TeamStore (TeamStore (..)) +import Wire.TeamStore.Cassandra.Queries qualified as Cql +import Wire.Util (embedClientInput, logEffect) + +interpretTeamStoreToCassandra :: + ( Member (Embed IO) r, + Member (Input ClientState) r, + Member TinyLog r, + Member ConversationStore r + ) => + Sem (TeamStore ': r) a -> + Sem r a +interpretTeamStoreToCassandra = interpret $ \case + CreateTeamMember tid mem -> do + logEffect "TeamStore.CreateTeamMember" + embedClientInput (addTeamMember tid mem) + SetTeamMemberPermissions perm0 tid uid perm1 -> do + logEffect "TeamStore.SetTeamMemberPermissions" + embedClientInput (updateTeamMember perm0 tid uid perm1) + CreateTeam t uid n i k b -> do + logEffect "TeamStore.CreateTeam" + createTeam t uid n i k b + DeleteTeamMember tid uid -> do + logEffect "TeamStore.DeleteTeamMember" + embedClientInput (removeTeamMember tid uid) + GetBillingTeamMembers tid -> do + logEffect "TeamStore.GetBillingTeamMembers" + embedClientInput (listBillingTeamMembers tid) + GetTeamAdmins tid -> do + logEffect "TeamStore.GetTeamAdmins" + embedClientInput (listTeamAdmins tid) + GetTeam tid -> do + logEffect "TeamStore.GetTeam" + embedClientInput (team tid) + GetTeamName tid -> do + logEffect "TeamStore.GetTeamName" + embedClientInput (getTeamName tid) + SelectTeams uid tids -> do + logEffect "TeamStore.SelectTeams" + embedClientInput (teamIdsOf uid tids) + GetTeamMember tid uid -> do + logEffect "TeamStore.GetTeamMember" + teamMember tid uid + GetTeamMembers tid -> do + logEffect "TeamStore.GetTeamMembers" + teamMembersCollectedWithPagination tid + GetTeamMembersWithLimit tid n -> do + logEffect "TeamStore.GetTeamMembersWithLimit" + teamMembersWithLimit tid n + SelectTeamMembers tid uids -> do + logEffect "TeamStore.SelectTeamMembers" + teamMembersLimited tid uids + SelectTeamMemberInfos tid uids -> do + logEffect "TeamStore.SelectTeamMemberInfos" + embedClientInput (teamMemberInfos tid uids) + GetUserTeams uid -> do + logEffect "TeamStore.GetUserTeams" + embedClientInput (userTeams uid) + GetUsersTeams uids -> do + logEffect "TeamStore.GetUsersTeams" + embedClientInput (usersTeams uids) + GetOneUserTeam uid -> do + logEffect "TeamStore.GetOneUserTeam" + embedClientInput (oneUserTeam uid) + GetTeamsBindings tid -> do + logEffect "TeamStore.GetTeamsBindings" + embedClientInput (getTeamsBindings tid) + GetTeamBinding tid -> do + logEffect "TeamStore.GetTeamBinding" + embedClientInput (getTeamBinding tid) + GetTeamCreationTime tid -> do + logEffect "TeamStore.GetTeamCreationTime" + embedClientInput (teamCreationTime tid) + DeleteTeam tid -> do + logEffect "TeamStore.DeleteTeam" + deleteTeam tid + SetTeamData tid upd -> do + logEffect "TeamStore.SetTeamData" + embedClientInput (updateTeam tid upd) + SetTeamStatus tid st -> do + logEffect "TeamStore.SetTeamStatus" + embedClientInput (updateTeamStatus tid st) + +createTeam :: + ( Member (Input ClientState) r, + Member (Embed IO) r + ) => + Maybe TeamId -> + UserId -> + Range 1 256 Text -> + Icon -> + Maybe (Range 1 256 Text) -> + TeamBinding -> + Sem r Team +createTeam t uid (fromRange -> n) i k b = do + tid <- embed @IO $ maybe (Id <$> liftIO nextRandom) pure t + embedClientInput $ retry x5 $ write Cql.insertTeam (params LocalQuorum (tid, uid, n, i, fromRange <$> k, initialStatus b, b)) + pure (newTeam tid uid n i b & teamIconKey .~ (fromRange <$> k)) + where + initialStatus Binding = PendingActive + initialStatus NonBinding = Active + +listBillingTeamMembers :: TeamId -> Client [UserId] +listBillingTeamMembers tid = fmap runIdentity <$> retry x1 (query Cql.listBillingTeamMembers (params LocalQuorum (Identity tid))) + +listTeamAdmins :: TeamId -> Client [UserId] +listTeamAdmins tid = fmap runIdentity <$> retry x1 (query Cql.listTeamAdmins (params LocalQuorum (Identity tid))) + +getTeamName :: TeamId -> Client (Maybe Text) +getTeamName tid = fmap runIdentity <$> retry x1 (query1 Cql.selectTeamName (params LocalQuorum (Identity tid))) + +teamIdsOf :: UserId -> [TeamId] -> Client [TeamId] +teamIdsOf uid tids = fmap runIdentity <$> retry x1 (query Cql.selectUserTeamsIn (params LocalQuorum (uid, tids))) + +team :: TeamId -> Client (Maybe TeamData) +team tid = fmap toTeam <$> retry x1 (query1 Cql.selectTeam (params LocalQuorum (Identity tid))) + where + toTeam (u, n, i, k, d, s, st, b, ss) = + let t = newTeam tid u n i (fromMaybe NonBinding b) & teamIconKey .~ k & teamSplashScreen .~ fromMaybe DefaultIcon ss + status = if d then PendingDelete else fromMaybe Active s + in TeamData t status (writetimeToUTC <$> st) + +teamMember :: + ( Member (Embed IO) r, + Member (Input ClientState) r + ) => + TeamId -> + UserId -> + Sem r (Maybe TeamMember) +teamMember t u = do + mres <- embedClientInput $ retry x1 (query1 Cql.selectTeamMember (params LocalQuorum (t, u))) + pure $ fmap (\(perms, minvu, minvt, mulhStatus) -> newTeamMember' (u, perms, minvu, minvt, mulhStatus)) mres + +addTeamMember :: TeamId -> TeamMember -> Client () +addTeamMember t m = + retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + addPrepQuery Cql.insertTeamMember (t, m ^. userId, m ^. permissions, m ^? invitation . _Just . _1, m ^? invitation . _Just . _2) + addPrepQuery Cql.insertUserTeam (m ^. userId, t) + when (m `hasPermission` SetBilling) $ addPrepQuery Cql.insertBillingTeamMember (t, m ^. userId) + when (isAdminOrOwner (m ^. permissions)) $ addPrepQuery Cql.insertTeamAdmin (t, m ^. userId) + +updateTeamMember :: Permissions -> TeamId -> UserId -> Permissions -> Client () +updateTeamMember oldPerms tid uid newPerms = do + retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + addPrepQuery Cql.updatePermissions (newPerms, tid, uid) + let permDiff = Set.difference `on` self + acquiredPerms = newPerms `permDiff` oldPerms + lostPerms = oldPerms `permDiff` newPerms + when (SetBilling `Set.member` acquiredPerms) $ addPrepQuery Cql.insertBillingTeamMember (tid, uid) + when (SetBilling `Set.member` lostPerms) $ addPrepQuery Cql.deleteBillingTeamMember (tid, uid) + when (isAdminOrOwner newPerms && not (isAdminOrOwner oldPerms)) $ addPrepQuery Cql.insertTeamAdmin (tid, uid) + when (isAdminOrOwner oldPerms && not (isAdminOrOwner newPerms)) $ addPrepQuery Cql.deleteTeamAdmin (tid, uid) + +removeTeamMember :: TeamId -> UserId -> Client () +removeTeamMember tid uid = do + retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + addPrepQuery Cql.deleteTeamMember (tid, uid) + addPrepQuery Cql.deleteUserTeam (uid, tid) + addPrepQuery Cql.deleteBillingTeamMember (tid, uid) + addPrepQuery Cql.deleteTeamAdmin (tid, uid) + +userTeams :: UserId -> Client [TeamId] +userTeams u = map runIdentity <$> retry x1 (query Cql.selectUserTeams (params LocalQuorum (Identity u))) + +usersTeams :: [UserId] -> Client (Map UserId TeamId) +usersTeams uids = do + pairs :: [(UserId, TeamId)] <- catMaybes <$> UnliftIO.pooledMapConcurrentlyN 8 (\uid -> (uid,) <$$> oneUserTeam uid) uids + pure $ foldl' (\m (k, v) -> Map.insert k v m) Map.empty pairs + +oneUserTeam :: UserId -> Client (Maybe TeamId) +oneUserTeam u = fmap runIdentity <$> retry x1 (query1 Cql.selectOneUserTeam (params LocalQuorum (Identity u))) + +teamCreationTime :: TeamId -> Client (Maybe TeamCreationTime) +teamCreationTime t = checkCreation . fmap runIdentity <$> retry x1 (query1 Cql.selectTeamBindingWritetime (params LocalQuorum (Identity t))) + where + checkCreation (Just (Just ts)) = Just $ TeamCreationTime ts + checkCreation _ = Nothing + +getTeamBinding :: TeamId -> Client (Maybe TeamBinding) +getTeamBinding t = fmap (fromMaybe NonBinding . runIdentity) <$> retry x1 (query1 Cql.selectTeamBinding (params LocalQuorum (Identity t))) + +getTeamsBindings :: [TeamId] -> Client [TeamBinding] +getTeamsBindings = fmap catMaybes . UnliftIO.pooledMapConcurrentlyN 8 getTeamBinding + +deleteTeam :: + ( Member (Input ClientState) r, + Member (Embed IO) r, + Member ConversationStore r + ) => + TeamId -> + Sem r () +deleteTeam tid = do + embedClientInput (markTeamDeletedAndRemoveTeamMembers tid) + E.deleteTeamConversations tid + embedClientInput (retry x5 $ write Cql.deleteTeam (params LocalQuorum (Deleted, tid))) + +markTeamDeletedAndRemoveTeamMembers :: TeamId -> Client () +markTeamDeletedAndRemoveTeamMembers tid = do + retry x5 $ write Cql.markTeamDeleted (params LocalQuorum (PendingDelete, tid)) + mems <- teamMembersForPagination tid Nothing (unsafeRange 2000) + removeTeamMembers mems + where + removeTeamMembers mems = do + mapM_ (removeTeamMember tid . view _1) (result mems) + unless (null $ result mems) $ removeTeamMembers =<< liftClient (nextPage mems) + +updateTeamStatus :: TeamId -> TeamStatus -> Client () +updateTeamStatus t s = retry x5 $ write Cql.updateTeamStatus (params LocalQuorum (s, t)) + +updateTeam :: TeamId -> TeamUpdateData -> Client () +updateTeam tid u = retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + for_ (u ^. nameUpdate) $ \n -> addPrepQuery Cql.updateTeamName (fromRange n, tid) + for_ (u ^. iconUpdate) $ \i -> addPrepQuery Cql.updateTeamIcon (decodeUtf8 . toByteString' $ i, tid) + for_ (u ^. iconKeyUpdate) $ \k -> addPrepQuery Cql.updateTeamIconKey (fromRange k, tid) + for_ (u ^. splashScreenUpdate) $ \ss -> addPrepQuery Cql.updateTeamSplashScreen (decodeUtf8 . toByteString' $ ss, tid) + +newTeamMember' :: + (UserId, Permissions, Maybe UserId, Maybe UTCTimeMillis, Maybe UserLegalHoldStatus) -> + TeamMember +newTeamMember' (uid, perms, mInvUser, mInvTime, fromMaybe defUserLegalHoldStatus -> lhStatus) = + mkTeamMember uid perms ((,) <$> mInvUser <*> mInvTime) lhStatus + +type RawTeamMember = (UserId, Permissions, Maybe UserId, Maybe UTCTimeMillis, Maybe UserLegalHoldStatus) + +teamMembersForPagination :: TeamId -> Maybe UserId -> Range 1 HardTruncationLimit Int32 -> Client (Page RawTeamMember) +teamMembersForPagination tid start (fromRange -> max) = + case start of + Just u -> paginate Cql.selectTeamMembersFrom (paramsP LocalQuorum (tid, u) max) + Nothing -> paginate Cql.selectTeamMembers (paramsP LocalQuorum (Identity tid) max) + +teamMembersCollectedWithPagination :: + ( Member (Embed IO) r, + Member (Input ClientState) r + ) => + TeamId -> + Sem r [TeamMember] +teamMembersCollectedWithPagination tid = do + mems <- embedClientInput $ teamMembersForPagination tid Nothing (unsafeRange 2000) + collect [] mems + where + collect acc page = do + let tMembers = map newTeamMember' (result page) + if hasMore page + then do + page' <- embedClientInput (nextPage page) + collect (tMembers ++ acc) page' + else pure (tMembers ++ acc) + +teamMembersWithLimit :: + ( Member (Embed IO) r, + Member (Input ClientState) r + ) => + TeamId -> + Range 1 HardTruncationLimit Int32 -> + Sem r TeamMemberList +teamMembersWithLimit t (fromRange -> limit) = do + page <- embedClientInput $ retry x1 (paginate Cql.selectTeamMembers (paramsP LocalQuorum (Identity t) (limit + 1))) + let ms = map newTeamMember' . take (fromIntegral limit) $ result page + pure $ if hasMore page then newTeamMemberList ms ListTruncated else newTeamMemberList ms ListComplete + +teamMembersLimited :: + ( Member (Embed IO) r, + Member (Input ClientState) r + ) => + TeamId -> + [UserId] -> + Sem r [TeamMember] +teamMembersLimited t u = do + rows <- embedClientInput $ retry x1 (query Cql.selectTeamMembers' (params LocalQuorum (t, u))) + pure $ map (\(uid, perms, _, minvu, minvt, mlh) -> newTeamMember' (uid, perms, minvu, minvt, mlh)) rows + +teamMemberInfos :: TeamId -> [UserId] -> Client [TeamMemberInfo] +teamMemberInfos t u = mkTeamMemberInfo <$$> retry x1 (query Cql.selectTeamMembers' (params LocalQuorum (t, u))) + where + mkTeamMemberInfo (uid, perms, permsWT, _, _, _) = + TeamMemberInfo {Info.userId = uid, Info.permissions = perms, Info.permissionsWriteTime = toUTCTimeMillis $ writetimeToUTC permsWT} diff --git a/libs/wire-subsystems/src/Wire/TeamStore/Cassandra/Queries.hs b/libs/wire-subsystems/src/Wire/TeamStore/Cassandra/Queries.hs new file mode 100644 index 00000000000..921c718030f --- /dev/null +++ b/libs/wire-subsystems/src/Wire/TeamStore/Cassandra/Queries.hs @@ -0,0 +1,180 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 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.TeamStore.Cassandra.Queries where + +import Cassandra as C hiding (Value) +import Cassandra.Util (Writetime) +import Data.Id +import Data.Json.Util +import Data.LegalHold +import Imports +import Text.RawString.QQ +import Wire.API.Routes.Internal.Galley.TeamsIntra +import Wire.API.Team +import Wire.API.Team.Permission + +-- Teams -------------------------------------------------------------------- + +selectTeam :: PrepQuery R (Identity TeamId) (UserId, Text, Icon, Maybe Text, Bool, Maybe TeamStatus, Maybe (Writetime TeamStatus), Maybe TeamBinding, Maybe Icon) +selectTeam = "select creator, name, icon, icon_key, deleted, status, writetime(status), binding, splash_screen from team where team = ?" + +selectTeamName :: PrepQuery R (Identity TeamId) (Identity Text) +selectTeamName = "select name from team where team = ?" + +selectTeamBinding :: PrepQuery R (Identity TeamId) (Identity (Maybe TeamBinding)) +selectTeamBinding = "select binding from team where team = ?" + +selectTeamBindingWritetime :: PrepQuery R (Identity TeamId) (Identity (Maybe Int64)) +selectTeamBindingWritetime = "select writetime(binding) from team where team = ?" + +selectTeamMember :: + PrepQuery + R + (TeamId, UserId) + ( Permissions, + Maybe UserId, + Maybe UTCTimeMillis, + Maybe UserLegalHoldStatus + ) +selectTeamMember = "select perms, invited_by, invited_at, legalhold_status from team_member where team = ? and user = ?" + +selectTeamMembersBase :: (IsString a) => [String] -> a +selectTeamMembersBase conds = fromString $ selectFrom <> " where team = ?" <> whereClause <> " order by user" + where + selectFrom = "select user, perms, invited_by, invited_at, legalhold_status from team_member" + whereClause = concatMap (" and " <>) conds + +-- | This query fetches all members of a team, should be paginated. +selectTeamMembers :: + PrepQuery + R + (Identity TeamId) + ( UserId, + Permissions, + Maybe UserId, + Maybe UTCTimeMillis, + Maybe UserLegalHoldStatus + ) +selectTeamMembers = selectTeamMembersBase [] + +selectTeamMembersFrom :: + PrepQuery + R + (TeamId, UserId) + ( UserId, + Permissions, + Maybe UserId, + Maybe UTCTimeMillis, + Maybe UserLegalHoldStatus + ) +selectTeamMembersFrom = selectTeamMembersBase ["user > ?"] + +selectTeamMembers' :: + PrepQuery + R + (TeamId, [UserId]) + ( UserId, + Permissions, + Writetime Permissions, + Maybe UserId, + Maybe UTCTimeMillis, + Maybe UserLegalHoldStatus + ) +selectTeamMembers' = + [r| + select user, perms, writetime(perms), invited_by, invited_at, legalhold_status + from team_member + where team = ? and user in ? order by user + |] + +selectUserTeams :: PrepQuery R (Identity UserId) (Identity TeamId) +selectUserTeams = "select team from user_team where user = ? order by team" + +selectOneUserTeam :: PrepQuery R (Identity UserId) (Identity TeamId) +selectOneUserTeam = "select team from user_team where user = ? limit 1" + +selectUserTeamsIn :: PrepQuery R (UserId, [TeamId]) (Identity TeamId) +selectUserTeamsIn = "select team from user_team where user = ? and team in ? order by team" + +selectUserTeamsFrom :: PrepQuery R (UserId, TeamId) (Identity TeamId) +selectUserTeamsFrom = "select team from user_team where user = ? and team > ? order by team" + +insertTeam :: PrepQuery W (TeamId, UserId, Text, Icon, Maybe Text, TeamStatus, TeamBinding) () +insertTeam = "insert into team (team, creator, name, icon, icon_key, deleted, status, binding) values (?, ?, ?, ?, ?, false, ?, ?)" + +insertTeamMember :: PrepQuery W (TeamId, UserId, Permissions, Maybe UserId, Maybe UTCTimeMillis) () +insertTeamMember = "insert into team_member (team, user, perms, invited_by, invited_at) values (?, ?, ?, ?, ?)" + +deleteTeamMember :: PrepQuery W (TeamId, UserId) () +deleteTeamMember = "delete from team_member where team = ? and user = ?" + +insertBillingTeamMember :: PrepQuery W (TeamId, UserId) () +insertBillingTeamMember = "insert into billing_team_member (team, user) values (?, ?)" + +deleteBillingTeamMember :: PrepQuery W (TeamId, UserId) () +deleteBillingTeamMember = "delete from billing_team_member where team = ? and user = ?" + +listBillingTeamMembers :: PrepQuery R (Identity TeamId) (Identity UserId) +listBillingTeamMembers = "select user from billing_team_member where team = ?" + +insertTeamAdmin :: PrepQuery W (TeamId, UserId) () +insertTeamAdmin = "insert into team_admin (team, user) values (?, ?)" + +deleteTeamAdmin :: PrepQuery W (TeamId, UserId) () +deleteTeamAdmin = "delete from team_admin where team = ? and user = ?" + +listTeamAdmins :: PrepQuery R (Identity TeamId) (Identity UserId) +listTeamAdmins = "select user from team_admin where team = ?" + +updatePermissions :: PrepQuery W (Permissions, TeamId, UserId) () +updatePermissions = "update team_member set perms = ? where team = ? and user = ?" + +insertUserTeam :: PrepQuery W (UserId, TeamId) () +insertUserTeam = "insert into user_team (user, team) values (?, ?)" + +deleteUserTeam :: PrepQuery W (UserId, TeamId) () +deleteUserTeam = "delete from user_team where user = ? and team = ?" + +markTeamDeleted :: PrepQuery W (TeamStatus, TeamId) () +markTeamDeleted = "update team set status = ? where team = ?" + +deleteTeam :: PrepQuery W (TeamStatus, TeamId) () +deleteTeam = "update team using timestamp 32503680000000000 set name = 'default', icon = 'default', status = ? where team = ? " + +updateTeamName :: PrepQuery W (Text, TeamId) () +updateTeamName = "update team set name = ? where team = ?" + +updateTeamIcon :: PrepQuery W (Text, TeamId) () +updateTeamIcon = "update team set icon = ? where team = ?" + +updateTeamIconKey :: PrepQuery W (Text, TeamId) () +updateTeamIconKey = "update team set icon_key = ? where team = ?" + +updateTeamStatus :: PrepQuery W (TeamStatus, TeamId) () +updateTeamStatus = "update team set status = ? where team = ?" + +updateTeamSplashScreen :: PrepQuery W (Text, TeamId) () +updateTeamSplashScreen = "update team set splash_screen = ? where team = ?" + +-- LegalHold whitelist ------------------------------------------------------- + +selectLegalHoldWhitelistedTeam :: PrepQuery R (Identity TeamId) (Identity TeamId) +selectLegalHoldWhitelistedTeam = + [r| + select team from legalhold_whitelisted where team = ? + |] diff --git a/libs/wire-subsystems/src/Wire/TeamSubsystem.hs b/libs/wire-subsystems/src/Wire/TeamSubsystem.hs index 9bd7d5ea923..62914e0cea7 100644 --- a/libs/wire-subsystems/src/Wire/TeamSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/TeamSubsystem.hs @@ -20,6 +20,7 @@ module Wire.TeamSubsystem where import Data.Id +import Data.Qualified import Data.Range import Imports import Polysemy @@ -28,8 +29,10 @@ import Wire.API.Team.Member.Info (TeamMemberInfoList) data TeamSubsystem m a where InternalGetTeamMember :: UserId -> TeamId -> TeamSubsystem m (Maybe TeamMember) - InternalGetTeamMembers :: TeamId -> Maybe (Range 1 HardTruncationLimit Int32) -> TeamSubsystem m TeamMemberList + InternalGetTeamMembersWithLimit :: TeamId -> Maybe (Range 1 HardTruncationLimit Int32) -> TeamSubsystem m TeamMemberList + InternalSelectTeamMembers :: TeamId -> [UserId] -> TeamSubsystem m [TeamMember] InternalSelectTeamMemberInfos :: TeamId -> [UserId] -> TeamSubsystem m TeamMemberInfoList InternalGetTeamAdmins :: TeamId -> TeamSubsystem m TeamMemberList + InternalFinalizeDeleteTeam :: Local UserId -> Maybe ConnId -> TeamId -> TeamSubsystem m () makeSem ''TeamSubsystem diff --git a/libs/wire-subsystems/src/Wire/TeamSubsystem/GalleyAPI.hs b/libs/wire-subsystems/src/Wire/TeamSubsystem/GalleyAPI.hs index d5c51b3dd0d..d34edaf4045 100644 --- a/libs/wire-subsystems/src/Wire/TeamSubsystem/GalleyAPI.hs +++ b/libs/wire-subsystems/src/Wire/TeamSubsystem/GalleyAPI.hs @@ -23,9 +23,11 @@ import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess import Wire.TeamSubsystem -intepreterTeamSubsystemToGalleyAPI :: (Member GalleyAPIAccess r) => InterpreterFor TeamSubsystem r -intepreterTeamSubsystemToGalleyAPI = interpret $ \case +interpretTeamSubsystemToGalleyAPI :: (Member GalleyAPIAccess r) => InterpreterFor TeamSubsystem r +interpretTeamSubsystemToGalleyAPI = interpret $ \case InternalGetTeamMember userId teamId -> GalleyAPIAccess.getTeamMember userId teamId - InternalGetTeamMembers teamId maxResults -> GalleyAPIAccess.getTeamMembers teamId maxResults + InternalGetTeamMembersWithLimit teamId maxResults -> GalleyAPIAccess.getTeamMembersWithLimit teamId maxResults InternalSelectTeamMemberInfos teamId userIds -> GalleyAPIAccess.selectTeamMemberInfos teamId userIds + InternalSelectTeamMembers teamId userIds -> GalleyAPIAccess.selectTeamMembers teamId userIds InternalGetTeamAdmins teamId -> GalleyAPIAccess.getTeamAdmins teamId + InternalFinalizeDeleteTeam lusr mcon teamId -> GalleyAPIAccess.finalizeDeleteTeam lusr mcon teamId diff --git a/libs/wire-subsystems/src/Wire/TeamSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/TeamSubsystem/Interpreter.hs new file mode 100644 index 00000000000..d0f994ab3c2 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/TeamSubsystem/Interpreter.hs @@ -0,0 +1,205 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 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.TeamSubsystem.Interpreter where + +import Control.Lens (view, (%~), (^.)) +import Data.Default +import Data.Id +import Data.Json.Util +import Data.LegalHold (UserLegalHoldStatus (..)) +import Data.List.Extra qualified as List +import Data.List.NonEmpty (NonEmpty ((:|))) +import Data.Qualified +import Data.Time +import Imports +import Polysemy +import Polysemy.Input +import Wire.API.Event.Conversation qualified as Conv +import Wire.API.Event.Team +import Wire.API.Team.HardTruncationLimit +import Wire.API.Team.Member +import Wire.API.Team.Member.Info (TeamMemberInfoList (TeamMemberInfoList)) +import Wire.BrigAPIAccess +import Wire.BrigAPIAccess qualified as Brig +import Wire.ConversationStore +import Wire.ConversationStore qualified as ConvStore +import Wire.ExternalAccess +import Wire.ExternalAccess qualified as ExternalAccess +import Wire.LegalHoldStore (LegalHoldStore) +import Wire.LegalHoldStore qualified as LH +import Wire.NotificationSubsystem +import Wire.Sem.Now +import Wire.Sem.Now qualified as Now +import Wire.SparAPIAccess +import Wire.SparAPIAccess qualified as Spar +import Wire.StoredConversation +import Wire.TeamJournal +import Wire.TeamJournal qualified as Journal +import Wire.TeamStore (TeamStore) +import Wire.TeamStore qualified as TeamStore +import Wire.TeamSubsystem + +newtype TeamSubsystemConfig = TeamSubsystemConfig {concurrentDeletionEvents :: Int} + +interpretTeamSubsystem :: + ( Member TeamStore r, + Member LegalHoldStore r, + Member BrigAPIAccess r, + Member ExternalAccess r, + Member NotificationSubsystem r, + Member Now r, + Member SparAPIAccess r, + Member ConversationStore r, + Member TeamJournal r + ) => + TeamSubsystemConfig -> + InterpreterFor TeamSubsystem r +interpretTeamSubsystem config = + runInputConst config . interpretTeamSubsystemWithInputConfig . raiseUnder + +interpretTeamSubsystemWithInputConfig :: + ( Member TeamStore r, + Member LegalHoldStore r, + Member BrigAPIAccess r, + Member ExternalAccess r, + Member NotificationSubsystem r, + Member (Input TeamSubsystemConfig) r, + Member Now r, + Member SparAPIAccess r, + Member ConversationStore r, + Member TeamJournal r + ) => + InterpreterFor TeamSubsystem r +interpretTeamSubsystemWithInputConfig = + interpret $ \case + InternalGetTeamMember uid tid -> do + tms <- TeamStore.getTeamMember tid uid + for tms $ \tm -> do + hasImplicitConsent <- LH.isTeamLegalholdWhitelisted tid + pure $ if hasImplicitConsent then grantImplicitConsent tm else tm + InternalGetTeamMembersWithLimit tid maxResults -> do + tmList <- TeamStore.getTeamMembersWithLimit tid (fromMaybe hardTruncationLimitRange maxResults) + ms <- adjustMembersForImplicitConsent tid (tmList ^. teamMembers) + pure $ newTeamMemberList ms (tmList ^. teamMemberListType) + InternalSelectTeamMemberInfos tid uids -> TeamMemberInfoList <$> TeamStore.selectTeamMemberInfos tid uids + InternalSelectTeamMembers tid uids -> do + tms <- TeamStore.selectTeamMembers tid uids + adjustMembersForImplicitConsent tid tms + InternalGetTeamAdmins tid -> do + admins <- + TeamStore.getTeamAdmins tid + >>= TeamStore.selectTeamMembers tid + >>= adjustMembersForImplicitConsent tid + pure $ newTeamMemberList admins ListComplete + InternalFinalizeDeleteTeam luid mcon tid -> + internalFinalizeDeleteTeamImpl luid mcon tid + +adjustMembersForImplicitConsent :: (Member LegalHoldStore r) => TeamId -> [TeamMember] -> Sem r [TeamMember] +adjustMembersForImplicitConsent tid ms = do + hasImplicitConsent <- LH.isTeamLegalholdWhitelisted tid + pure $ if hasImplicitConsent then map grantImplicitConsent ms else ms + +grantImplicitConsent :: TeamMember -> TeamMember +grantImplicitConsent = + legalHoldStatus %~ \case + UserLegalHoldNoConsent -> UserLegalHoldDisabled + UserLegalHoldDisabled -> UserLegalHoldDisabled + UserLegalHoldPending -> UserLegalHoldPending + UserLegalHoldEnabled -> UserLegalHoldEnabled + +-- This function is "unchecked" because it does not validate that the user has the `DeleteTeam` permission. +internalFinalizeDeleteTeamImpl :: + forall r. + ( Member BrigAPIAccess r, + Member ExternalAccess r, + Member NotificationSubsystem r, + Member (Input TeamSubsystemConfig) r, + Member Now r, + Member LegalHoldStore r, + Member SparAPIAccess r, + Member TeamStore r, + Member ConversationStore r, + Member TeamJournal r + ) => + Local UserId -> + Maybe ConnId -> + TeamId -> + Sem r () +internalFinalizeDeleteTeamImpl lusr zcon tid = do + team <- TeamStore.getTeam tid + when (isJust team) $ do + Spar.deleteTeam tid + now <- Now.get + convs <- ConvStore.getTeamConversations tid + -- Even for LARGE TEAMS, we _DO_ want to fetch all team members here because we + -- want to generate conversation deletion events for non-team users. This should + -- be fine as it is done once during the life team of a team and we still do not + -- fanout this particular event to all team members anyway. And this is anyway + -- done asynchronously + + -- No need to adjust implicit consent here. + membs <- TeamStore.getTeamMembers tid + (ue, be) <- foldrM (createConvDeleteEvents now membs) ([], []) convs + let e = newEvent tid now EdTeamDelete + pushDeleteEvents membs e ue + ExternalAccess.deliverAsync be + -- TODO: we don't delete bots here, but we should do that, since + -- every bot user can only be in a single conversation. Just + -- deleting conversations from the database is not enough. + mapM_ (Brig.deleteUser . view userId) membs + Journal.teamDelete tid + LH.unsetTeamLegalholdWhitelisted tid + TeamStore.deleteTeam tid + where + pushDeleteEvents :: [TeamMember] -> Event -> [Push] -> Sem r () + pushDeleteEvents membs e ue = do + let r = userRecipient (tUnqualified lusr) :| membersToRecipients (Just (tUnqualified lusr)) membs + + -- To avoid DoS on gundeck, send team deletion events in chunks + chunkSize <- inputs (.concurrentDeletionEvents) + let chunks = List.chunksOf chunkSize (toList r) + forM_ chunks $ \chunk -> + -- push TeamDelete events. Note that despite having a complete list, we are guaranteed in the + -- push module to never fan this out to more than the limit + pushNotifications [def {origin = Just (tUnqualified lusr), json = toJSONObject e, recipients = chunk, conn = zcon}] + -- To avoid DoS on gundeck, send conversation deletion events slowly + pushNotificationsSlowly ue + createConvDeleteEvents :: + UTCTime -> + [TeamMember] -> + ConvId -> + ([Push], [(BotMember, Conv.Event)]) -> + Sem r ([Push], [(BotMember, Conv.Event)]) + createConvDeleteEvents now teamMembs cid (pp, ee) = do + let qconvId = tUntagged $ qualifyAs lusr cid + (bots, convMembs) <- localBotsAndUsers <$> ConvStore.getLocalMembers cid + -- Only nonTeamMembers need to get any events, since on team deletion, + -- all team users are deleted immediately after these events are sent + -- and will thus never be able to see these events in practice. + let mm = nonTeamMembers convMembs teamMembs + let e = Conv.Event qconvId Nothing (Conv.EventFromUser (tUntagged lusr)) now (Just tid) Conv.EdConvDelete + -- This event always contains all the required recipients + let p = + def + { origin = Just (tUnqualified lusr), + json = toJSONObject e, + recipients = map localMemberToRecipient mm + } + let ee' = map (,e) bots + let pp' = (p {conn = zcon}) : pp + pure (pp', ee' ++ ee) diff --git a/libs/wire-subsystems/src/Wire/TeamSubsystem/Util.hs b/libs/wire-subsystems/src/Wire/TeamSubsystem/Util.hs index da795e73daf..f864cd02e23 100644 --- a/libs/wire-subsystems/src/Wire/TeamSubsystem/Util.hs +++ b/libs/wire-subsystems/src/Wire/TeamSubsystem/Util.hs @@ -53,7 +53,7 @@ generateTeamEvents uid tid eventsData = do { recipientUserId = u, recipientClients = RecipientClientsAll } - | u <- admins ^.. TM.teamMembers . traverse . TM.userId + | u <- admins ^.. TM.teamMembers . traverse . TM.userId ], transient = False } diff --git a/libs/wire-subsystems/src/Wire/UserGroupStore.hs b/libs/wire-subsystems/src/Wire/UserGroupStore.hs index cb4c9449046..1dd366eda17 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupStore.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupStore.hs @@ -25,35 +25,18 @@ import Data.Time.Clock import Data.Vector import Imports import Polysemy -import Wire.API.Pagination import Wire.API.User.Profile import Wire.API.UserGroup import Wire.API.UserGroup.Pagination -import Wire.PaginationState - -data UserGroupPageRequest = UserGroupPageRequest - { team :: TeamId, - searchString :: Maybe Text, - paginationState :: PaginationState UserGroupId, - sortOrder :: SortOrder, - pageSize :: PageSize, - includeMemberCount :: Bool, - includeChannels :: Bool - } userGroupCreatedAtPaginationState :: UserGroup_ f -> (UTCTime, UserGroupId) userGroupCreatedAtPaginationState ug = (fromUTCTimeMillis ug.createdAt, ug.id_) -toSortBy :: PaginationState UserGroupId -> SortBy -toSortBy = \case - PaginationSortByName _ -> SortByName - PaginationSortByCreatedAt _ -> SortByCreatedAt - data UserGroupStore m a where CreateUserGroup :: TeamId -> NewUserGroup -> ManagedBy -> UserGroupStore m UserGroup GetUserGroup :: TeamId -> UserGroupId -> Bool -> UserGroupStore m (Maybe UserGroup) - GetUserGroups :: UserGroupPageRequest -> UserGroupStore m UserGroupPage - GetUserGroupsWithMembers :: UserGroupPageRequest -> UserGroupStore m UserGroupPageWithMembers + GetUserGroups :: TeamId -> UserGroupPageRequest -> UserGroupStore m UserGroupPage + GetUserGroupsWithMembers :: TeamId -> UserGroupPageRequest -> UserGroupStore m UserGroupPageWithMembers GetUserGroupsForConv :: ConvId -> UserGroupStore m (Vector UserGroup) UpdateUserGroup :: TeamId -> UserGroupId -> UserGroupUpdate -> UserGroupStore m (Maybe ()) DeleteUserGroup :: TeamId -> UserGroupId -> UserGroupStore m (Maybe ()) diff --git a/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs b/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs index c5609389a38..ce4e3ae515e 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupStore/Postgres.hs @@ -48,7 +48,7 @@ import Wire.API.UserGroup hiding (UpdateUserGroupChannels) import Wire.API.UserGroup.Pagination import Wire.PaginationState import Wire.Postgres -import Wire.UserGroupStore (UserGroupPageRequest (..), UserGroupStore (..)) +import Wire.UserGroupStore (UserGroupStore (..)) type UserGroupStorePostgresEffectConstraints r = ( Member (Embed IO) r, @@ -64,8 +64,8 @@ interpretUserGroupStoreToPostgres = interpret $ \case CreateUserGroup team newUserGroup managedBy -> createUserGroup team newUserGroup managedBy GetUserGroup team userGroupId includeChannels -> getUserGroup team userGroupId includeChannels - GetUserGroups req -> getUserGroups req - GetUserGroupsWithMembers req -> getUserGroupsWithMembers req + GetUserGroups tid req -> getUserGroups tid req + GetUserGroupsWithMembers tid req -> getUserGroupsWithMembers tid req GetUserGroupsForConv convId -> getUserGroupsForConv convId UpdateUserGroup tid gid gup -> updateGroup tid gid gup DeleteUserGroup tid gid -> deleteGroup tid gid @@ -223,15 +223,15 @@ getUserGroup team id_ includeChannels = do getUserGroupsWithMembers :: forall r. - ( UserGroupStorePostgresEffectConstraints r - ) => + (UserGroupStorePostgresEffectConstraints r) => + TeamId -> UserGroupPageRequest -> Sem r UserGroupPageWithMembers -getUserGroupsWithMembers req = +getUserGroupsWithMembers tid req = runTransaction TxSessions.ReadCommitted TxSessions.Read $ UserGroupPage <$> Tx.statement () (refineResult (mapM toUserGroup) $ buildStatement query rows) - <*> getUserGroupCount req + <*> getUserGroupCount tid req where rows :: HD.Result [(UUID, Text, Int32, UTCTime, Vector UUID, Int32)] rows = @@ -262,7 +262,7 @@ getUserGroupsWithMembers req = "from user_group ug", "left join user_group_member gm on ug.id = gm.user_group_id" ] - <> [where_ (groupMatchIdName req <> groupPaginationWhereClause req)] + <> [where_ (groupMatchIdName tid req <> groupPaginationWhereClause req)] <> [ literal "group by ug.team_id, ug.id" ] <> groupPaginationOrderBy req @@ -277,12 +277,18 @@ getUserGroupsWithMembers req = members = Identity (fmap Id members' :: Vector UserId) pure $ UserGroup_ {..} -groupMatchIdName :: UserGroupPageRequest -> [QueryFragment] -groupMatchIdName req = - clause1 "ug.team_id" "=" req.team - : case req.searchString of +groupMatchIdName :: TeamId -> UserGroupPageRequest -> [QueryFragment] +groupMatchIdName tid req = + clause1 "ug.team_id" "=" tid + : managedByClause + <> nameClause + where + nameClause = case req.searchString of Just name -> [like "ug.name" name] Nothing -> [] + managedByClause = case req.managedByFilter of + Just managedBy -> [clause1 "ug.managed_by" "=" (managedByToInt32 managedBy)] + Nothing -> [] groupPaginationWhereClause :: UserGroupPageRequest -> [QueryFragment] groupPaginationWhereClause req = case paginationClause req.paginationState of @@ -291,22 +297,22 @@ groupPaginationWhereClause req = case paginationClause req.paginationState of groupPaginationOrderBy :: UserGroupPageRequest -> [QueryFragment] groupPaginationOrderBy req = - [ orderBy - [ (sortColumn req.paginationState, req.sortOrder), - ("ug.id", req.sortOrder) - ], - limit (pageSizeToInt32 req.pageSize) + [ orderBy $ + case req.paginationState of + PaginationSortByName _ -> [("ug.name", req.sortOrder), ("ug.id", req.sortOrder)] + PaginationSortByCreatedAt _ -> [("ug.created_at", req.sortOrder), ("ug.id", req.sortOrder)] + _ -> [("ug.id", req.sortOrder)], + limit $ + fromIntegral @_ @Int32 (pageSizeToWord req.pageSize) ] - where - sortColumn :: PaginationState a -> Text - sortColumn = \case - PaginationSortByName _ -> "ug.name" - PaginationSortByCreatedAt _ -> "ug.created_at" + <> case req.paginationState of + PaginationOffset n -> [offset (fromIntegral n :: Int32)] + _ -> [] -getUserGroupCount :: UserGroupPageRequest -> Tx.Transaction Int -getUserGroupCount req = Tx.statement () $ refineResult parseCount $ buildStatement query decoder +getUserGroupCount :: TeamId -> UserGroupPageRequest -> Tx.Transaction Int +getUserGroupCount tid req = Tx.statement () $ refineResult parseCount $ buildStatement query decoder where - query = literal "select count(*) from user_group ug" <> where_ (groupMatchIdName req) + query = literal "select count(*) from user_group ug" <> where_ (groupMatchIdName tid req) decoder = HD.singleRow (HD.column (HD.nonNullable HD.int8)) decodeUuidVector :: HD.Row (Vector UUID) @@ -329,12 +335,13 @@ getUserGroups :: ( UserGroupStorePostgresEffectConstraints r, Member (Input (Local ())) r ) => + TeamId -> UserGroupPageRequest -> Sem r UserGroupPage -getUserGroups req@(UserGroupPageRequest {..}) = do +getUserGroups tid req@(UserGroupPageRequest {..}) = do loc <- inputQualifyLocal () runTransaction TxSessions.ReadCommitted TxSessions.Read $ - UserGroupPage <$> getUserGroupsSession loc <*> getUserGroupCount req + UserGroupPage <$> getUserGroupsSession loc <*> getUserGroupCount tid req where getUserGroupsSession :: Local () -> Tx.Transaction [UserGroupMeta] getUserGroupsSession loc = @@ -345,7 +352,7 @@ getUserGroups req@(UserGroupPageRequest {..}) = do [ literal "select", literal selectors, literal "from user_group as ug", - where_ (groupMatchIdName req <> groupPaginationWhereClause req) + where_ (groupMatchIdName tid req <> groupPaginationWhereClause req) ] <> groupPaginationOrderBy req ) diff --git a/libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs b/libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs index 68071e5d671..4f535f3537d 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupSubsystem.hs @@ -20,48 +20,19 @@ module Wire.UserGroupSubsystem where -import Data.Default import Data.Id -import Data.Time.Clock import Data.Vector (Vector) import Imports import Polysemy -import Wire.API.Pagination import Wire.API.Routes.Internal.Brig import Wire.API.User.Profile (ManagedBy) import Wire.API.UserGroup import Wire.API.UserGroup.Pagination -data GroupSearch = GroupSearch - { query :: Maybe Text, - sortBy :: Maybe SortBy, - sortOrder :: Maybe SortOrder, - pageSize :: Maybe PageSize, - lastName :: Maybe Text, - lastCreatedAt :: Maybe UTCTime, - lastId :: Maybe UserGroupId, - includeMemberCount :: Bool, - includeChannels :: Bool - } - -instance Default GroupSearch where - def = - GroupSearch - { query = Nothing, - sortBy = Nothing, - sortOrder = Nothing, - pageSize = Nothing, - lastName = Nothing, - lastCreatedAt = Nothing, - lastId = Nothing, - includeMemberCount = False, - includeChannels = False - } - data UserGroupSubsystem m a where CreateGroup :: UserId -> NewUserGroup -> UserGroupSubsystem m UserGroup GetGroup :: UserId -> UserGroupId -> Bool -> UserGroupSubsystem m (Maybe UserGroup) - GetGroups :: UserId -> GroupSearch -> UserGroupSubsystem m UserGroupPage + GetGroups :: UserId -> UserGroupPageRequest -> UserGroupSubsystem m UserGroupPage UpdateGroup :: UserId -> UserGroupId -> UserGroupUpdate -> UserGroupSubsystem m () DeleteGroup :: UserId -> UserGroupId -> UserGroupSubsystem m () DeleteGroupManaged :: ManagedBy -> TeamId -> UserGroupId -> UserGroupSubsystem m () @@ -75,7 +46,7 @@ data UserGroupSubsystem m a where -- Internal API handlers CreateGroupInternal :: ManagedBy -> TeamId -> Maybe UserId -> NewUserGroup -> UserGroupSubsystem r UserGroup GetGroupInternal :: TeamId -> UserGroupId -> Bool -> UserGroupSubsystem m (Maybe UserGroup) - GetGroupsInternal :: TeamId -> Maybe Text -> UserGroupSubsystem m UserGroupPageWithMembers + GetGroupsInternal :: TeamId -> Maybe Text -> Maybe ManagedBy -> Word -> Maybe Word -> UserGroupSubsystem m UserGroupPageWithMembers ResetUserGroupInternal :: UpdateGroupInternalRequest -> UserGroupSubsystem m () makeSem ''UserGroupSubsystem diff --git a/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs index ff446ae855a..20b9ea71d94 100644 --- a/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserGroupSubsystem/Interpreter.hs @@ -49,12 +49,10 @@ import Wire.BackgroundJobsPublisher import Wire.Error import Wire.GalleyAPIAccess (GalleyAPIAccess, internalGetConversation) import Wire.NotificationSubsystem -import Wire.PaginationState import Wire.Sem.Random qualified as Random import Wire.TeamSubsystem -import Wire.UserGroupStore (UserGroupPageRequest (..)) import Wire.UserGroupStore qualified as Store -import Wire.UserGroupSubsystem (GroupSearch (..), UserGroupSubsystem (..)) +import Wire.UserGroupSubsystem (UserGroupSubsystem (..)) import Wire.UserSubsystem (UserSubsystem, getLocalUserProfiles, getUserTeam) interpretUserGroupSubsystem :: @@ -86,7 +84,7 @@ interpretUserGroupSubsystem = interpret $ \case -- Internal API handlers CreateGroupInternal managedBy team mbCreator newGroup -> createUserGroupFullImpl managedBy team mbCreator newGroup GetGroupInternal tid gid includeChannels -> getUserGroupInternal tid gid includeChannels - GetGroupsInternal tid displayNameSubstring -> getUserGroupsInternal tid displayNameSubstring + GetGroupsInternal tid displayNameSubstring mbManagedBy startIndex mbCount -> getUserGroupsInternal tid displayNameSubstring mbManagedBy startIndex mbCount ResetUserGroupInternal req -> resetUserGroupInternal req data UserGroupSubsystemError @@ -250,54 +248,38 @@ getUserGroups :: Member (Error UserGroupSubsystemError) r ) => UserId -> - GroupSearch -> + UserGroupPageRequest -> Sem r UserGroupPage -getUserGroups getter search = do +getUserGroups getter pageReq = do team :: TeamId <- getUserTeam getter >>= ifNothing UserGroupNotATeamAdmin getterCanSeeAll :: Bool <- fromMaybe False <$> runMaybeT (mkGetterCanSeeAll getter team) unless getterCanSeeAll (throw UserGroupNotATeamAdmin) - let pageReq = - UserGroupPageRequest - { pageSize = fromMaybe def search.pageSize, - sortOrder = fromMaybe Desc search.sortOrder, - paginationState = - mkPaginationState - (fromMaybe def search.sortBy) - search.lastName - search.lastCreatedAt - search.lastId, - team = team, - searchString = search.query, - includeMemberCount = search.includeMemberCount, - includeChannels = search.includeChannels - } - Store.getUserGroups pageReq + Store.getUserGroups team pageReq where ifNothing :: UserGroupSubsystemError -> Maybe a -> Sem r a ifNothing e = maybe (throw e) pure getUserGroupsInternal :: forall r. - ( Member Store.UserGroupStore r - ) => + (Member Store.UserGroupStore r) => TeamId -> Maybe Text -> + Maybe ManagedBy -> + Word -> + Maybe Word -> Sem r UserGroupPageWithMembers -getUserGroupsInternal team displayNameSubstring = do - let -- hscim doesn't support pagination at the time of writing this, - -- so we better fit all groups into one page! - pageSize = pageSizeFromIntUnsafe 500 - pageReq = +getUserGroupsInternal team displayNameSubstring mbManagedBy startIndex mbCount = do + let pageReq = UserGroupPageRequest - { pageSize = pageSize, + { pageSize = maybe def pageSizeFromIntegralTotal mbCount, sortOrder = Asc, - paginationState = mkPaginationState SortByName (Just "displayName") Nothing Nothing, - team = team, + paginationState = PaginationOffset startIndex, searchString = displayNameSubstring, + managedByFilter = mbManagedBy, includeMemberCount = True, includeChannels = False } - Store.getUserGroupsWithMembers pageReq + Store.getUserGroupsWithMembers team pageReq updateGroup :: ( Member UserSubsystem r, @@ -526,15 +508,15 @@ removeUserFromAllGroups uid tid = do go [] = pure () nextPage mug = - fmap (.page) . Store.getUserGroups $ + fmap (.page) . Store.getUserGroups tid $ UserGroupPageRequest { pageSize = def, sortOrder = Desc, paginationState = PaginationSortByCreatedAt $ fmap Store.userGroupCreatedAtPaginationState mug, - team = tid, searchString = Nothing, + managedByFilter = Nothing, includeMemberCount = False, includeChannels = False } diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem.hs b/libs/wire-subsystems/src/Wire/UserSubsystem.hs index 3ca9df5e115..ee6f58cff40 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem.hs @@ -50,7 +50,7 @@ import Wire.API.Team.Feature import Wire.API.Team.Member (IsPerm (..), TeamMember) import Wire.API.User import Wire.API.User.Activation -import Wire.API.User.IdentityProvider hiding (team) +import Wire.API.User.IdentityProvider hiding (domain, team) import Wire.API.User.Search import Wire.ActivationCodeStore import Wire.Arbitrary @@ -301,11 +301,11 @@ requestEmailChange lusr email allowScim = do ) => Sem r' () guardBlockedDomainEmail = do - domain <- + eDomain <- either (throwGuardFailed . InvalidDomain) pure $ emailDomain email blocked <- blockedDomains <$> input - when (domain `elem` blocked) $ + when (eDomain `elem` blocked) $ throw UserSubsystemBlockedDomain -- | Prepare changing the email (checking a number of invariants). diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index 6d5ec52c9ca..86be662b3c3 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs @@ -453,9 +453,9 @@ getLocalUserProfileImpl emailVisibilityConfigWithViewer luid = do pure $ maybe defUserLegalHoldStatus (view legalHoldStatus) teamMember let user = mkUserFromStored domain locale storedUser usrProfile = mkUserProfile emailVisibilityConfigWithViewer user lhs - app <- lift $ getApp storedUser.id + app <- lift $ mapM (getApp storedUser.id) storedUser.teamId lift $ deleteLocalIfExpired user - pure $ case app of + pure $ case join app of Nothing -> usrProfile Just _ -> usrProfile {profileType = UserTypeApp} diff --git a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs index e8eb7bcb25b..204fc5ed737 100644 --- a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs +++ b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs @@ -305,7 +305,7 @@ miniBackendLowerEffectsInterpreters mb@(MiniBackendParams {..}) = . miniGalleyAPIAccess teams galleyConfigs . inMemoryNotificationSubsystemInterpreter . noopEmailSubsystemInterpreter - . intepreterTeamSubsystemToGalleyAPI + . interpretTeamSubsystemToGalleyAPI type StateEffects = '[ State [Push], @@ -733,12 +733,12 @@ miniFederationAPIAccess :: Sem r a miniFederationAPIAccess online = do let runner :: FederatedActionRunner MiniFederationMonad r - runner domain rpc = pure . Right $ runMiniFederation domain online rpc + runner ownDomain rpc = pure . Right $ runMiniFederation ownDomain online rpc interpret \case RunFederatedEither remote rpc -> if isJust (M.lookup (qDomain $ tUntagged remote) online) then FI.runFederatedEither runner remote rpc else pure $ Left do FederationUnexpectedError "RunFederatedEither" - RunFederatedConcurrently _remotes _rpc -> error "unimplemented: RunFederatedConcurrently" - RunFederatedBucketed _domain _rpc -> error "unimplemented: RunFederatedBucketed" + RunFederatedConcurrentlyEither _remotes _rpc -> error "unimplemented: RunFederatedConcurrently" + RunFederatedConcurrentlyBucketsEither _domain _rpc -> error "unimplemented: RunFederatedBucketed" IsFederationConfigured -> pure True diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ActivationCodeStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ActivationCodeStore.hs index 67773aa5cb9..dfb21478e8c 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ActivationCodeStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ActivationCodeStore.hs @@ -40,8 +40,7 @@ emailKeyToCode = . show inMemoryActivationCodeStoreInterpreter :: - ( Member (State (Map EmailKey (Maybe UserId, ActivationCode))) r - ) => + (Member (State (Map EmailKey (Maybe UserId, ActivationCode))) r) => InterpreterFor ActivationCodeStore r inMemoryActivationCodeStoreInterpreter = interpret \case LookupActivationCode ek -> gets (!? ek) diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/AppStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/AppStore.hs index 470a1dc1308..4eec779120b 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/AppStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/AppStore.hs @@ -30,4 +30,4 @@ inMemoryAppStoreInterpreter :: InterpreterFor AppStore r inMemoryAppStoreInterpreter = interpret $ \case CreateApp app -> modify (app :) - GetApp uid -> gets $ find $ \app -> app.id == uid + GetApp uid tid -> gets $ find $ \app -> app.id == uid && app.teamId == tid diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/GalleyAPIAccess.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/GalleyAPIAccess.hs index 3224c6cc972..2d448a620f5 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/GalleyAPIAccess.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/GalleyAPIAccess.hs @@ -46,7 +46,7 @@ miniGalleyAPIAccess teams configs = interpret $ \case AddTeamMember {} -> error "AddTeamMember not implemented in miniGalleyAPIAccess" CreateTeam {} -> error "CreateTeam not implemented in miniGalleyAPIAccess" GetTeamMember uid tid -> pure $ getTeamMemberImpl teams uid tid - GetTeamMembers tid maxResults -> pure $ getTeamMembersImpl teams tid maxResults + GetTeamMembersWithLimit tid maxResults -> pure $ getTeamMembersImpl teams tid maxResults GetTeamId _ -> error "GetTeamId not implemented in miniGalleyAPIAccess" GetTeam _ -> error "GetTeam not implemented in miniGalleyAPIAccess" GetTeamName _ -> error "GetTeamName not implemented in miniGalleyAPIAccess" @@ -55,6 +55,7 @@ miniGalleyAPIAccess teams configs = interpret $ \case GetTeamSearchVisibility _ -> pure SearchVisibilityStandard ChangeTeamStatus {} -> error "ChangeTeamStatus not implemented in miniGalleyAPIAccess" + FinalizeDeleteTeam {} -> error "FinalizeDeleteTeam not implemented in miniGalleyAPIAccess" MemberIsTeamOwner tid uid -> pure $ memberIsTeamOwnerImpl teams tid uid GetAllTeamFeaturesForUser _ -> pure configs @@ -68,6 +69,7 @@ miniGalleyAPIAccess teams configs = interpret $ \case SelectTeamMemberInfos tid uids -> pure $ selectTeamMemberInfosImpl teams tid uids InternalGetConversation _ -> error "GetConv not implemented in InternalGetConversation" GetTeamContacts _ -> pure Nothing + SelectTeamMembers {} -> error "SelectTeamMembers not implemented in miniGalleyAPIAccess" -- this is called but the result is not needed in unit tests selectTeamMemberInfosImpl :: Map TeamId [TeamMember] -> TeamId -> [UserId] -> TeamMemberInfoList diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/SparAPIAccess.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/SparAPIAccess.hs index eff9c2b70ae..4122706e412 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/SparAPIAccess.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/SparAPIAccess.hs @@ -30,6 +30,8 @@ miniSparAPIAccess :: (Member (Input (Map TeamId IdPList)) r) => InterpreterFor S miniSparAPIAccess = interpret $ \case GetIdentityProviders tid -> Map.findWithDefault (IdPList []) tid <$> input + DeleteTeam {} -> error "DeleteTeam not implemented in miniSparAPIAccess" + LookupScimUserInfo {} -> error "LookupScimUserInfo not implemented in miniSparAPIAccess" emptySparAPIAccess :: InterpreterFor SparAPIAccess r emptySparAPIAccess = runInputConst mempty . miniSparAPIAccess . raiseUnder diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs index 0e01041ab80..20e55ebe8e9 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserGroupStore.hs @@ -28,7 +28,6 @@ import Wire.API.UserGroup hiding (UpdateUserGroupChannels) import Wire.API.UserGroup.Pagination import Wire.MockInterpreters.Now import Wire.MockInterpreters.Random -import Wire.PaginationState import Wire.Sem.Random qualified as Rnd import Wire.UserGroupStore @@ -62,8 +61,8 @@ userGroupStoreTestInterpreter = interpret $ \case CreateUserGroup tid ng mb -> createUserGroupImpl tid ng mb GetUserGroup tid gid includeChannels -> getUserGroupImpl tid gid includeChannels - GetUserGroups req -> getUserGroupsImpl req - GetUserGroupsWithMembers req -> getUserGroupsWithMembersImpl req + GetUserGroups tid req -> getUserGroupsImpl tid req + GetUserGroupsWithMembers tid req -> getUserGroupsWithMembersImpl tid req GetUserGroupsForConv cid -> getUserGroupsForConvImpl cid UpdateUserGroup tid gid gup -> updateUserGroupImpl tid gid gup DeleteUserGroup tid gid -> deleteUserGroupImpl tid gid @@ -124,16 +123,16 @@ filterChannels includeChannels ug = then (ug :: UserGroup) {channelsCount = Just $ maybe 0 length ug.channels} else (ug :: UserGroup) {channels = mempty} -getUserGroupsImpl :: (UserGroupStoreInMemEffectConstraints r) => UserGroupPageRequest -> Sem r UserGroupPage -getUserGroupsImpl req = do - UserGroupPage pages count <- getUserGroupsWithMembersImpl req +getUserGroupsImpl :: (UserGroupStoreInMemEffectConstraints r) => TeamId -> UserGroupPageRequest -> Sem r UserGroupPage +getUserGroupsImpl tid req = do + UserGroupPage pages count <- getUserGroupsWithMembersImpl tid req pure $ UserGroupPage (map removeMembers pages) count where removeMembers :: UserGroup -> UserGroupMeta removeMembers UserGroup_ {..} = UserGroup_ {members = Const (), ..} -getUserGroupsWithMembersImpl :: (UserGroupStoreInMemEffectConstraints r) => UserGroupPageRequest -> Sem r UserGroupPageWithMembers -getUserGroupsWithMembersImpl UserGroupPageRequest {..} = do +getUserGroupsWithMembersImpl :: (UserGroupStoreInMemEffectConstraints r) => TeamId -> UserGroupPageRequest -> Sem r UserGroupPageWithMembers +getUserGroupsWithMembersImpl tid UserGroupPageRequest {..} = do meta <- ((snd <$>) . sieve . fmap (_2 %~ (filterChannels includeChannels)) . Map.toList) <$> get @UserGroupInMemState pure $ UserGroupPage meta (length meta) where @@ -142,6 +141,7 @@ getUserGroupsWithMembersImpl UserGroupPageRequest {..} = do dropBeforeStart, orderByKeys, narrowToSearchString, + narrowToManagedBy, narrowToTeam :: [((TeamId, UserGroupId), UserGroup)] -> [((TeamId, UserGroupId), UserGroup)] @@ -150,9 +150,13 @@ getUserGroupsWithMembersImpl UserGroupPageRequest {..} = do . dropBeforeStart . orderByKeys . narrowToSearchString + . narrowToManagedBy . narrowToTeam - narrowToTeam = filter (\((thisTid, _), _) -> thisTid == team) + narrowToTeam = filter (\((thisTid, _), _) -> thisTid == tid) + + narrowToManagedBy = + filter (\(_, ug) -> maybe True (== ug.managedBy) managedByFilter) narrowToSearchString = filter (\(_, ug) -> maybe True (`T.isInfixOf` userGroupNameToText ug.name) searchString) @@ -164,6 +168,8 @@ getUserGroupsWithMembersImpl UserGroupPageRequest {..} = do (PaginationSortByName _, Desc) -> (n', i') `compare` (n, i) (PaginationSortByCreatedAt _, Asc) -> (c, i) `compare` (c', i') (PaginationSortByCreatedAt _, Desc) -> (c', i') `compare` (c, i) + (PaginationOffset _, Asc) -> i `compare` i' + (PaginationOffset _, Desc) -> i' `compare` i where n = ug.name n' = ug'.name @@ -173,7 +179,9 @@ getUserGroupsWithMembersImpl UserGroupPageRequest {..} = do c' = ug'.createdAt dropBeforeStart = do - dropWhile sqlConds + case paginationState of + PaginationOffset n -> drop (fromIntegral n) + _ -> dropWhile sqlConds where sqlConds :: ((TeamId, UserGroupId), UserGroup) -> Bool sqlConds ((_, _), row) = diff --git a/libs/wire-subsystems/test/unit/Wire/TeamInvitationSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/TeamInvitationSubsystem/InterpreterSpec.hs index ea2f5faf711..0e7c71ca1f7 100644 --- a/libs/wire-subsystems/test/unit/Wire/TeamInvitationSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/TeamInvitationSubsystem/InterpreterSpec.hs @@ -97,7 +97,7 @@ runAllEffects args = . evalState (mkStdGen 3) . randomToStatefulStdGen . miniGalleyAPIAccess args.teams def - . intepreterTeamSubsystemToGalleyAPI + . interpretTeamSubsystemToGalleyAPI . discardTinyLogs . enterpriseLoginSubsystemTestInterpreter args.constGuardResult . runError diff --git a/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs index 9f21da1caef..019cd848e36 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserGroupSubsystem/InterpreterSpec.hs @@ -36,7 +36,6 @@ import Data.Set qualified as Set import Data.UUID qualified as UUID import Data.Vector qualified as V import Imports -import Numeric.Natural import Polysemy import Polysemy.Error import Polysemy.Input (Input, runInputConst) @@ -109,7 +108,7 @@ interpretDependencies initialUsers initialTeams = . runInputConst (toLocalUnsafe (Domain "example.com") ()) . runInMemoryUserGroupStore def . miniGalleyAPIAccess initialTeams def - . intepreterTeamSubsystemToGalleyAPI + . interpretTeamSubsystemToGalleyAPI . userSubsystemTestInterpreter initialUsers runDependenciesWithReturnState :: @@ -128,7 +127,7 @@ runDependenciesWithReturnState initialUsers initialTeams = . runInputConst (toLocalUnsafe (Domain "example.com") ()) . runInMemoryUserGroupStore def . miniGalleyAPIAccess initialTeams def - . intepreterTeamSubsystemToGalleyAPI + . interpretTeamSubsystemToGalleyAPI . userSubsystemTestInterpreter initialUsers expectRight :: (Show err) => Either err Property -> Property @@ -274,14 +273,14 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do getGroups (ownerId team) def - { query = Just (userGroupNameToText userGroupName) + { searchString = Just (userGroupNameToText userGroupName) } getGroupsOutsider <- try $ getGroups (ownerId otherTeam) def - { query = Just (userGroupNameToText userGroupName) + { searchString = Just (userGroupNameToText userGroupName) } pure $ getGroupAdmin === Just group1 @@ -308,13 +307,13 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do getGroups (ownerId team1) def - { query = Just (userGroupNameToText userGroupName1) + { searchString = Just (userGroupNameToText userGroupName1) } getOtherGroups <- getGroups (ownerId team1) def - { query = Just (userGroupNameToText userGroupName2) + { searchString = Just (userGroupNameToText userGroupName2) } pure $ @@ -329,10 +328,10 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do let newGroups = [newUserGroup (either undefined id $ userGroupNameFromText name) | name <- ["1", "2", "2", "33"]] groups <- (\ng -> passTime 1 >> createGroup (ownerId team1) ng) `mapM` newGroups - get0 <- getGroups (ownerId team1) def {query = Just "nope"} - get1 <- getGroups (ownerId team1) def {query = Just "1"} - get2 <- getGroups (ownerId team1) def {query = Just "2"} - get3 <- getGroups (ownerId team1) def {query = Just "3"} + get0 <- getGroups (ownerId team1) def {searchString = Just "nope"} + get1 <- getGroups (ownerId team1) def {searchString = Just "1"} + get2 <- getGroups (ownerId team1) def {searchString = Just "2"} + get3 <- getGroups (ownerId team1) def {searchString = Just "3"} pure do get0.page `shouldBe` [] @@ -342,54 +341,96 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do prop "getGroups: pagination (happy flow)" $ do \(WithMods team1 :: WithMods '[AtLeastOneNonAdmin] ArbitraryTeam) - numGroupsPre - pageSizePre -> - let numGroups = fromIntegral @Natural numGroupsPre + 1 - pageSize = - let smallify = (\case 0 -> 3; other -> other) . (`mod` (numGroups + 5)) - in PageSize . unsafeRange . smallify . fromRange . fromPageSize $ pageSizePre - in expectRight - . runDependencies (allUsers team1) (galleyTeam team1) - . interpretUserGroupSubsystem - $ do - let mkNewGroup = newUserGroup (either undefined id $ userGroupNameFromText "same name") - mkGroup = passTime 1 >> createGroup (ownerId team1) mkNewGroup - - -- groups are only distinguished by creation date - groups <- replicateM (fromIntegral numGroups) mkGroup - - results :: [UserGroupPage] <- do - let fetch mLastThing = do - p <- - getGroups - (ownerId team1) - def - { sortBy = Just SortByCreatedAt, - pageSize = Just pageSize, - lastName = fmap (userGroupNameToText . (.name)) mLastThing, - lastCreatedAt = fmap (fromUTCTimeMillis . (.createdAt)) mLastThing, - lastId = fmap (.id_) mLastThing - } + (Positive (Small (numGroups :: Int))) + (Positive (Small (pageSizeFromIntegralTotal @Int -> pageSize))) -> + expectRight + . runDependencies (allUsers team1) (galleyTeam team1) + . interpretUserGroupSubsystem + $ do + let mkNewGroup = newUserGroup (either undefined id $ userGroupNameFromText "same name") + mkGroup = passTime 1 >> createGroup (ownerId team1) mkNewGroup + + -- groups are only distinguished by creation date + groups <- replicateM numGroups mkGroup + + results :: [UserGroupPage] <- do + let fetch mLastThing = do + p <- + getGroups + (ownerId team1) + def + { paginationState = PaginationSortByCreatedAt $ (,) <$> fmap (fromUTCTimeMillis . (.createdAt)) mLastThing <*> fmap (.id_) mLastThing, + pageSize + } + if null p.page + then pure [] + else if length p.page < pageSizeToInt pageSize then pure [p] else (p :) <$> fetch (Just (last p.page)) - fetch Nothing - - let all' :: (x -> Property) -> [x] -> Property - all' mkProp = foldr (\x acc -> mkProp x .&&. acc) (True === True) - - assertLessThanOrEq :: (Show a, Ord a) => a -> a -> Property - assertLessThanOrEq x y = counterexample (show x <> "\n>\n" <> show y) $ x <= y - pure $ - -- result is complete and correct (`reverse` because `createdAt` defaults to `Desc`) - mconcat ((.page) <$> results) === (userGroupToMeta <$> reverse groups) - -- every page has the expected size - .&&. all' - (\r -> length r.page === pageSizeToInt pageSize) - (take (length results - 2) results) - .&&. all' - (\r -> length r.page `assertLessThanOrEq` pageSizeToInt pageSize) - (drop (length results - 2) results) + fetch Nothing + + let all' :: (x -> Property) -> [x] -> Property + all' mkProp = foldr (\x acc -> mkProp x .&&. acc) (True === True) + + assertLessThanOrEq :: (Show a, Ord a) => a -> a -> Property + assertLessThanOrEq x y = counterexample (show x <> "\n>\n" <> show y) $ x <= y + pure $ + Right pageSize === pageSizeFromInt 0 + .||. ( + -- result is complete and correct (`reverse` because `createdAt` defaults to `Desc`) + mconcat ((.page) <$> results) === (userGroupToMeta <$> reverse groups) + -- every page has the expected size + .&&. all' + (\r -> length r.page === pageSizeToInt pageSize) + (take (length results - 2) results) + .&&. all' + (\r -> length r.page `assertLessThanOrEq` pageSizeToInt pageSize) + (drop (length results - 2) results) + ) + + prop "getGroups: pagination via offset" $ \(WithMods team1 :: WithMods '[AtLeastOneNonAdmin] ArbitraryTeam) -> + runDependenciesFailOnError (allUsers team1) (galleyTeam team1) . interpretUserGroupSubsystem $ do + -- Create groups + groups <- forM ["1", "2", "3", "4", "5"] $ \name -> do + passTime 1 + createGroup (ownerId team1) $ newUserGroup $ UserGroupName $ unsafeRange name + let groupIds = map (.id_) groups :: [UserGroupId] + + -- Define helper to fetch all pages, providing desired + -- sortOrder and page size to the mock database + let getAllPages :: (Member UserGroupSubsystem r) => SortOrder -> Word -> Sem r [UserGroupPage] + getAllPages sortOrder' pageSize' = go 0 + where + go :: (Member UserGroupSubsystem r) => Word -> Sem r [UserGroupPage] + go offset = do + p <- + getGroups + (ownerId team1) + def + { paginationState = PaginationOffset offset, + pageSize = PageSize $ unsafeRange $ fromIntegral pageSize', + sortOrder = sortOrder' + } + let len = length p.page + if + | len > 0 && len < fromIntegral pageSize' -> pure [p] + | len == 0 -> pure [] + | otherwise -> (p :) <$> go (offset + pageSize') + + ascendingPages :: [UserGroupPage] <- getAllPages Asc 2 + descendingPages :: [UserGroupPage] <- getAllPages Desc 3 + exactlyOnePage :: [UserGroupPage] <- getAllPages Desc 5 + + pure do + -- Page sizes are as expected + map (length . (.page)) ascendingPages `shouldBe` [2, 2, 1] + map (length . (.page)) descendingPages `shouldBe` [3, 2] + map (length . (.page)) exactlyOnePage `shouldBe` [5] + + -- Sort order is accounted for, pages do not overlap + (map (.id_) . (.page) =<< ascendingPages) `shouldBe` sort groupIds + (map (.id_) . (.page) =<< descendingPages) `shouldBe` sortBy (comparing Down) groupIds it "getGroups (ordering)" $ do WithMods team1 :: WithMods '[AtLeastOneNonAdmin] ArbitraryTeam <- generate arbitrary @@ -412,15 +453,15 @@ spec = timeoutHook $ describe "UserGroupSubsystem.Interpreter" do getGroups (ownerId team1) def - { sortBy = Just SortByName, - sortOrder = Just Desc + { paginationState = PaginationSortByName Nothing, + sortOrder = Desc } sortByCreatedAtAsc <- getGroups (ownerId team1) def - { sortBy = Just SortByCreatedAt, - sortOrder = Just Asc + { paginationState = PaginationSortByCreatedAt Nothing, + sortOrder = Asc } let expectSortByDefaults = [[group1b, group2b, group3b], [group1a, group2a, group3a]] diff --git a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs index 28c9463abf3..edfb8af1f83 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs @@ -50,7 +50,7 @@ import Test.QuickCheck import Wire.API.EnterpriseLogin import Wire.API.Federation.Error import Wire.API.Team.Collaborator -import Wire.API.Team.Feature +import Wire.API.Team.Feature (FeatureStatus (..), LockStatus (..), LockableFeature (..), MlsE2EIdConfig, npUpdate) import Wire.API.Team.Member import Wire.API.Team.Permission import Wire.API.Team.Role @@ -102,7 +102,7 @@ spec = describe "UserSubsystem.Interpreter" do Nothing (mkUserFromStored domain miniLocale targetUser) defUserLegalHoldStatus - | targetUser <- users + | targetUser <- users ] expectedLocalProfiles = mkExpectedProfiles localDomain localTargetUsers expectedProfiles1 = mkExpectedProfiles remoteDomain1 targetUsers1 diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index 96866075479..ff58ed19431 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -21,6 +21,14 @@ extra-source-files: test/resources/**/*.txt test/resources/**/*.yaml +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +flag nix-dev-env + description: In a Nix dev environment. + default: False + manual: True + common common-all default-language: GHC2021 ghc-options: @@ -80,6 +88,7 @@ common common-all , amazonka , amazonka-core , amazonka-ses + , amazonka-sqs , amqp , async , attoparsec @@ -106,6 +115,7 @@ common common-all , extended , extra , file-embed + , galley-types , hashable , HaskellNet , HaskellNet-SSL @@ -141,6 +151,7 @@ common common-all , polysemy-wire-zoo , profunctors , prometheus-client + , proto-lens , QuickCheck , raw-strings-qq , resource-pool @@ -166,6 +177,7 @@ common common-all , token-bucket , transformers , types-common + , types-common-journal , unliftio , unordered-containers , uri-bytestring @@ -263,6 +275,10 @@ library Wire.InternalEvent Wire.InvitationStore Wire.InvitationStore.Cassandra + Wire.LegalHoldStore + Wire.LegalHoldStore.Cassandra + Wire.LegalHoldStore.Cassandra.Queries + Wire.LegalHoldStore.Env Wire.ListItems Wire.NotificationSubsystem Wire.NotificationSubsystem.Interpreter @@ -278,10 +294,13 @@ library Wire.PropertyStore.Cassandra Wire.PropertySubsystem Wire.PropertySubsystem.Interpreter + Wire.ProposalStore + Wire.ProposalStore.Cassandra Wire.RateLimit Wire.RateLimit.Interpreter Wire.Rpc Wire.ScimSubsystem + Wire.ScimSubsystem.Error Wire.ScimSubsystem.Interpreter Wire.ServiceStore Wire.ServiceStore.Cassandra @@ -298,8 +317,14 @@ library Wire.TeamInvitationSubsystem Wire.TeamInvitationSubsystem.Error Wire.TeamInvitationSubsystem.Interpreter + Wire.TeamJournal + Wire.TeamJournal.Aws + Wire.TeamStore + Wire.TeamStore.Cassandra + Wire.TeamStore.Cassandra.Queries Wire.TeamSubsystem Wire.TeamSubsystem.GalleyAPI + Wire.TeamSubsystem.Interpreter Wire.TeamSubsystem.Util Wire.UserGroupStore Wire.UserGroupStore.Postgres @@ -405,6 +430,7 @@ library , time-out , time-units , tinylog + , tls , token-bucket , transformers , types-common @@ -421,14 +447,14 @@ library , zauth test-suite wire-subsystems-tests - import: common-all - type: exitcode-stdio-1.0 + import: common-all + type: exitcode-stdio-1.0 -- include everything in source dirs we want to watch when running -- `ghcid --command 'cabal repl test:wire-subsystems-tests' --test='main'`. - hs-source-dirs: test/unit - main-is: ../Main.hs - ghc-options: -fplugin=Polysemy.Plugin -Wno-x-partial -threaded + hs-source-dirs: test/unit + main-is: ../Main.hs + ghc-options: -fplugin=Polysemy.Plugin -Wno-x-partial -threaded -- cabal-fmt: expand test/unit other-modules: @@ -486,7 +512,9 @@ test-suite wire-subsystems-tests Wire.Util Wire.VerificationCodeSubsystem.InterpreterSpec - build-tool-depends: hspec-discover:hspec-discover + if !flag(nix-dev-env) + build-tool-depends: hspec-discover:hspec-discover + build-depends: , hspec , QuickCheck diff --git a/nix/default.nix b/nix/default.nix index b945b4adc9a..71f9845b8d9 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -1,15 +1,5 @@ +{ pkgs, pkgs_24_11, bomDependenciesDrv, inputs, }: let - sources = import ./sources.nix; - - pkgs = import sources.nixpkgs { - config.allowUnfree = true; - overlays = [ - # All wire-server specific packages - (import ./overlay.nix) - (import ./overlay-docs.nix) - ]; - }; - profileEnv = pkgs.writeTextFile { name = "profile-env"; destination = "/.profile"; @@ -20,7 +10,7 @@ let ''; }; - wireServer = import ./wire-server.nix pkgs; + wireServer = import ./wire-server.nix { inherit pkgs pkgs_24_11 bomDependenciesDrv inputs; }; nginz = pkgs.callPackage ./nginz.nix { }; # packages necessary to build wire-server docs diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index 8184e905762..5481009262b 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -1,23 +1,20 @@ # How to add a git pin: # -# 1. If your target git repository has only package with the cabal file at the +# 1. Add the target git repo to the inputs section of flake.nix like this: +# = { +# url = "github:/?rev="; +# flake = false; +# }; +# 2. If your target git repository has only package with the cabal file at the # root, add it like this under 'gitPins': # = { -# src = fetchgit { -# url = ""; -# rev = ""; -# sha256 = ""; -# }; +# src = inputs.; # }; # -# 2. If your target git repsitory has many packages, add it like this under 'gitPins': +# 3. If your target git repsitory has many packages, add it like this under 'gitPins': # # = { -# src = fetchgit { -# url = ""; -# rev = ""; -# sha256 = ""; -# }; +# src = inputs.; # packages = { # = ""; # = ""; @@ -25,38 +22,30 @@ # }; # }; # -# 3. Run 'nix build -f ./nix wireServer.haskellPackagesUnoptimizedNoDocs.'. -# This should produce an error saying expected sha and the actual sha. Replace the empty string in 'sha256' with the actual -# sha. -# # How to update a git pin: # # 1. Determine the new commit ID/SHA of the git repository that you want to pin -# and update the 'rev' field of the pin under 'gitPins'. -# -# 2. Update 'sha256' field under `fetchgit` to be an empty string. (This step is optional: -# since the sha256 has changed, the error will be the same if you remove it or if you leave the -# old value in place.) -# -# 3. Run step 3. from how to add a git pin. +# and update the 'rev' param in the URL in the inputs section of the flake.nix. # # How to add a hackage pin: # # 1. Add your package like this, under 'hackagePins': # = { # version = ""; -# sha256 = "sha256-gD9b9AXpLkpPSAeg8oPBU7tsHtSNQjxIZKBo+7+r3+c="; +# sha256 = ""; # }; # -# 2. Run step 3. from how to add a git pin. +# 2. Run 'nix build '.#wireServer.haskellPackagesUnoptimizedNoDocs.'. +# This should produce an error saying expected sha and the actual sha. Replace the empty string in 'sha256' with the actual +# sha. # # How to update a hackage pin: # # 1. Update version number. # 2. Make the 'sha256' blank string. -# 3. Run step 3. from how to add a git pin. -{ lib, fetchgit, pkgs }: hself: hsuper: +# 3. Run step 2. from how to add a hackage pin. +{ lib, inputs }: hself: hsuper: let gitPins = { # ---------------- @@ -64,11 +53,7 @@ let # ---------------- cryptobox-haskell = { - src = fetchgit { - url = "https://github.com/wireapp/cryptobox-haskell"; - rev = "7546a1a25635ef65183e3d44c1052285e8401608"; - hash = "sha256-9mMVgmMB1NWCPm/3inLeF4Ouiju0uIb/92UENoP88TU="; - }; + src = inputs.cryptobox-haskell; }; # -------------------- @@ -76,40 +61,24 @@ let # -------------------- bloodhound = { - src = fetchgit { - url = "https://github.com/wireapp/bloodhound"; - rev = "dac0f1384b335ce35dc026bf8154e574b1a15d62"; - hash = "sha256-E3co9FGZP135T3RocX4vbUELbbgGbYddD8CcVNUzHu8="; - }; + src = inputs.bloodhound; }; # Merged PR https://github.com/dylex/hsaml2/pull/20 hsaml2 = { - src = fetchgit { - url = "https://github.com/dylex/hsaml2"; - rev = "874627ad22e69afe4d9a797e39633ffb30697c78"; - hash = "sha256-gufEAC7fFqafG8dXkGIOSfAcVv+ZWkawmBgUV+Ics2s="; - }; + src = inputs.hsaml2; }; # PR: https://github.com/informatikr/hedis/pull/224 # PR: https://github.com/informatikr/hedis/pull/226 # PR: https://github.com/informatikr/hedis/pull/227 hedis = { - src = fetchgit { - url = "https://github.com/wireapp/hedis"; - rev = "00d7fbf5f19b812b9e64e12be8860c4741be8558"; - sha256 = "sha256-BwcqQZf2GaEn2i6o9bVl+jiu/CjShYlHCmO81bYfc8Y="; - }; + src = inputs.hedis; }; # Our fork because we need to a few special things http-client = { - src = fetchgit { - url = "https://github.com/wireapp/http-client"; - rev = "37494bb9a89dd52f97a8dc582746c6ff52943934"; - hash = "sha256-z47GlT+tHsSlRX4ApSGQIpOpaZiBeqr72/tWuvzw8tc="; - }; + src = inputs.http-client; packages = { "http-client" = "http-client"; "http-client-tls" = "http-client-tls"; @@ -120,48 +89,30 @@ let # PR: https://github.com/hspec/hspec-wai/pull/49 hspec-wai = { - src = fetchgit { - url = "https://github.com/wireapp/hspec-wai"; - rev = "08176f07fa893922e2e78dcaf996c33d79d23ce2"; - hash = "sha256-Nc5POjA+mJt7Vi3drczEivGsv9PXeVOCSwp21lLmz58="; - }; + src = inputs.hspec-wai; }; # PR: https://gitlab.com/twittner/cql/-/merge_requests/11 cql = { - src = fetchgit { - url = "https://github.com/wireapp/cql"; - rev = "abbd2739969d17a909800f282d10d42a254c4e3b"; - hash = "sha256-2MYwZKiTdwgjJdLNvECi7gtcIo+3H4z1nYzen5x0lgU="; - }; + src = inputs.cql; }; # PR: https://gitlab.com/twittner/cql-io/-/merge_requests/20 cql-io = { - src = fetchgit { - url = "https://github.com/wireapp/cql-io"; - rev = "c2b6aa995b5817ed7c78c53f72d5aa586ef87c36"; - hash = "sha256-DMRWUq4yorG5QFw2ZyF/DWnRjfnzGupx0njTiOyLzPI="; - }; + src = inputs.cql-io; }; # missing upstream PR, this will get removed when completing # servantification + # + # this is currently still used/needed in the proxy service wai-predicates = { - src = fetchgit { - url = "https://github.com/wireapp/wai-predicates"; - rev = "ff95282a982ab45cced70656475eaf2cefaa26ea"; - hash = "sha256-x2XSv2+/+DG9FXN8hfUWGNIO7V4iBhlzYz19WWKaLKQ="; - }; + src = inputs.wai-predicates; }; # PR: https://github.com/UnkindPartition/tasty/pull/351 tasty = { - src = fetchgit { - url = "https://github.com/wireapp/tasty"; - rev = "97df5c1db305b626ffa0b80055361b7b28e69cec"; - hash = "sha256-oACehxazeKgRr993gASRbQMf74heh5g0B+70ceAg17I="; - }; + src = inputs.tasty; packages = { tasty-hunit = "hunit"; }; @@ -170,96 +121,25 @@ let # sets the required flag for HTTP request bodies. # PR: https://github.com/biocad/servant-openapi3/pull/49 servant-openapi3 = { - src = fetchgit { - url = "https://github.com/wireapp/servant-openapi3"; - rev = "0db0095040df2c469a48f5b8724595f82afbad0c"; - hash = "sha256-iKMWd+qm8hHhKepa13VWXDPCpTMXxoOwWyoCk4lLlIY="; - }; + src = inputs.servant-openapi3; }; # we need HEAD, the latest release is too old postie = { - src = fetchgit { - url = "https://github.com/alexbiehl/postie"; - rev = "13404b8cb7164cd9010c9be6cda5423194dd0c06"; - hash = "sha256-nNivtyBpr4DFsbaXxlCznX+MYtzNshU7vfVpnhMh52c="; - }; + src = inputs.postie; }; tinylog = { - src = fetchgit { - url = "https://github.com/wireapp/tinylog.git"; - rev = "9609104263e8cd2a631417c1c3ef23e090de0d09"; - hash = "sha256-htEIJY+LmIMACVZrflU60+X42/g14NxUyFM7VJs4E6w="; - }; + src = inputs.tinylog; }; # PR: https://github.com/ocharles/tasty-ant-xml/pull/32 tasty-ant-xml = { - src = fetchgit { - url = "https://github.com/wireapp/tasty-ant-xml"; - rev = "11c53e976e2e941f25a33e8768669eb576d19ea8"; - hash = "sha256-Aj/iTVECsCGq4f+32FXWyYj/iLH5e4Gm4hYRmewnJJM="; - }; + src = inputs.tasty-ant-xml; }; text-icu-translit = { - src = pkgs.fetchFromGitHub { - owner = "wireapp"; - repo = "text-icu-translit"; - rev = "317bbd27ea5ae4e7f93836ee9ca664f9bde7c583"; - hash = "sha256-E35PVxi/4iJFfWts3td52KKZKQt4dj9KFP3SvWG77Cc="; - }; - }; - - # open PR https://github.com/yesodweb/wai/pull/958 for sending connection: close when closing connection - warp = { - packages.warp = "warp"; - src = pkgs.fetchFromGitHub { - owner = "yesodweb"; - repo = "wai"; - rev = "8b20c9db265a202a2c7ba2a9ec8786a1ee59957b"; - hash = "sha256-fKUSiRl38FKY1gFSmbksktoqoLfQrDxRRWEh4k+RRW4="; - }; - }; - - # this contains an important fix to the initialization of the window size - # and should be switched to upstream as soon as we can - # version = "5.2.5"; - # This patch also includes suppressing ConnectionIsClosed - http2 = { - src = fetchgit { - url = "https://github.com/wireapp/http2"; - rev = "45653e3caab0642e539fab2681cb09402aae29ca"; - hash = "sha256-L90PQtDw/JFwyltSVFvmfjTAb0ZLhFt9Hl0jbzn+cFQ="; - }; - }; - - # hs-opentelemetry-* has not been released for a while on hackage. Thus, - # we're following main. - hs-opentelemetry = { - src = fetchgit { - url = "https://github.com/iand675/hs-opentelemetry"; - rev = "ee8a6dad7db306eb67748ddcd77df4974ad8259e"; - hash = "sha256-UirBRxY9gAv5x/t87RZcWCy6GtsigzFMABKqrhS9b7s="; - }; - packages = { - hs-opentelemetry-sdk = "sdk"; - hs-opentelemetry-api = "api"; - hs-opentelemetry-propagator-datadog = "propagators/datadog"; - hs-opentelemetry-instrumentation-http-client = "instrumentation/http-client"; - hs-opentelemetry-instrumentation-wai = "instrumentation/wai"; - hs-opentelemetry-exporter-otlp = "exporters/otlp"; - hs-opentelemetry-utils-exceptions = "utils/exceptions"; - }; - }; - - HaskellNet = { - src = fetchgit { - url = "https://github.com/wireapp/HaskellNet"; - rev = "74cde03b4beb09794a6120ea5321a09430bcd2c7"; - hash = "sha256-VIM60sXCVC25ULf/2yPvqANK/h9BY6dEYY3o3/xiEEQ="; - }; + src = inputs.text-icu-translit; }; # Our fork of 2.0.0. This release hasn't been updated for a while and Nix @@ -269,16 +149,16 @@ let # N.B. only the listed packages work. If you want to use another: # - list it here # - patch it on the fork (if required) + # + # Can't currently be removed because amazonka-dynamodb-attributevalue + # does not exist on hackage amazonka = { - src = fetchgit { - url = "https://github.com/wireapp/amazonka"; - rev = "d98cefc04bcc7076a915076a322ab5905c6a4945"; - hash = "sha256-8HNHoTUaLi5lyOrKYybacZsDSHrju9/oo+Lf/YulbIo="; - }; + src = inputs.amazonka; packages = { amazonka = "lib/amazonka"; amazonka-core = "lib/amazonka-core"; amazonka-dynamodb = "lib/services/amazonka-dynamodb"; + amazonka-dynamodb-attributevalue = "lib/amazonka-dynamodb-attributevalue"; amazonka-s3 = "lib/services/amazonka-s3"; amazonka-sts = "lib/services/amazonka-sts"; amazonka-sqs = "lib/services/amazonka-sqs"; @@ -294,41 +174,18 @@ let hackagePins = { # start pinned dependencies for http2 http-semantics = { - version = "0.1.2"; - sha256 = "sha256-S4rGBCIKVPpLPumLcVzrPONrbWm8VBizqxI3dXNIfr0="; - }; - - tasty-ant-xml = { - version = "1.1.9"; - sha256 = "sha256-aB7B61XSAZ5V+uW+QBe/PKBmhdFfX3OoOjDE9jB7Mek="; + version = "0.4.0"; + sha256 = "sha256-rh0z51EKvsu5rQd5n2z3fSRjjEObouNZSBPO9NFYOF0="; }; network-run = { - version = "0.3.0"; - sha256 = "sha256-FP2GZKwacC+TLLwEIVgKBtnKplYPf5xOIjDfvlbQV0o="; - }; - time-manager = { - version = "0.1.0"; - sha256 = "sha256-WRe9LZrOIPJVBFk0vMN2IMoxgP0a0psQCiCiOFWJc74="; - }; - hasql = { - version = "1.9.1.2"; - sha256 = "sha256-W2pAC3wLIizmbspWHeWDQqb5AROtwA8Ok+lfZtzTlQg="; - }; - - hasql-pool = { - version = "1.3.0.1"; - sha256 = "sha256-TtNrs1z8L39WnX8277V97g9Ot1DwutKLrAB1JOjQQoQ="; - }; - - postgresql-syntax = { - version = "0.4.1.3"; - sha256 = "sha256-afC4lQUPUL5cHe+7vTG1lFZ4wWyQzdh9MEhMT/TtP5c="; + version = "0.5.0"; + sha256 = "sha256-vbXh+CzxDsGApjqHxCYf/ijpZtUCApFbkcF5gyN0THU="; }; - network-control = { - version = "0.1.0"; - sha256 = "sha256-D6pKb6+0Pr08FnObGbXBVMv04ys3N731p7U+GYH1oEg="; + time-manager = { + version = "0.2.4"; + sha256 = "sha256-sAt/331YLQ2IU3z90aKYSq1nxoazv87irsuJp7ZG3pw="; }; # end pinned dependencies for http2 @@ -339,6 +196,21 @@ let version = "2.0.0.0"; sha256 = "sha256-SQyFjl1Zf4vnntjZHJpf46gMR3LXWCQAMsR56NdsvRA="; }; + + # Pin uri-bytestring: newer parser rejects unescaped Set-Cookie in SSO mobile redirect query, breaking Spar’s URI substitution; stick to 0.3.3.1 for now + uri-bytestring = { + version = "0.3.3.1"; + sha256 = "sha256-jgSTBBDcxRQ0tjs0wTyvEpEAkGA7npJKjdXDT81VpT4="; + }; + + warp = { + version = "3.4.12"; + sha256 = "sha256-Y9xQ1wBbBtSZ4qw3yTGSYX27qi2uFRDJVtAdmQqRnFQ="; + }; + http2 = { + version = "5.4.0"; + sha256 = "sha256-PeEWVd61bQ8G7LvfLeXklzXqNJFaAjE2ecRMWJZESPE="; + }; }; # Name -> Source -> Maybe Subpath -> Drv mkGitDrv = name: src: subpath: diff --git a/nix/local-haskell-packages.nix b/nix/local-haskell-packages.nix index 00e5d82aaec..2e065b6d403 100644 --- a/nix/local-haskell-packages.nix +++ b/nix/local-haskell-packages.nix @@ -50,6 +50,7 @@ inconsistencies = hself.callPackage ../tools/db/inconsistencies/default.nix { inherit gitignoreSource; }; migrate-features = hself.callPackage ../tools/db/migrate-features/default.nix { inherit gitignoreSource; }; migrate-sso-feature-flag = hself.callPackage ../tools/db/migrate-sso-feature-flag/default.nix { inherit gitignoreSource; }; + mls-users = hself.callPackage ../tools/db/mls-users/default.nix { inherit gitignoreSource; }; move-team = hself.callPackage ../tools/db/move-team/default.nix { inherit gitignoreSource; }; phone-users = hself.callPackage ../tools/db/phone-users/default.nix { inherit gitignoreSource; }; repair-brig-clients-table = hself.callPackage ../tools/db/repair-brig-clients-table/default.nix { inherit gitignoreSource; }; diff --git a/nix/manual-overrides.nix b/nix/manual-overrides.nix index 189db41faa6..ea0449d5306 100644 --- a/nix/manual-overrides.nix +++ b/nix/manual-overrides.nix @@ -8,13 +8,16 @@ hself: hsuper: { # FUTUREWORK: investigate whether all of these tests need to fail # ---------------- + # tests don't work, but only in a flake + saml2-web-sso = hlib.dontCheck hsuper.saml2-web-sso; + # test suite doesn't compile and needs network access bloodhound = hlib.dontCheck hsuper.bloodhound; # tests need network access, cabal2nix disables haddocks cql-io = hlib.doHaddock (hlib.dontCheck hsuper.cql-io); - quickcheck-state-machine = hlib.dontCheck hsuper.quickcheck-state-machine; + quickcheck-state-machine = hlib.markUnbroken (hlib.dontCheck hsuper.quickcheck-state-machine); # Tests require a running redis hedis = hlib.dontCheck hsuper.hedis; @@ -25,7 +28,7 @@ hself: hsuper: { hasql = hlib.dontCheck hsuper.hasql; hasql-pool = hlib.dontCheck hsuper.hasql-pool; hasql-migration = hlib.markUnbroken (hlib.dontCheck hsuper.hasql-migration); - hasql-transaction = hlib.dontCheck hsuper.hasql-transaction_1_2_0_1; + hasql-transaction = hlib.dontCheck hsuper.hasql-transaction; # users 1.2.1 from nixpkgs postgresql-binary = hlib.dontCheck (hsuper.postgresql-binary); # --------------------- @@ -37,6 +40,10 @@ hself: hsuper: { bytestring-arbitrary = hlib.markUnbroken (hlib.doJailbreak hsuper.bytestring-arbitrary); lens-datetime = hlib.markUnbroken (hlib.doJailbreak hsuper.lens-datetime); postie = hlib.doJailbreak hsuper.postie; + lrucaching = hlib.doJailbreak (hlib.markUnbroken hsuper.lrucaching); + # added servant-openapi3 because the version bounds of some dependent packages + # of our pin exclude the versions in our current nixpkgs + servant-openapi3 = hlib.doJailbreak (hlib.dontCheck hsuper.servant-openapi3); # the libsodium haskell library is incompatible with the new version of the libsodium c library # that nixpkgs has - this downgrades libsodium from 1.0.19 to 1.0.18 @@ -53,13 +60,18 @@ hself: hsuper: { } ))); + # hs-opentelemetry pin removal bumps API -> 0.3.0.0 and SDK -> 0.1.0.1 from the pinned commit; instrumentation stays at 0.1.1.0/0.1.0.1. + hs-opentelemetry-instrumentation-wai = hlib.markUnbroken (hlib.doJailbreak hsuper.hs-opentelemetry-instrumentation-wai); + hs-opentelemetry-instrumentation-conduit = hlib.markUnbroken (hlib.doJailbreak hsuper.hs-opentelemetry-instrumentation-conduit); + hs-opentelemetry-instrumentation-http-client = hlib.doJailbreak hsuper.hs-opentelemetry-instrumentation-http-client; + hs-opentelemetry-utils-exceptions = hlib.markUnbroken (hlib.doJailbreak hsuper.hs-opentelemetry-utils-exceptions); + # ------------------------------------ # okay but marked broken (nixpkgs bug) # (we can unfortunately not do anything here but update nixpkgs) # ------------------------------------ template = hlib.markUnbroken hsuper.template; system-linux-proc = hlib.markUnbroken hsuper.system-linux-proc; - lrucaching = hlib.markUnbroken hsuper.lrucaching; # ----------------- # version overrides @@ -68,12 +80,6 @@ hself: hsuper: { # warp requires curl in its testsuite warp = hlib.addTestToolDepends hsuper.warp [ curl ]; - # cabal multirepl requires Cabal 3.12 - Cabal = hsuper.Cabal_3_12_1_0; - Cabal-syntax = hsuper.Cabal-syntax_3_14_2_0; - - text-builder = hlib.doJailbreak (hsuper.text-builder_1_0_0_4); - # ----------------- # flags and patches # (these are fine) diff --git a/nix/overlay.nix b/nix/overlay.nix index 36af565994b..2ccc18888e2 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -1,57 +1,3 @@ -let - staticBinaryInTarball = { stdenv, fetchurl, pname, version, linuxAmd64Url, linuxAmd64Sha256, darwinAmd64Url, darwinAmd64Sha256, binPath ? pname }: - stdenv.mkDerivation { - inherit pname version; - - src = - if stdenv.isDarwin - then - fetchurl - { - url = darwinAmd64Url; - sha256 = darwinAmd64Sha256; - } - else - fetchurl { - url = linuxAmd64Url; - sha256 = linuxAmd64Sha256; - }; - - installPhase = '' - mkdir -p $out/bin - cp ${binPath} $out/bin - ''; - }; - - staticBinary = { stdenv, fetchurl, pname, version, linuxAmd64Url, linuxAmd64Sha256, darwinAmd64Url, darwinAmd64Sha256, binPath ? pname }: - stdenv.mkDerivation { - inherit pname version; - - src = - if stdenv.isDarwin - then - fetchurl - { - url = darwinAmd64Url; - sha256 = darwinAmd64Sha256; - } - else - fetchurl { - url = linuxAmd64Url; - sha256 = linuxAmd64Sha256; - }; - phases = [ "installPhase" "patchPhase" ]; - - installPhase = '' - mkdir -p $out/bin - cp $src $out/bin/${binPath} - chmod +x $out/bin/${binPath} - ''; - }; - - sources = import ./sources.nix; -in - self: super: { cryptobox = self.callPackage ./pkgs/cryptobox { }; @@ -83,4 +29,16 @@ self: super: { rabbitmqadmin = super.callPackage ./pkgs/rabbitmqadmin { }; sbomqs = super.callPackage ./pkgs/sbomqs { }; + + # Disable hlint in HLS to get around this bug: + # https://github.com/haskell/haskell-language-server/issues/4674 + haskell = super.haskell // { + packages = super.haskell.packages // { + ghc910 = super.haskell.packages.ghc910.override { + overrides = hfinal: hprev: { + haskell-language-server = self.haskell.lib.disableCabalFlag hprev.haskell-language-server "hlint"; + }; + }; + }; + }; } diff --git a/nix/sources.json b/nix/sources.json deleted file mode 100644 index 45723e16396..00000000000 --- a/nix/sources.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "nixpkgs": { - "branch": "nixpkgs-unstable", - "description": "Nix Packages collection", - "homepage": "https://github.com/NixOS/nixpkgs", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "c53baa6685261e5253a1c355a1b322f82674a824", - "sha256": "07iy9la8yc56477q0rh6bmkcgnm57n267g19i2si5yb2j320gpj7", - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/c53baa6685261e5253a1c355a1b322f82674a824.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - } -} diff --git a/nix/sources.nix b/nix/sources.nix deleted file mode 100644 index fe3dadf7ebb..00000000000 --- a/nix/sources.nix +++ /dev/null @@ -1,198 +0,0 @@ -# This file has been generated by Niv. - -let - - # - # The fetchers. fetch_ fetches specs of type . - # - - fetch_file = pkgs: name: spec: - let - name' = sanitizeName name + "-src"; - in - if spec.builtin or true then - builtins_fetchurl { inherit (spec) url sha256; name = name'; } - else - pkgs.fetchurl { inherit (spec) url sha256; name = name'; }; - - fetch_tarball = pkgs: name: spec: - let - name' = sanitizeName name + "-src"; - in - if spec.builtin or true then - builtins_fetchTarball { name = name'; inherit (spec) url sha256; } - else - pkgs.fetchzip { name = name'; inherit (spec) url sha256; }; - - fetch_git = name: spec: - let - ref = - spec.ref or ( - if spec ? branch then "refs/heads/${spec.branch}" else - if spec ? tag then "refs/tags/${spec.tag}" else - abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!" - ); - submodules = spec.submodules or false; - submoduleArg = - let - nixSupportsSubmodules = builtins.compareVersions builtins.nixVersion "2.4" >= 0; - emptyArgWithWarning = - if submodules - then - builtins.trace - ( - "The niv input \"${name}\" uses submodules " - + "but your nix's (${builtins.nixVersion}) builtins.fetchGit " - + "does not support them" - ) - { } - else { }; - in - if nixSupportsSubmodules - then { inherit submodules; } - else emptyArgWithWarning; - in - builtins.fetchGit - ({ url = spec.repo; inherit (spec) rev; inherit ref; } // submoduleArg); - - fetch_local = spec: spec.path; - - fetch_builtin-tarball = name: throw - ''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`. - $ niv modify ${name} -a type=tarball -a builtin=true''; - - fetch_builtin-url = name: throw - ''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`. - $ niv modify ${name} -a type=file -a builtin=true''; - - # - # Various helpers - # - - # https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695 - sanitizeName = name: - ( - concatMapStrings (s: if builtins.isList s then "-" else s) - ( - builtins.split "[^[:alnum:]+._?=-]+" - ((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name) - ) - ); - - # The set of packages used when specs are fetched using non-builtins. - mkPkgs = sources: system: - let - sourcesNixpkgs = - import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; }; - hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath; - hasThisAsNixpkgsPath = == ./.; - in - if builtins.hasAttr "nixpkgs" sources - then sourcesNixpkgs - else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then - import { } - else - abort - '' - Please specify either (through -I or NIX_PATH=nixpkgs=...) or - add a package called "nixpkgs" to your sources.json. - ''; - - # The actual fetching function. - fetch = pkgs: name: spec: - - if ! builtins.hasAttr "type" spec then - abort "ERROR: niv spec ${name} does not have a 'type' attribute" - else if spec.type == "file" then fetch_file pkgs name spec - else if spec.type == "tarball" then fetch_tarball pkgs name spec - else if spec.type == "git" then fetch_git name spec - else if spec.type == "local" then fetch_local spec - else if spec.type == "builtin-tarball" then fetch_builtin-tarball name - else if spec.type == "builtin-url" then fetch_builtin-url name - else - abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}"; - - # If the environment variable NIV_OVERRIDE_${name} is set, then use - # the path directly as opposed to the fetched source. - replace = name: drv: - let - saneName = stringAsChars (c: if (builtins.match "[a-zA-Z0-9]" c) == null then "_" else c) name; - ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}"; - in - if ersatz == "" then drv else - # this turns the string into an actual Nix path (for both absolute and - # relative paths) - if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}"; - - # Ports of functions for older nix versions - - # a Nix version of mapAttrs if the built-in doesn't exist - mapAttrs = builtins.mapAttrs or ( - f: set: with builtins; - listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)) - ); - - # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295 - range = first: last: if first > last then [ ] else builtins.genList (n: first + n) (last - first + 1); - - # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 - stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); - - # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269 - stringAsChars = f: s: concatStrings (map f (stringToCharacters s)); - concatMapStrings = f: list: concatStrings (map f list); - concatStrings = builtins.concatStringsSep ""; - - # https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331 - optionalAttrs = cond: as: if cond then as else { }; - - # fetchTarball version that is compatible between all the versions of Nix - builtins_fetchTarball = { url, name ? null, sha256 }@attrs: - let - inherit (builtins) lessThan nixVersion fetchTarball; - in - if lessThan nixVersion "1.12" then - fetchTarball ({ inherit url; } // (optionalAttrs (name != null) { inherit name; })) - else - fetchTarball attrs; - - # fetchurl version that is compatible between all the versions of Nix - builtins_fetchurl = { url, name ? null, sha256 }@attrs: - let - inherit (builtins) lessThan nixVersion fetchurl; - in - if lessThan nixVersion "1.12" then - fetchurl ({ inherit url; } // (optionalAttrs (name != null) { inherit name; })) - else - fetchurl attrs; - - # Create the final "sources" from the config - mkSources = config: - mapAttrs - ( - name: spec: - if builtins.hasAttr "outPath" spec - then - abort - "The values in sources.json should not have an 'outPath' attribute" - else - spec // { outPath = replace name (fetch config.pkgs name spec); } - ) - config.sources; - - # The "config" used by the fetchers - mkConfig = - { sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null - , sources ? if sourcesFile == null then { } else builtins.fromJSON (builtins.readFile sourcesFile) - , system ? builtins.currentSystem - , pkgs ? mkPkgs sources system - }: rec { - # The sources, i.e. the attribute set of spec name to spec - inherit sources; - - # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers - inherit pkgs; - }; - -in -mkSources (mkConfig { }) // { __functor = _: settings: mkSources (mkConfig settings); } diff --git a/nix/wire-server.nix b/nix/wire-server.nix index 5063f8c4f84..dd2b183877f 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -42,7 +42,8 @@ # Using these tweaks we can get a haskell package set which has wire-server # components and the required dependencies. We then use this package set along # with nixpkgs' dockerTools to make derivations for docker images that we need. -pkgs: + +{ pkgs, pkgs_24_11, bomDependenciesDrv, inputs, }: let inherit (pkgs) lib; hlib = pkgs.haskell.lib; @@ -94,9 +95,7 @@ let inherit (lib) attrsets; pinnedPackages = import ./haskell-pins.nix { - inherit pkgs; - inherit (pkgs) fetchgit; - inherit lib; + inherit lib inputs; }; localPackages = { enableOptimization, enableDocs, enableTests }: hsuper: hself: @@ -324,43 +323,52 @@ let ]; images = localMods@{ enableOptimization, enableDocs, enableTests }: - let exes = staticExecs localMods; + let + exes = staticExecs localMods; + allImages = attrsets.mapAttrs + (execName: drv: + pkgs.dockerTools.streamLayeredImage { + name = "quay.io/wire/${execName}"; + maxLayers = 10; + contents = [ + pkgs.cacert + pkgs.iana-etc + pkgs.dumb-init + pkgs.dockerTools.fakeNss + pkgs.dockerTools.usrBinEnv + drv + tmpDir + ] ++ debugUtils ++ pkgs.lib.optionals (builtins.hasAttr execName (extraContents exes)) (builtins.getAttr execName (extraContents exes)); + # 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 = [ "${pkgs.dumb-init}/bin/dumb-init" "--" "${drv}/bin/${execName}" ]; + Env = [ + "SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt" + "LOCALE_ARCHIVE=${pkgs.glibcLocales}/lib/locale/locale-archive" + "LANG=en_GB.UTF-8" + # Use stable conventions for tracing http in opentelemetry + # https://opentelemetry.io/blog/2023/http-conventions-declared-stable/#migration-plan + "OTEL_SEMCONV_STABILITY_OPT_IN=http" + ]; + User = "65534"; + }; + } + ) + exes; in - attrsets.mapAttrs - (execName: drv: - pkgs.dockerTools.streamLayeredImage { - name = "quay.io/wire/${execName}"; - maxLayers = 10; - contents = [ - pkgs.cacert - pkgs.iana-etc - pkgs.dumb-init - pkgs.dockerTools.fakeNss - pkgs.dockerTools.usrBinEnv - drv - tmpDir - ] ++ debugUtils ++ pkgs.lib.optionals (builtins.hasAttr execName (extraContents exes)) (builtins.getAttr execName (extraContents exes)); - # 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 = [ "${pkgs.dumb-init}/bin/dumb-init" "--" "${drv}/bin/${execName}" ]; - Env = [ - "SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt" - "LOCALE_ARCHIVE=${pkgs.glibcLocales}/lib/locale/locale-archive" - "LANG=en_GB.UTF-8" - # Use stable conventions for tracing http in opentelemetry - # https://opentelemetry.io/blog/2023/http-conventions-declared-stable/#migration-plan - "OTEL_SEMCONV_STABILITY_OPT_IN=http" - ]; - User = "65534"; - }; - } - ) - exes; + allImages + // { + all = pkgs.linkFarm "all-images" (attrsets.mapAttrsToList + (name: path: + { inherit name path; } + ) + allImages); + }; localModsEnableAll = { enableOptimization = true; @@ -380,7 +388,7 @@ let imagesList = pkgs.writeTextFile { name = "imagesList"; - text = "${lib.concatStringsSep "\n" (builtins.attrNames (images localModsEnableAll))}"; + text = "${lib.concatStringsSep "\n" (builtins.attrNames (staticExecs localModsEnableAll))}"; }; wireServerPackages = (builtins.attrNames (localPackages localModsEnableAll { } { })); @@ -422,7 +430,7 @@ let pkgs.kubelogin-oidc pkgs.nixpkgs-fmt pkgs.openssl - pkgs.ormolu + pkgs.haskellPackages.ormolu pkgs.vacuum-go pkgs.shellcheck pkgs.treefmt @@ -439,17 +447,18 @@ let # nicely in docker.nix at the root of https://github.com/nixos/nix. We get # this file using "${pkgs.nix.src}/docker.nix" so we don't have to also pin # the nix repository along with the nixpkgs repository. - ciImage = import "${pkgs.nix.src}/docker.nix" { + ciImage = import "${pkgs.nixVersions.latest.src}/docker.nix" { inherit pkgs; name = "quay.io/wire/wire-server-ci"; maxLayers = 2; + nix = pkgs.nixVersions.latest; # We don't need to push the "latest" tag, every step in CI should depend # deterministically on a specific image. tag = null; bundleNixpkgs = false; extraPkgs = commonTools ++ [ pkgs.cachix ]; nixConf = { - experimental-features = "nix-command"; + experimental-features = "nix-command flakes"; }; }; @@ -474,9 +483,8 @@ let haskellPackages = hPkgs localModsEnableAll; haskellPackagesUnoptimizedNoDocs = hPkgs localModsOnlyTests; - tom-bombadil = builtins.getFlake "github:wireapp/tom-bombadil"; localPkgs = map (e: (hPkgs localModsEnableAll).${e}) wireServerPackages; - bomDependencies = tom-bombadil.lib.${builtins.currentSystem}.bomDependenciesDrv pkgs localPkgs haskellPackages; + bomDependencies = bomDependenciesDrv pkgs localPkgs haskellPackages; in { inherit ciImage hoogleImage allImages haskellPackages haskellPackagesUnoptimizedNoDocs imagesList bomDependencies; @@ -498,12 +506,12 @@ in pkgs.bash pkgs.crate2nix pkgs.dash - (pkgs.haskell-language-server.override { supportedGhcVersions = [ "98" ]; }) + (pkgs.haskell-language-server.override { supportedGhcVersions = [ "910" ]; }) pkgs.ghcid pkgs.kind pkgs.netcat pkgs.niv - pkgs.haskellPackages.apply-refact + pkgs.haskell.packages.ghc912.apply-refact (pkgs.python3.withPackages (ps: with ps; [ black @@ -525,7 +533,7 @@ in pkgs.sbomqs pkgs.postgresql - pkgs.cabal-install + pkgs_24_11.cabal-install pkgs.nix-prefetch-git pkgs.haskellPackages.cabal-plan pkgs.lsof diff --git a/postgres-schema.sql b/postgres-schema.sql index 649dc4bf1f2..378195989b4 100644 --- a/postgres-schema.sql +++ b/postgres-schema.sql @@ -7,7 +7,7 @@ -- PostgreSQL database dump -- -\restrict 7mIWfK3KzS3dh5XHiAV6Z4AKzhK7lHVugezBtZY2vcFi7gDC9DiStSTii9adK62 +\restrict 79bbfb4630959c48307653a5cd3d83f2582b3c2210f75f10d79e3ebf0015620 -- Dumped from database version 17.6 -- Dumped by pg_dump version 17.6 @@ -51,7 +51,10 @@ SET default_table_access_method = heap; CREATE TABLE public.apps ( user_id uuid NOT NULL, team_id uuid NOT NULL, - metadata json + metadata json, + category text DEFAULT 'other'::text NOT NULL, + description text DEFAULT ''::text NOT NULL, + creator uuid NOT NULL ); @@ -396,6 +399,13 @@ CREATE INDEX collaborators_user_id_idx ON public.collaborators USING btree (user CREATE INDEX conversation_member_user_idx ON public.conversation_member USING btree ("user"); +-- +-- Name: conversation_team_group_type_lower_name_id_idx; Type: INDEX; Schema: public; Owner: wire-server +-- + +CREATE INDEX conversation_team_group_type_lower_name_id_idx ON public.conversation USING btree (team, group_conv_type, lower(name), id); + + -- -- Name: conversation_team_idx; Type: INDEX; Schema: public; Owner: wire-server -- @@ -477,4 +487,4 @@ REVOKE USAGE ON SCHEMA public FROM PUBLIC; -- PostgreSQL database dump complete -- -\unrestrict 7mIWfK3KzS3dh5XHiAV6Z4AKzhK7lHVugezBtZY2vcFi7gDC9DiStSTii9adK62 +\unrestrict 79bbfb4630959c48307653a5cd3d83f2582b3c2210f75f10d79e3ebf0015620 diff --git a/services/background-worker/background-worker.integration.yaml b/services/background-worker/background-worker.integration.yaml index ecb27c4c449..4ee7abbe100 100644 --- a/services/background-worker/background-worker.integration.yaml +++ b/services/background-worker/background-worker.integration.yaml @@ -65,6 +65,9 @@ backendNotificationPusher: remotesRefreshInterval: 10000 # 10ms migrateConversations: false +migrateConversationsOptions: + pageSize: 10000 + parallelism: 2 # Background jobs consumer configuration for integration backgroundJobs: diff --git a/services/background-worker/src/Wire/BackgroundWorker.hs b/services/background-worker/src/Wire/BackgroundWorker.hs index 657263eb654..c30c1d809aa 100644 --- a/services/background-worker/src/Wire/BackgroundWorker.hs +++ b/services/background-worker/src/Wire/BackgroundWorker.hs @@ -53,7 +53,7 @@ run opts = do then runAppT env $ withNamedLogger "migrate-conversations" $ - MigrateConversations.startWorker + MigrateConversations.startWorker opts.migrateConversationsOptions else pure $ pure () cleanupJobs <- runAppT env $ diff --git a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs index e9e6240ada4..1c9b3416bd5 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Jobs/Registry.hs @@ -78,9 +78,9 @@ dispatchJob job = do . interpretRace . runDelay . runError - . mapError @FederationError (T.pack . show) + . mapError @FederationError (T.pack . displayException) . mapError @UsageError (T.pack . show) - . mapError @ParseException (T.pack . show) + . mapError @ParseException (T.pack . displayException) . mapError @MigrationError (T.pack . show) . interpretTinyLog env job.requestId job.jobId . runInputConst env.hasqlPool diff --git a/services/background-worker/src/Wire/BackgroundWorker/Options.hs b/services/background-worker/src/Wire/BackgroundWorker/Options.hs index 899f0904adc..48cc531b586 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Options.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Options.hs @@ -28,6 +28,7 @@ import Network.AMQP.Extended import System.Logger.Extended import Util.Options import Wire.ConversationStore (PostgresMigrationOpts) +import Wire.ConversationStore.Migration (MigrationOptions) data Opts = Opts { logLevel :: !Level, @@ -49,7 +50,8 @@ data Opts = Opts postgresqlPassword :: !(Maybe FilePathSecrets), postgresqlPool :: !PoolConfig, postgresMigration :: !PostgresMigrationOpts, - migrateConversations :: Bool, + migrateConversations :: !Bool, + migrateConversationsOptions :: !MigrationOptions, backgroundJobs :: BackgroundJobsConfig, federationDomain :: Domain } diff --git a/services/background-worker/src/Wire/MigrateConversations.hs b/services/background-worker/src/Wire/MigrateConversations.hs index c2e4f318188..75587e1ae5b 100644 --- a/services/background-worker/src/Wire/MigrateConversations.hs +++ b/services/background-worker/src/Wire/MigrateConversations.hs @@ -25,20 +25,22 @@ import Wire.BackgroundWorker.Env import Wire.BackgroundWorker.Util import Wire.ConversationStore.Migration -startWorker :: AppT IO CleanupAction -startWorker = do +startWorker :: MigrationOptions -> AppT IO CleanupAction +startWorker migOpts = do cassClient <- asks (.cassandraGalley) pgPool <- asks (.hasqlPool) logger <- asks (.logger) Log.info logger $ Log.msg (Log.val "starting conversation migration") convMigCounter <- register $ counter $ Prometheus.Info "wire_local_convs_migrated_to_pg" "Number of local conversations migrated to Postgresql" - convMigFinished <- register $ counter $ Prometheus.Info "wire_local_convs_migration_finished" "Whether the conversation migration to Postgresql is finished" + convMigFinished <- register $ counter $ Prometheus.Info "wire_local_convs_migration_finished" "Whether the conversation migration to Postgresql is finished successfully" + convMigFailed <- register $ counter $ Prometheus.Info "wire_local_convs_migration_failed" "Whether the conversation migration to Postgresql has failed" userMigCounter <- register $ counter $ Prometheus.Info "wire_user_remote_convs_migrated_to_pg" "Number of users whose remote conversation membership data is migrated to Postgresql" - userMigFinished <- register $ counter $ Prometheus.Info "wire_user_remote_convs_migration_finished" "Whether the migration of remote conversation membership data to Postgresql is finished" + userMigFinished <- register $ counter $ Prometheus.Info "wire_user_remote_convs_migration_finished" "Whether the migration of remote conversation membership data to Postgresql is finished successfully" + userMigFailed <- register $ counter $ Prometheus.Info "wire_user_remote_convs_migration_failed" "Whether the migration of remote conversation membership data to Postgresql has failed" - convLoop <- async . lift $ migrateConvsLoop cassClient pgPool logger convMigCounter convMigFinished - userLoop <- async . lift $ migrateUsersLoop cassClient pgPool logger userMigCounter userMigFinished + convLoop <- async . lift $ migrateConvsLoop migOpts cassClient pgPool logger convMigCounter convMigFinished convMigFailed + userLoop <- async . lift $ migrateUsersLoop migOpts cassClient pgPool logger userMigCounter userMigFinished userMigFailed Log.info logger $ Log.msg (Log.val "started conversation migration") pure $ do diff --git a/services/brig/docs/swagger-v14.json b/services/brig/docs/swagger-v14.json new file mode 100644 index 00000000000..c8a6bd595e5 --- /dev/null +++ b/services/brig/docs/swagger-v14.json @@ -0,0 +1,38256 @@ +{ + "components": { + "schemas": { + "ASCII": { + "example": "aGVsbG8", + "type": "string" + }, + "AcceptTeamInvitation": { + "description": "Accept an invitation to join a team on Wire.", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "password": { + "description": "The user account password.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "code", + "password" + ], + "type": "object" + }, + "Access": { + "description": "How users can join conversations", + "enum": [ + "private", + "invite", + "link", + "code" + ], + "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": { + "deprecated": true, + "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": "#/components/schemas/TokenType" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user", + "access_token", + "token_type", + "expires_in" + ], + "type": "object" + }, + "AccessTokenType": { + "enum": [ + "DPoP" + ], + "type": "string" + }, + "AccountStatus": { + "enum": [ + "active", + "suspended", + "deleted", + "ephemeral", + "pending-invitation" + ], + "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", + "modify_add_permission" + ], + "type": "string" + }, + "Activate": { + "description": "Data for an activation request.", + "properties": { + "code": { + "$ref": "#/components/schemas/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": "#/components/schemas/Email" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "code", + "dryrun" + ], + "type": "object" + }, + "ActivationResponse": { + "description": "Response body of a successful activation request", + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + }, + "first": { + "description": "Whether this is the first successful activation (i.e. account activation).", + "type": "boolean" + }, + "sso_id": { + "$ref": "#/components/schemas/UserSSOId" + } + }, + "type": "object" + }, + "AddBot": { + "properties": { + "locale": { + "$ref": "#/components/schemas/Locale" + }, + "provider": { + "$ref": "#/components/schemas/UUID" + }, + "service": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "provider", + "service" + ], + "type": "object" + }, + "AddBotResponse": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "client": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "event": { + "$ref": "#/components/schemas/Event" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "id", + "client", + "name", + "accent_id", + "assets", + "event" + ], + "type": "object" + }, + "AddPermission": { + "enum": [ + "admins", + "everyone" + ], + "type": "string" + }, + "AddPermissionUpdate": { + "description": "The action of changing the permission to add members to a channel", + "properties": { + "add_permission": { + "$ref": "#/components/schemas/AddPermission" + } + }, + "required": [ + "add_permission" + ], + "type": "object" + }, + "AllTeamFeatures": { + "properties": { + "allowedGlobalOperations": { + "$ref": "#/components/schemas/AllowedGlobalOperationsConfig.LockableFeature" + }, + "appLock": { + "$ref": "#/components/schemas/AppLockConfig.LockableFeature" + }, + "apps": { + "$ref": "#/components/schemas/AppsConfig.LockableFeature" + }, + "assetAuditLog": { + "$ref": "#/components/schemas/AssetAuditLogConfig.LockableFeature" + }, + "cells": { + "$ref": "#/components/schemas/CellsConfig.LockableFeature" + }, + "cellsInternal": { + "$ref": "#/components/schemas/CellsInternalConfig.LockableFeature" + }, + "channels": { + "$ref": "#/components/schemas/ChannelsConfig.LockableFeature" + }, + "chatBubbles": { + "$ref": "#/components/schemas/ChatBubblesConfig.LockableFeature" + }, + "classifiedDomains": { + "$ref": "#/components/schemas/ClassifiedDomainsConfig.LockableFeature" + }, + "conferenceCalling": { + "$ref": "#/components/schemas/ConferenceCallingConfig.LockableFeature" + }, + "consumableNotifications": { + "$ref": "#/components/schemas/ConsumableNotificationsConfig.LockableFeature" + }, + "conversationGuestLinks": { + "$ref": "#/components/schemas/GuestLinksConfig.LockableFeature" + }, + "digitalSignatures": { + "$ref": "#/components/schemas/DigitalSignaturesConfig.LockableFeature" + }, + "domainRegistration": { + "$ref": "#/components/schemas/DomainRegistrationConfig.LockableFeature" + }, + "enforceFileDownloadLocation": { + "$ref": "#/components/schemas/EnforceFileDownloadLocation.LockableFeature" + }, + "exposeInvitationURLsToTeamAdmin": { + "$ref": "#/components/schemas/ExposeInvitationURLsToTeamAdminConfig.LockableFeature" + }, + "fileSharing": { + "$ref": "#/components/schemas/FileSharingConfig.LockableFeature" + }, + "legalhold": { + "$ref": "#/components/schemas/LegalholdConfig.LockableFeature" + }, + "limitedEventFanout": { + "$ref": "#/components/schemas/LimitedEventFanoutConfig.LockableFeature" + }, + "meetings": { + "$ref": "#/components/schemas/MeetingsConfig.LockableFeature" + }, + "meetingsPremium": { + "$ref": "#/components/schemas/MeetingsPremiumConfig.LockableFeature" + }, + "mls": { + "$ref": "#/components/schemas/MLSConfig.LockableFeature" + }, + "mlsE2EId": { + "$ref": "#/components/schemas/MlsE2EIdConfig.LockableFeature" + }, + "mlsMigration": { + "$ref": "#/components/schemas/MlsMigration.LockableFeature" + }, + "outlookCalIntegration": { + "$ref": "#/components/schemas/OutlookCalIntegrationConfig.LockableFeature" + }, + "searchVisibility": { + "$ref": "#/components/schemas/SearchVisibilityAvailableConfig.LockableFeature" + }, + "searchVisibilityInbound": { + "$ref": "#/components/schemas/SearchVisibilityInboundConfig.LockableFeature" + }, + "selfDeletingMessages": { + "$ref": "#/components/schemas/SelfDeletingMessagesConfig.LockableFeature" + }, + "simplifiedUserConnectionRequestQRCode": { + "$ref": "#/components/schemas/SimplifiedUserConnectionRequestQRCode.LockableFeature" + }, + "sndFactorPasswordChallenge": { + "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.LockableFeature" + }, + "sso": { + "$ref": "#/components/schemas/SSOConfig.LockableFeature" + }, + "stealthUsers": { + "$ref": "#/components/schemas/StealthUsersConfig.LockableFeature" + }, + "validateSAMLemails": { + "$ref": "#/components/schemas/ValidateSAMLEmailsConfig.LockableFeature" + } + }, + "required": [ + "legalhold", + "sso", + "searchVisibility", + "searchVisibilityInbound", + "validateSAMLemails", + "digitalSignatures", + "appLock", + "fileSharing", + "classifiedDomains", + "conferenceCalling", + "selfDeletingMessages", + "conversationGuestLinks", + "sndFactorPasswordChallenge", + "mls", + "exposeInvitationURLsToTeamAdmin", + "outlookCalIntegration", + "mlsE2EId", + "mlsMigration", + "enforceFileDownloadLocation", + "limitedEventFanout", + "domainRegistration", + "channels", + "cells", + "allowedGlobalOperations", + "consumableNotifications", + "chatBubbles", + "apps", + "simplifiedUserConnectionRequestQRCode", + "assetAuditLog", + "stealthUsers", + "cellsInternal", + "meetings", + "meetingsPremium" + ], + "type": "object" + }, + "AllowedGlobalOperationsConfig": { + "properties": { + "mlsConversationReset": { + "type": "boolean" + } + }, + "required": [ + "mlsConversationReset" + ], + "type": "object" + }, + "AllowedGlobalOperationsConfig.LockableFeature": { + "properties": { + "config": { + "$ref": "#/components/schemas/AllowedGlobalOperationsConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "AppLockConfig": { + "properties": { + "enforceAppLock": { + "type": "boolean" + }, + "inactivityTimeoutSecs": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "enforceAppLock", + "inactivityTimeoutSecs" + ], + "type": "object" + }, + "AppLockConfig.Feature": { + "properties": { + "config": { + "$ref": "#/components/schemas/AppLockConfig" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "AppLockConfig.LockableFeature": { + "properties": { + "config": { + "$ref": "#/components/schemas/AppLockConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "ApproveLegalHoldForUserRequest": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "AppsConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "Asset": { + "properties": { + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "expires": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "key": { + "$ref": "#/components/schemas/AssetKey" + }, + "token": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "key", + "domain" + ], + "type": "object" + }, + "AssetAuditLogConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "AssetKey": { + "description": "S3 asset key for an icon image with retention information.", + "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": "#/components/schemas/Id_AuthnRequest" + }, + "issueInstant": { + "$ref": "#/components/schemas/Time" + }, + "issuer": { + "type": "string" + }, + "nameIDPolicy": { + "$ref": "#/components/schemas/NameIdPolicy" + } + }, + "required": [ + "iD", + "issueInstant", + "issuer" + ], + "type": "object" + }, + "BackendConfig": { + "properties": { + "config_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "webapp_url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "config_url" + ], + "type": "object" + }, + "Base64ByteString": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "Base64URLByteString": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "BaseProtocol": { + "enum": [ + "proteus", + "mls" + ], + "type": "string" + }, + "BindingNewTeamUser": { + "properties": { + "currency": { + "$ref": "#/components/schemas/Currency.Alpha" + }, + "icon": { + "$ref": "#/components/schemas/Icon" + }, + "icon_key": { + "description": "The decryption key for the team icon S3 asset", + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "name": { + "description": "team name", + "maxLength": 256, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name", + "icon" + ], + "type": "object" + }, + "BotConvView": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "members": { + "items": { + "$ref": "#/components/schemas/OtherMember" + }, + "type": "array" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "members" + ], + "type": "object" + }, + "BotUserView": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "team": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "id", + "name", + "accent_id" + ], + "type": "object" + }, + "Category": { + "enum": [ + "security", + "collaboration", + "productivity", + "automation", + "files", + "ai", + "developer", + "support", + "finance", + "hr", + "integration", + "compliance", + "other" + ], + "type": "string" + }, + "CellsBackend": { + "properties": { + "url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "url" + ], + "type": "object" + }, + "CellsCollabora": { + "properties": { + "edition": { + "$ref": "#/components/schemas/CollaboraEdition" + } + }, + "required": [ + "edition" + ], + "type": "object" + }, + "CellsCollaboraStatus": { + "properties": { + "enabled": { + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "CellsConfig": { + "example": { + "channels": { + "default": "enabled", + "enabled": true + }, + "collabora": { + "enabled": false + }, + "groups": { + "default": "enabled", + "enabled": true + }, + "metadata": { + "namespaces": { + "usermetaTags": { + "allowFreeValues": true, + "defaultValues": [] + } + } + }, + "one2one": { + "default": "enabled", + "enabled": true + }, + "publicLinks": { + "enableFiles": true, + "enableFolders": true, + "enforceExpirationDefault": 0, + "enforceExpirationMax": 0, + "enforcePassword": false + }, + "storage": { + "perFileQuotaBytes": "100000000", + "recycle": { + "allowSkip": false, + "autoPurgeDays": 30, + "disable": false + } + }, + "users": { + "externals": true, + "guests": false + } + }, + "properties": { + "channels": { + "$ref": "#/components/schemas/CellsProperty" + }, + "collabora": { + "$ref": "#/components/schemas/CellsCollaboraStatus" + }, + "groups": { + "$ref": "#/components/schemas/CellsProperty" + }, + "metadata": { + "$ref": "#/components/schemas/CellsMetadata" + }, + "one2one": { + "$ref": "#/components/schemas/CellsProperty" + }, + "publicLinks": { + "$ref": "#/components/schemas/CellsPublicLinks" + }, + "storage": { + "$ref": "#/components/schemas/CellsConfigStorage" + }, + "users": { + "$ref": "#/components/schemas/CellsUsers" + } + }, + "required": [ + "channels", + "groups", + "one2one", + "users", + "collabora", + "publicLinks", + "storage", + "metadata" + ], + "type": "object" + }, + "CellsConfig.Feature": { + "properties": { + "config": { + "$ref": "#/components/schemas/CellsConfig" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "CellsConfig.LockableFeature": { + "properties": { + "config": { + "$ref": "#/components/schemas/CellsConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "CellsConfigStorage": { + "properties": { + "perFileQuotaBytes": { + "type": "string" + }, + "recycle": { + "$ref": "#/components/schemas/CellsRecycle" + } + }, + "required": [ + "perFileQuotaBytes", + "recycle" + ], + "type": "object" + }, + "CellsInternalConfig": { + "properties": { + "backend": { + "$ref": "#/components/schemas/CellsBackend" + }, + "collabora": { + "$ref": "#/components/schemas/CellsCollabora" + }, + "storage": { + "$ref": "#/components/schemas/CellsStorage" + } + }, + "required": [ + "backend", + "collabora", + "storage" + ], + "type": "object" + }, + "CellsInternalConfig.LockableFeature": { + "properties": { + "config": { + "$ref": "#/components/schemas/CellsInternalConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "CellsMetadata": { + "properties": { + "namespaces": { + "$ref": "#/components/schemas/CellsNamespaces" + } + }, + "required": [ + "namespaces" + ], + "type": "object" + }, + "CellsNamespaces": { + "properties": { + "usermetaTags": { + "$ref": "#/components/schemas/CellsUserMetaTags" + } + }, + "required": [ + "usermetaTags" + ], + "type": "object" + }, + "CellsProperty": { + "properties": { + "default": { + "$ref": "#/components/schemas/CellsPropertyStatus" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "enabled", + "default" + ], + "type": "object" + }, + "CellsPropertyStatus": { + "enum": [ + "enabled", + "disabled", + "enforced" + ], + "type": "string" + }, + "CellsPublicLinks": { + "properties": { + "enableFiles": { + "type": "boolean" + }, + "enableFolders": { + "type": "boolean" + }, + "enforceExpirationDefault": { + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "enforceExpirationMax": { + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "enforcePassword": { + "type": "boolean" + } + }, + "required": [ + "enableFiles", + "enableFolders", + "enforcePassword", + "enforceExpirationMax", + "enforceExpirationDefault" + ], + "type": "object" + }, + "CellsRecycle": { + "properties": { + "allowSkip": { + "type": "boolean" + }, + "autoPurgeDays": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "disable": { + "type": "boolean" + } + }, + "required": [ + "autoPurgeDays", + "disable", + "allowSkip" + ], + "type": "object" + }, + "CellsState": { + "enum": [ + "disabled", + "pending", + "ready" + ], + "type": "string" + }, + "CellsStorage": { + "properties": { + "perUserQuotaBytes": { + "type": "string" + } + }, + "required": [ + "perUserQuotaBytes" + ], + "type": "object" + }, + "CellsUserMetaTags": { + "properties": { + "allowFreeValues": { + "type": "boolean" + }, + "defaultValues": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "defaultValues", + "allowFreeValues" + ], + "type": "object" + }, + "CellsUsers": { + "properties": { + "externals": { + "type": "boolean" + }, + "guests": { + "type": "boolean" + } + }, + "required": [ + "externals", + "guests" + ], + "type": "object" + }, + "ChallengeToken": { + "properties": { + "challenge_token": { + "$ref": "#/components/schemas/Token" + } + }, + "required": [ + "challenge_token" + ], + "type": "object" + }, + "ChannelPermissions": { + "enum": [ + "team-members", + "everyone", + "admins" + ], + "type": "string" + }, + "ChannelsConfig": { + "properties": { + "allowed_to_create_channels": { + "$ref": "#/components/schemas/ChannelPermissions" + }, + "allowed_to_open_channels": { + "$ref": "#/components/schemas/ChannelPermissions" + } + }, + "required": [ + "allowed_to_create_channels", + "allowed_to_open_channels" + ], + "type": "object" + }, + "ChannelsConfig.Feature": { + "properties": { + "config": { + "$ref": "#/components/schemas/ChannelsConfig" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "ChannelsConfig.LockableFeature": { + "properties": { + "config": { + "$ref": "#/components/schemas/ChannelsConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "ChatBubblesConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "CheckHandles": { + "properties": { + "handles": { + "items": { + "type": "string" + }, + "maxItems": 50, + "minItems": 1, + "type": "array" + }, + "return": { + "maximum": 10, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "handles", + "return" + ], + "type": "object" + }, + "CheckUserGroupName": { + "properties": { + "name": { + "maxLength": 4000, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "CipherSuiteTag": { + "description": "The cipher suite of the corresponding MLS group", + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "ClassifiedDomainsConfig": { + "properties": { + "domains": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "domains" + ], + "type": "object" + }, + "ClassifiedDomainsConfig.LockableFeature": { + "properties": { + "config": { + "$ref": "#/components/schemas/ClassifiedDomainsConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "Client": { + "properties": { + "capabilities": { + "$ref": "#/components/schemas/ClientCapabilityList" + }, + "class": { + "$ref": "#/components/schemas/ClientClass" + }, + "cookie": { + "type": "string" + }, + "id": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "label": { + "type": "string" + }, + "last_active": { + "$ref": "#/components/schemas/UTCTime" + }, + "mls_public_keys": { + "$ref": "#/components/schemas/MLSPublicKeys" + }, + "model": { + "type": "string" + }, + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "type": { + "$ref": "#/components/schemas/ClientType" + } + }, + "required": [ + "id", + "type", + "time" + ], + "type": "object" + }, + "ClientCapability": { + "enum": [ + "legalhold-implicit-consent", + "consumable-notifications" + ], + "type": "string" + }, + "ClientCapabilityList": { + "items": { + "$ref": "#/components/schemas/ClientCapability" + }, + "type": "array" + }, + "ClientClass": { + "enum": [ + "phone", + "tablet", + "desktop", + "legalhold" + ], + "type": "string" + }, + "ClientIdentity": { + "properties": { + "client_id": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "user_id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "domain", + "user_id", + "client_id" + ], + "type": "object" + }, + "ClientMismatch": { + "properties": { + "deleted": { + "$ref": "#/components/schemas/UserClients" + }, + "missing": { + "$ref": "#/components/schemas/UserClients" + }, + "redundant": { + "$ref": "#/components/schemas/UserClients" + }, + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" + } + }, + "required": [ + "time", + "missing", + "redundant", + "deleted" + ], + "type": "object" + }, + "ClientPrekey": { + "properties": { + "client": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "prekey": { + "$ref": "#/components/schemas/Prekey" + } + }, + "required": [ + "client", + "prekey" + ], + "type": "object" + }, + "ClientType": { + "enum": [ + "temporary", + "permanent", + "legalhold" + ], + "type": "string" + }, + "CodeChallengeMethod": { + "description": "The method used to encode the code challenge. Only `S256` is supported.", + "enum": [ + "S256" + ], + "type": "string" + }, + "CollaboraEdition": { + "enum": [ + "NO", + "CODE", + "COOL" + ], + "type": "string" + }, + "CollaboratorPermission": { + "enum": [ + "create_team_conversation", + "implicit_connection" + ], + "type": "string" + }, + "CommitBundle": { + "description": "This object can only be parsed in TLS format. Please refer to the MLS specification for details." + }, + "CompletePasswordReset": { + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "key", + "code", + "password" + ], + "type": "object" + }, + "ConferenceCallingConfig": { + "properties": { + "useSFTForOneToOneCalls": { + "type": "boolean" + } + }, + "type": "object" + }, + "ConferenceCallingConfig.Feature": { + "properties": { + "config": { + "$ref": "#/components/schemas/ConferenceCallingConfig" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "ConferenceCallingConfig.LockableFeature": { + "properties": { + "config": { + "$ref": "#/components/schemas/ConferenceCallingConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "Connect": { + "properties": { + "email": { + "type": "string" + }, + "message": { + "type": "string" + }, + "name": { + "type": "string" + }, + "qualified_recipient": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "recipient": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "qualified_recipient" + ], + "type": "object" + }, + "ConnectionUpdate": { + "properties": { + "status": { + "$ref": "#/components/schemas/Relation" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Connections_Page": { + "properties": { + "connections": { + "items": { + "$ref": "#/components/schemas/UserConnection" + }, + "type": "array" + }, + "has_more": { + "type": "boolean" + }, + "paging_state": { + "$ref": "#/components/schemas/Connections_PagingState" + } + }, + "required": [ + "connections", + "has_more", + "paging_state" + ], + "type": "object" + }, + "Connections_PagingState": { + "type": "string" + }, + "ConsumableNotificationsConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "Contact": { + "description": "Contact discovered through search", + "properties": { + "accent_id": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "handle": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "team": { + "$ref": "#/components/schemas/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": "#/components/schemas/OtherMember" + }, + "type": "array" + }, + "self": { + "$ref": "#/components/schemas/Member" + } + }, + "required": [ + "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": "#/components/schemas/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": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/ConvMembers" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/Protocol" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch" + ], + "type": "object" + }, + "ConversationAccessData": { + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + } + }, + "required": [ + "access", + "access_role" + ], + "type": "object" + }, + "ConversationAccessDataV2": { + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/components/schemas/AccessRoleLegacy" + }, + "access_role_v2": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + } + }, + "required": [ + "access" + ], + "type": "object" + }, + "ConversationCode": { + "description": "Contains conversation properties to update", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "key", + "code" + ], + "type": "object" + }, + "ConversationCodeInfo": { + "description": "Contains conversation properties to update", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "has_password": { + "description": "Whether the conversation has a password", + "type": "boolean" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + }, + "uri": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "key", + "code", + "uri", + "has_password" + ], + "type": "object" + }, + "ConversationCoverView": { + "description": "Limited view of Conversation.", + "properties": { + "has_password": { + "type": "boolean" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "has_password" + ], + "type": "object" + }, + "ConversationIds_Page": { + "properties": { + "has_more": { + "type": "boolean" + }, + "paging_state": { + "$ref": "#/components/schemas/ConversationIds_PagingState" + }, + "qualified_conversations": { + "items": { + "$ref": "#/components/schemas/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": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "type": "object" + }, + "ConversationPage": { + "description": "This is the last page if it contains fewer rows than requested. There may be 0 rows on a page.", + "properties": { + "page": { + "items": { + "$ref": "#/components/schemas/ConversationSearchResult" + }, + "type": "array" + } + }, + "required": [ + "page" + ], + "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" + }, + "ConversationReset": { + "properties": { + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "new_group_id": { + "$ref": "#/components/schemas/GroupId" + } + }, + "required": [ + "group_id" + ], + "type": "object" + }, + "ConversationRole": { + "properties": { + "actions": { + "description": "The set of actions allowed for this role", + "items": { + "$ref": "#/components/schemas/Action" + }, + "type": "array" + }, + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + } + } + }, + "ConversationRolesList": { + "properties": { + "conversation_roles": { + "items": { + "$ref": "#/components/schemas/ConversationRole" + }, + "type": "array" + } + }, + "required": [ + "conversation_roles" + ], + "type": "object" + }, + "ConversationSearchResult": { + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "admin_count": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "member_count": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "access", + "member_count", + "admin_count" + ], + "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": "#/components/schemas/Qualified_ConvId" + }, + "type": "array" + }, + "found": { + "items": { + "$ref": "#/components/schemas/OwnConversation" + }, + "type": "array" + }, + "not_found": { + "description": "These conversations either don't exist or are deleted.", + "items": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "type": "array" + } + }, + "required": [ + "found", + "not_found", + "failed" + ], + "type": "object" + }, + "Cookie": { + "properties": { + "created": { + "$ref": "#/components/schemas/UTCTime" + }, + "expires": { + "$ref": "#/components/schemas/UTCTime" + }, + "id": { + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "label": { + "type": "string" + }, + "successor": { + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "type": { + "$ref": "#/components/schemas/CookieType" + } + }, + "required": [ + "id", + "type", + "created", + "expires" + ], + "type": "object" + }, + "CookieList": { + "description": "List of cookie information", + "properties": { + "cookies": { + "items": { + "$ref": "#/components/schemas/Cookie" + }, + "type": "array" + } + }, + "required": [ + "cookies" + ], + "type": "object" + }, + "CookieType": { + "enum": [ + "session", + "persistent" + ], + "type": "string" + }, + "CreateConversationCodeRequest": { + "description": "Request body for creating a conversation code", + "properties": { + "password": { + "description": "Password for accessing the conversation via guest link. Set to null or omit for no password.", + "maxLength": 1024, + "minLength": 8, + "type": "string" + } + }, + "type": "object" + }, + "CreateGroupConversation": { + "description": "A created group-conversation object extended with a list of failed-to-add users", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "failed_to_add": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/ConvMembers" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/Protocol" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch", + "failed_to_add" + ], + "type": "object" + }, + "CreateOAuthAuthorizationCodeRequest": { + "properties": { + "client_id": { + "$ref": "#/components/schemas/UUID" + }, + "code_challenge": { + "$ref": "#/components/schemas/OAuthCodeChallenge" + }, + "code_challenge_method": { + "$ref": "#/components/schemas/CodeChallengeMethod" + }, + "redirect_uri": { + "$ref": "#/components/schemas/RedirectUrl" + }, + "response_type": { + "$ref": "#/components/schemas/OAuthResponseType" + }, + "scope": { + "description": "The scopes which are requested to get authorization for, separated by a space", + "type": "string" + }, + "state": { + "description": "An opaque value used by the client to maintain state between the request and callback. The authorization server includes this value when redirecting the user-agent back to the client. The parameter SHOULD be used for preventing cross-site request forgery", + "type": "string" + } + }, + "required": [ + "client_id", + "scope", + "response_type", + "redirect_uri", + "state", + "code_challenge_method", + "code_challenge" + ], + "type": "object" + }, + "CreateScimToken": { + "properties": { + "description": { + "type": "string" + }, + "idp": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "verification_code": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "description" + ], + "type": "object" + }, + "CreateScimTokenResponse": { + "properties": { + "info": { + "$ref": "#/components/schemas/ScimTokenInfo" + }, + "token": { + "type": "string" + } + }, + "required": [ + "token", + "info" + ], + "type": "object" + }, + "CreateUserTeam": { + "properties": { + "team_id": { + "$ref": "#/components/schemas/UUID" + }, + "team_name": { + "type": "string" + } + }, + "required": [ + "team_id", + "team_name" + ], + "type": "object" + }, + "CreatedApp": { + "properties": { + "cookie": { + "$ref": "#/components/schemas/SomeUserToken" + }, + "user": { + "$ref": "#/components/schemas/User" + } + }, + "required": [ + "user", + "cookie" + ], + "type": "object" + }, + "Currency.Alpha": { + "description": "ISO 4217 alphabetic codes. This is only stored by the backend, not processed. It can be removed once billing supports currency changes after team creation.", + "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" + ], + "example": "EUR", + "type": "string" + }, + "CustomBackend": { + "description": "Description of a custom backend", + "properties": { + "config_json_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "webapp_welcome_url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "config_json_url", + "webapp_welcome_url" + ], + "type": "object" + }, + "DPoPAccessToken": { + "type": "string" + }, + "DPoPAccessTokenResponse": { + "properties": { + "expires_in": { + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "token": { + "$ref": "#/components/schemas/DPoPAccessToken" + }, + "type": { + "$ref": "#/components/schemas/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" + }, + "DeleteKeyPackages": { + "properties": { + "key_packages": { + "items": { + "$ref": "#/components/schemas/KeyPackageRef" + }, + "maxItems": 1000, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "key_packages" + ], + "type": "object" + }, + "DeleteProvider": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "DeleteService": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "password" + ], + "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" + }, + "DigitalSignaturesConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "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" + }, + "DomainOwnershipToken": { + "properties": { + "domain_ownership_token": { + "$ref": "#/components/schemas/Token" + } + }, + "required": [ + "domain_ownership_token" + ], + "type": "object" + }, + "DomainRedirect Tag": { + "enum": [ + "none", + "locked", + "sso", + "backend", + "no-registration", + "pre-authorized" + ], + "type": "string" + }, + "DomainRedirectConfig": { + "properties": { + "backend": { + "$ref": "#/components/schemas/backend_config" + }, + "domain_redirect": { + "$ref": "#/components/schemas/DomainRedirectConfigTag" + } + }, + "required": [ + "domain_redirect", + "backend" + ], + "type": "object" + }, + "DomainRedirectConfigTag": { + "enum": [ + "remove", + "backend", + "no-registration" + ], + "type": "string" + }, + "DomainRedirectResponseV10": { + "properties": { + "backend": { + "$ref": "#/components/schemas/BackendConfig" + }, + "domain_redirect": { + "$ref": "#/components/schemas/DomainRedirect Tag" + }, + "due_to_existing_account": { + "type": "boolean" + }, + "sso_code": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "domain_redirect", + "sso_code", + "backend" + ], + "type": "object" + }, + "DomainRegistrationConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "DomainRegistrationResponse": { + "properties": { + "authorized_team": { + "$ref": "#/components/schemas/UUID" + }, + "backend": { + "$ref": "#/components/schemas/BackendConfig" + }, + "dns_verification_token": { + "$ref": "#/components/schemas/ASCII" + }, + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "domain_redirect": { + "$ref": "#/components/schemas/DomainRedirect Tag" + }, + "sso_code": { + "$ref": "#/components/schemas/UUID" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "team_invite": { + "$ref": "#/components/schemas/TeamInvite Tag" + } + }, + "required": [ + "domain", + "domain_redirect", + "sso_code", + "backend", + "team_invite", + "team" + ], + "type": "object" + }, + "DomainVerificationChallenge": { + "properties": { + "dns_verification_token": { + "$ref": "#/components/schemas/ASCII" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "token": { + "$ref": "#/components/schemas/Token" + } + }, + "required": [ + "id", + "token", + "dns_verification_token" + ], + "type": "object" + }, + "EdMemberLeftReason": { + "enum": [ + "left", + "user-deleted", + "removed" + ], + "type": "string" + }, + "Either_OAuthAccessTokenRequest_OAuthRefreshAccessTokenRequest": { + "oneOf": [ + { + "properties": { + "Left": { + "$ref": "#/components/schemas/OAuthAccessTokenRequest" + } + }, + "required": [ + "Left" + ], + "title": "Left", + "type": "object" + }, + { + "properties": { + "Right": { + "$ref": "#/components/schemas/OAuthRefreshAccessTokenRequest" + } + }, + "required": [ + "Right" + ], + "title": "Right", + "type": "object" + } + ] + }, + "Email": { + "type": "string" + }, + "EmailUpdate": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "EnforceFileDownloadLocation": { + "properties": { + "enforcedDownloadLocation": { + "type": "string" + } + }, + "type": "object" + }, + "EnforceFileDownloadLocation.Feature": { + "properties": { + "config": { + "$ref": "#/components/schemas/EnforceFileDownloadLocation" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "EnforceFileDownloadLocation.LockableFeature": { + "properties": { + "config": { + "$ref": "#/components/schemas/EnforceFileDownloadLocation" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "EpochTimestamp": { + "example": "2021-05-12T10:52:02Z", + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string" + }, + "Event": { + "properties": { + "conversation": { + "$ref": "#/components/schemas/UUID" + }, + "data": { + "description": "The action of changing the permission to add members to a channel", + "example": "ZXhhbXBsZQo=", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/components/schemas/AccessRoleLegacy" + }, + "access_role_v2": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission" + }, + "add_type": { + "$ref": "#/components/schemas/JoinType" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "creator": { + "$ref": "#/components/schemas/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": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/EpochTimestamp" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "has_password": { + "description": "Whether the conversation has a password", + "type": "boolean" + }, + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/OwnConvMembers" + }, + "message": { + "type": "string" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "new_group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "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" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/Protocol" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "qualified_recipient": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "qualified_target": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "qualified_user_ids": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + }, + "reason": { + "$ref": "#/components/schemas/EdMemberLeftReason" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "recipient": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "sender": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/TypingStatus" + }, + "target": { + "$ref": "#/components/schemas/UUID" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "text": { + "description": "The ciphertext for the recipient (Base64 in JSON)", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/ConvType" + }, + "uri": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "user_ids": { + "deprecated": true, + "description": "Deprecated, use qualified_user_ids", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "users": { + "items": { + "$ref": "#/components/schemas/SimpleMember" + }, + "type": "array" + } + }, + "required": [ + "users", + "add_type", + "reason", + "qualified_user_ids", + "user_ids", + "qualified_target", + "name", + "access", + "key", + "code", + "uri", + "has_password", + "qualified_id", + "type", + "members", + "group_id", + "epoch", + "epoch_timestamp", + "cipher_suite", + "qualified_recipient", + "receipt_mode", + "sender", + "recipient", + "text", + "status", + "add_permission" + ], + "type": "object" + }, + "from": { + "$ref": "#/components/schemas/UUID" + }, + "qualified_conversation": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "qualified_from": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "subconv": { + "type": "string" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "type": { + "$ref": "#/components/schemas/EventType" + }, + "via": { + "$ref": "#/components/schemas/EventVia" + } + }, + "required": [ + "type", + "data", + "qualified_conversation", + "qualified_from", + "via", + "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.mls-reset", + "conversation.connect-request", + "conversation.typing", + "conversation.otr-message-add", + "conversation.mls-message-add", + "conversation.mls-welcome", + "conversation.protocol-update", + "conversation.add-permission-update" + ], + "type": "string" + }, + "EventVia": { + "enum": [ + "scim", + "user" + ], + "type": "string" + }, + "ExposeInvitationURLsToTeamAdminConfig.Feature": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "ExposeInvitationURLsToTeamAdminConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "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.Feature": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "FileSharingConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "Fingerprint": { + "example": "ioy3GeIjgQRsobf2EKGO3O8mq/FofFxHRqy0T4ERIZ8=", + "type": "string" + }, + "FormRedirect": { + "properties": { + "uri": { + "type": "string" + }, + "xml": { + "$ref": "#/components/schemas/AuthnRequest" + } + }, + "type": "object" + }, + "GetApp": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "category": { + "$ref": "#/components/schemas/Category" + }, + "description": { + "maxLength": 300, + "minLength": 0, + "type": "string" + }, + "metadata": { + "type": "object" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict" + } + }, + "required": [ + "name", + "metadata", + "category", + "description" + ], + "type": "object" + }, + "GetDomainRegistrationRequest": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "GetPaginated_Connections": { + "description": "A request to list some or all of a user's Connections, including remote ones", + "properties": { + "paging_state": { + "$ref": "#/components/schemas/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": "#/components/schemas/ConversationIds_PagingState" + }, + "size": { + "description": "optional, must be <= 1000, defaults to 1000.", + "format": "int32", + "maximum": 1000, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, + "GroupConvType": { + "enum": [ + "group_conversation", + "channel" + ], + "type": "string" + }, + "GroupId": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "GroupInfoData": { + "description": "This object can only be parsed in TLS format. Please refer to the MLS specification for details." + }, + "GuestLinksConfig.Feature": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "GuestLinksConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "Handle": { + "type": "string" + }, + "HandleUpdate": { + "properties": { + "handle": { + "type": "string" + } + }, + "required": [ + "handle" + ], + "type": "object" + }, + "HttpsUrl": { + "example": "https://example.com", + "type": "string" + }, + "Icon": { + "description": "S3 asset key for an icon image with retention information. Allows special value 'default'.", + "example": "3-1-47de4580-ae51-4650-acbb-d10c028cb0ac", + "type": "string" + }, + "Id": { + "properties": { + "id": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "IdPConfig_WireIdP": { + "properties": { + "extraInfo": { + "$ref": "#/components/schemas/WireIdP" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "metadata": { + "$ref": "#/components/schemas/IdPMetadata" + } + }, + "required": [ + "id", + "metadata", + "extraInfo" + ], + "type": "object" + }, + "IdPList": { + "properties": { + "providers": { + "items": { + "$ref": "#/components/schemas/IdPConfig_WireIdP" + }, + "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" + }, + "Id_AuthnRequest": { + "properties": { + "iD": { + "type": "string" + } + }, + "required": [ + "iD" + ], + "type": "object" + }, + "Invitation": { + "description": "An invitation to join a team on Wire. If invitee is invited from an existing personal account, inviter email is included.", + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "created_by": { + "$ref": "#/components/schemas/UUID" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "description": "Name of the invitee (1 - 128 characters)", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "role": { + "$ref": "#/components/schemas/Role" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "url": { + "$ref": "#/components/schemas/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": "#/components/schemas/Invitation" + }, + "type": "array" + } + }, + "required": [ + "invitations", + "has_more" + ], + "type": "object" + }, + "InvitationRequest": { + "description": "A request to join a team on Wire.", + "properties": { + "allow_existing": { + "description": "Whether invitations to existing users are allowed.", + "type": "boolean" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "locale": { + "$ref": "#/components/schemas/Locale" + }, + "name": { + "description": "Name of the invitee (1 - 128 characters).", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "role": { + "$ref": "#/components/schemas/Role" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "InvitationUserView": { + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "created_by": { + "$ref": "#/components/schemas/UUID" + }, + "created_by_email": { + "$ref": "#/components/schemas/Email" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "description": "Name of the invitee (1 - 128 characters)", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "role": { + "$ref": "#/components/schemas/Role" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "url": { + "$ref": "#/components/schemas/URIRef_Absolute" + } + }, + "required": [ + "team", + "id", + "created_at", + "email" + ], + "type": "object" + }, + "InviteQualified": { + "properties": { + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "qualified_users": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "qualified_users" + ], + "type": "object" + }, + "JoinConversationByCode": { + "description": "Request body for joining a conversation by code", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + }, + "password": { + "maxLength": 1024, + "minLength": 8, + "type": "string" + } + }, + "required": [ + "key", + "code" + ], + "type": "object" + }, + "JoinType": { + "enum": [ + "external_add", + "internal_add" + ], + "type": "string" + }, + "KeyPackage": { + "example": "a2V5IHBhY2thZ2UgZGF0YQo=", + "type": "string" + }, + "KeyPackageBundle": { + "properties": { + "key_packages": { + "items": { + "$ref": "#/components/schemas/KeyPackageBundleEntry" + }, + "type": "array" + } + }, + "required": [ + "key_packages" + ], + "type": "object" + }, + "KeyPackageBundleEntry": { + "properties": { + "client": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "key_package": { + "$ref": "#/components/schemas/KeyPackage" + }, + "key_package_ref": { + "$ref": "#/components/schemas/KeyPackageRef" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "domain", + "user", + "client", + "key_package_ref", + "key_package" + ], + "type": "object" + }, + "KeyPackageRef": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "KeyPackageUpload": { + "properties": { + "key_packages": { + "items": { + "$ref": "#/components/schemas/KeyPackage" + }, + "type": "array" + } + }, + "required": [ + "key_packages" + ], + "type": "object" + }, + "LHServiceStatus": { + "enum": [ + "configured", + "not_configured", + "disabled" + ], + "type": "string" + }, + "LegalholdConfig.Feature": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "LegalholdConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LimitedEventFanoutConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LimitedQualifiedUserIdList_500": { + "properties": { + "qualified_users": { + "items": { + "$ref": "#/components/schemas/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": "#/components/schemas/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" + }, + "ListUsersById": { + "properties": { + "failed": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "minItems": 1, + "type": "array" + }, + "found": { + "items": { + "$ref": "#/components/schemas/UserProfile" + }, + "type": "array" + } + }, + "required": [ + "found" + ], + "type": "object" + }, + "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": "#/components/schemas/Qualified_Handle" + }, + "type": "array" + }, + "qualified_ids": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + } + }, + "type": "object" + }, + "Locale": { + "type": "string" + }, + "LocaleUpdate": { + "properties": { + "locale": { + "$ref": "#/components/schemas/Locale" + } + }, + "required": [ + "locale" + ], + "type": "object" + }, + "LockStatus": { + "enum": [ + "locked", + "unlocked" + ], + "type": "string" + }, + "Login": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + }, + "label": { + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "verification_code": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "MLSConfig": { + "description": "allowlist of users that may change protocols", + "properties": { + "allowedCipherSuites": { + "items": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "type": "array" + }, + "defaultCipherSuite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "defaultProtocol": { + "$ref": "#/components/schemas/Protocol" + }, + "groupInfoDiagnostics": { + "type": "boolean" + }, + "protocolToggleUsers": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "supportedProtocols": { + "items": { + "$ref": "#/components/schemas/Protocol" + }, + "type": "array" + } + }, + "required": [ + "protocolToggleUsers", + "defaultProtocol", + "allowedCipherSuites", + "defaultCipherSuite", + "supportedProtocols" + ], + "type": "object" + }, + "MLSConfig.Feature": { + "properties": { + "config": { + "$ref": "#/components/schemas/MLSConfig" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "MLSConfig.LockableFeature": { + "properties": { + "config": { + "$ref": "#/components/schemas/MLSConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "MLSKeys": { + "properties": { + "ecdsa_secp256r1_sha256": { + "$ref": "#/components/schemas/SomeKey" + }, + "ecdsa_secp384r1_sha384": { + "$ref": "#/components/schemas/SomeKey" + }, + "ecdsa_secp521r1_sha512": { + "$ref": "#/components/schemas/SomeKey" + }, + "ed25519": { + "$ref": "#/components/schemas/SomeKey" + } + }, + "required": [ + "ed25519", + "ecdsa_secp256r1_sha256", + "ecdsa_secp384r1_sha384", + "ecdsa_secp521r1_sha512" + ], + "type": "object" + }, + "MLSKeysByPurpose": { + "properties": { + "removal": { + "$ref": "#/components/schemas/MLSKeys" + } + }, + "required": [ + "removal" + ], + "type": "object" + }, + "MLSMessage": { + "description": "This object can only be parsed in TLS format. Please refer to the MLS specification for details." + }, + "MLSMessageSendingStatus": { + "properties": { + "events": { + "description": "A list of events caused by sending the message.", + "items": { + "$ref": "#/components/schemas/Event" + }, + "type": "array" + }, + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" + } + }, + "required": [ + "events", + "time" + ], + "type": "object" + }, + "MLSOne2OneConversation_SomeKey": { + "properties": { + "conversation": { + "$ref": "#/components/schemas/OwnConversationV9" + }, + "public_keys": { + "$ref": "#/components/schemas/MLSKeysByPurpose" + } + }, + "required": [ + "conversation", + "public_keys" + ], + "type": "object" + }, + "MLSPublicKeys": { + "additionalProperties": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "description": "Mapping from signature scheme (tags) to public key data", + "example": { + "ecdsa_secp256r1_sha256": "ZXhhbXBsZQo=", + "ecdsa_secp384r1_sha384": "ZXhhbXBsZQo=", + "ecdsa_secp521r1_sha512": "ZXhhbXBsZQo=", + "ed25519": "ZXhhbXBsZQo=" + }, + "type": "object" + }, + "MLSReset": { + "properties": { + "epoch": { + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + } + }, + "required": [ + "group_id", + "epoch" + ], + "type": "object" + }, + "ManagedBy": { + "enum": [ + "wire", + "scim" + ], + "type": "string" + }, + "MeetingsConfig.Feature": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "MeetingsConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "MeetingsPremiumConfig.Feature": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "MeetingsPremiumConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "Member": { + "description": "The user ID of the requestor", + "properties": { + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/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": "#/components/schemas/Qualified_UserId" + }, + "service": { + "$ref": "#/components/schemas/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": "#/components/schemas/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": "#/components/schemas/Qualified_UserId" + }, + "target": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "qualified_target" + ], + "type": "object" + }, + "MembersJoin": { + "properties": { + "add_type": { + "$ref": "#/components/schemas/JoinType" + }, + "user_ids": { + "deprecated": true, + "description": "deprecated", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "users": { + "items": { + "$ref": "#/components/schemas/SimpleMember" + }, + "type": "array" + } + }, + "required": [ + "users", + "add_type" + ], + "type": "object" + }, + "MessageSendingStatus": { + "description": "The Proteus message sending status. It has these fields:\n- `time`: Time of sending message.\n- `missing`: Clients that the message /should/ have been encrypted for, but wasn't.\n- `redundant`: Clients that the message /should not/ have been encrypted for, but was.\n- `deleted`: Clients that were deleted.\n- `failed_to_send`: When message sending fails for some clients but succeeds for others, e.g., because a remote backend is unreachable, this field will contain the list of clients for which the message sending failed. This list should be empty when message sending is not even tried, like when some clients are missing.", + "properties": { + "deleted": { + "$ref": "#/components/schemas/QualifiedUserClients" + }, + "failed_to_confirm_clients": { + "$ref": "#/components/schemas/QualifiedUserClients" + }, + "failed_to_send": { + "$ref": "#/components/schemas/QualifiedUserClients" + }, + "missing": { + "$ref": "#/components/schemas/QualifiedUserClients" + }, + "redundant": { + "$ref": "#/components/schemas/QualifiedUserClients" + }, + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" + } + }, + "required": [ + "time", + "missing", + "redundant", + "deleted", + "failed_to_send", + "failed_to_confirm_clients" + ], + "type": "object" + }, + "MlsE2EIdConfig": { + "description": "When a client first tries to fetch or renew a certificate, they may need to login to an identity provider (IdP) depending on their IdP domain authentication policy. The user may have a grace period during which they can \"snooze\" this login. The duration of this grace period (in seconds) is set in the `verificationDuration` parameter, which is enforced separately by each client. After the grace period has expired, the client will not allow the user to use the application until they have logged to refresh the certificate. The default value is 1 day (86400s). The client enrolls using the Automatic Certificate Management Environment (ACME) protocol. The `acmeDiscoveryUrl` parameter must be set to the HTTPS URL of the ACME server discovery endpoint for this team. It is of the form \"https://acme.{backendDomain}/acme/{provisionerName}/discovery\". For example: `https://acme.example.com/acme/provisioner1/discovery`.", + "properties": { + "acmeDiscoveryUrl": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "crlProxy": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "useProxyOnMobile": { + "type": "boolean" + }, + "verificationExpiration": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "verificationExpiration" + ], + "type": "object" + }, + "MlsE2EIdConfig.Feature": { + "properties": { + "config": { + "$ref": "#/components/schemas/MlsE2EIdConfig" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "MlsE2EIdConfig.LockableFeature": { + "properties": { + "config": { + "$ref": "#/components/schemas/MlsE2EIdConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "MlsMigration": { + "properties": { + "finaliseRegardlessAfter": { + "example": "2021-05-12T10:52:02Z", + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string" + }, + "startTime": { + "example": "2021-05-12T10:52:02Z", + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string" + } + }, + "type": "object" + }, + "MlsMigration.Feature": { + "properties": { + "config": { + "$ref": "#/components/schemas/MlsMigration" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "MlsMigration.LockableFeature": { + "properties": { + "config": { + "$ref": "#/components/schemas/MlsMigration" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "NameIDFormat": { + "enum": [ + "NameIDFUnspecified", + "NameIDFEmail", + "NameIDFX509", + "NameIDFWindows", + "NameIDFKerberos", + "NameIDFEntity", + "NameIDFPersistent", + "NameIDFTransient" + ], + "type": "string" + }, + "NameIdPolicy": { + "properties": { + "allowCreate": { + "type": "boolean" + }, + "format": { + "$ref": "#/components/schemas/NameIDFormat" + }, + "spNameQualifier": { + "type": "string" + } + }, + "required": [ + "format", + "allowCreate" + ], + "type": "object" + }, + "NewApp": { + "properties": { + "app": { + "$ref": "#/components/schemas/GetApp" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "app", + "password" + ], + "type": "object" + }, + "NewAssetToken": { + "properties": { + "token": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "token" + ], + "type": "object" + }, + "NewClient": { + "properties": { + "capabilities": { + "$ref": "#/components/schemas/ClientCapabilityList" + }, + "class": { + "$ref": "#/components/schemas/ClientClass" + }, + "cookie": { + "description": "The cookie label, i.e. the label used when logging in.", + "type": "string" + }, + "label": { + "type": "string" + }, + "lastkey": { + "$ref": "#/components/schemas/Prekey" + }, + "mls_public_keys": { + "$ref": "#/components/schemas/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": "#/components/schemas/Prekey" + }, + "type": "array" + }, + "type": { + "$ref": "#/components/schemas/ClientType" + }, + "verification_code": { + "$ref": "#/components/schemas/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": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission" + }, + "cells": { + "type": "boolean" + }, + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType" + }, + "message_timer": { + "description": "Per-conversation message timer", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/BaseProtocol" + }, + "qualified_users": { + "description": "List of qualified user IDs (excluding the requestor) to be part of this conversation", + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "skip_creator": { + "description": "Don't add creator to the conversation, only works for team admins not wanting to be part of the channels they create.", + "type": "boolean" + }, + "team": { + "$ref": "#/components/schemas/ConvTeamInfo" + }, + "users": { + "deprecated": true, + "description": "List of user IDs (excluding the requestor) to be part of this conversation (deprecated)", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "type": "object" + }, + "NewLegalHoldService": { + "properties": { + "auth_token": { + "$ref": "#/components/schemas/ASCII" + }, + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "public_key": { + "$ref": "#/components/schemas/ServiceKeyPEM" + } + }, + "required": [ + "base_url", + "public_key", + "auth_token" + ], + "type": "object" + }, + "NewOne2OneConv": { + "description": "JSON object to create a new 1:1 conversation. When using 'qualified_users' (preferred), you can omit 'users'", + "properties": { + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "qualified_users": { + "description": "List of qualified user IDs (excluding the requestor) to be part of this conversation", + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + }, + "team": { + "$ref": "#/components/schemas/ConvTeamInfo" + }, + "users": { + "deprecated": true, + "description": "List of user IDs (excluding the requestor) to be part of this conversation (deprecated)", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "type": "object" + }, + "NewPasswordReset": { + "description": "Data to initiate a password reset", + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + }, + "phone": { + "description": "Email", + "type": "string" + } + }, + "type": "object" + }, + "NewProvider": { + "properties": { + "description": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "name", + "email", + "url", + "description" + ], + "type": "object" + }, + "NewProviderResponse": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "password": { + "maxLength": 1024, + "minLength": 8, + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "NewService": { + "properties": { + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "auth_token": { + "$ref": "#/components/schemas/ASCII" + }, + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "description": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "public_key": { + "$ref": "#/components/schemas/ServiceKeyPEM" + }, + "summary": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/ServiceTag" + }, + "maxItems": 3, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "name", + "summary", + "description", + "base_url", + "public_key", + "assets", + "tags" + ], + "type": "object" + }, + "NewServiceResponse": { + "properties": { + "auth_token": { + "$ref": "#/components/schemas/ASCII" + }, + "id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "NewTeamCollaborator": { + "properties": { + "permissions": { + "items": { + "$ref": "#/components/schemas/CollaboratorPermission" + }, + "type": "array" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user", + "permissions" + ], + "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": "#/components/schemas/UTCTimeMillis" + }, + "created_by": { + "$ref": "#/components/schemas/UUID" + }, + "permissions": { + "$ref": "#/components/schemas/Permissions" + }, + "user": { + "$ref": "#/components/schemas/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": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "email_code": { + "$ref": "#/components/schemas/ASCII" + }, + "expires_in": { + "maximum": 604800, + "minimum": 1, + "type": "integer" + }, + "invitation_code": { + "$ref": "#/components/schemas/ASCII" + }, + "label": { + "type": "string" + }, + "locale": { + "$ref": "#/components/schemas/Locale" + }, + "managed_by": { + "$ref": "#/components/schemas/ManagedBy" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 8, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict" + }, + "sso_id": { + "$ref": "#/components/schemas/UserSSOId" + }, + "supported_protocols": { + "items": { + "$ref": "#/components/schemas/BaseProtocol" + }, + "type": "array" + }, + "team": { + "$ref": "#/components/schemas/BindingNewTeamUser" + }, + "team_code": { + "$ref": "#/components/schemas/ASCII" + }, + "team_id": { + "$ref": "#/components/schemas/UUID" + }, + "uuid": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "NewUserGroup": { + "properties": { + "members": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "name": { + "maxLength": 4000, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name", + "members" + ], + "type": "object" + }, + "OAuthAccessTokenRequest": { + "properties": { + "client_id": { + "$ref": "#/components/schemas/UUID" + }, + "code": { + "$ref": "#/components/schemas/OAuthAuthorizationCode" + }, + "code_verifier": { + "description": "The code verifier to complete the code challenge", + "maxLength": 128, + "minLength": 43, + "type": "string" + }, + "grant_type": { + "$ref": "#/components/schemas/OAuthGrantType" + }, + "redirect_uri": { + "$ref": "#/components/schemas/RedirectUrl" + } + }, + "required": [ + "grant_type", + "client_id", + "code_verifier", + "code", + "redirect_uri" + ], + "type": "object" + }, + "OAuthAccessTokenResponse": { + "properties": { + "access_token": { + "description": "The access token, which has a relatively short lifetime", + "type": "string" + }, + "expires_in": { + "description": "The lifetime of the access token in seconds", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "refresh_token": { + "description": "The refresh token, which has a relatively long lifetime, and can be used to obtain a new access token", + "type": "string" + }, + "token_type": { + "$ref": "#/components/schemas/OAuthAccessTokenType" + } + }, + "required": [ + "access_token", + "token_type", + "expires_in", + "refresh_token" + ], + "type": "object" + }, + "OAuthAccessTokenType": { + "description": "The type of the access token. Currently only `Bearer` is supported.", + "enum": [ + "Bearer" + ], + "type": "string" + }, + "OAuthApplication": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "description": "The OAuth client's name", + "maxLength": 256, + "minLength": 6, + "type": "string" + }, + "sessions": { + "description": "The OAuth client's sessions", + "items": { + "$ref": "#/components/schemas/OAuthSession" + }, + "type": "array" + } + }, + "required": [ + "id", + "name", + "sessions" + ], + "type": "object" + }, + "OAuthAuthorizationCode": { + "description": "The authorization code", + "type": "string" + }, + "OAuthClient": { + "properties": { + "application_name": { + "maxLength": 256, + "minLength": 6, + "type": "string" + }, + "client_id": { + "$ref": "#/components/schemas/UUID" + }, + "redirect_url": { + "$ref": "#/components/schemas/RedirectUrl" + } + }, + "required": [ + "client_id", + "application_name", + "redirect_url" + ], + "type": "object" + }, + "OAuthCodeChallenge": { + "description": "Generated by the client from the code verifier (unpadded base64url-encoded SHA256 hash of the code verifier)", + "type": "string" + }, + "OAuthGrantType": { + "description": "Indicates which authorization flow to use. Use `authorization_code` for authorization code flow.", + "enum": [ + "authorization_code", + "refresh_token" + ], + "type": "string" + }, + "OAuthRefreshAccessTokenRequest": { + "properties": { + "client_id": { + "$ref": "#/components/schemas/UUID" + }, + "grant_type": { + "$ref": "#/components/schemas/OAuthGrantType" + }, + "refresh_token": { + "description": "The refresh token", + "type": "string" + } + }, + "required": [ + "grant_type", + "client_id", + "refresh_token" + ], + "type": "object" + }, + "OAuthResponseType": { + "description": "Indicates which authorization flow to use. Use `code` for authorization code flow.", + "enum": [ + "code" + ], + "type": "string" + }, + "OAuthRevokeRefreshTokenRequest": { + "properties": { + "client_id": { + "$ref": "#/components/schemas/UUID" + }, + "refresh_token": { + "description": "The refresh token", + "type": "string" + } + }, + "required": [ + "client_id", + "refresh_token" + ], + "type": "object" + }, + "OAuthSession": { + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "refresh_token_id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "refresh_token_id", + "created_at" + ], + "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": "#/components/schemas/RoleName" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "service": { + "$ref": "#/components/schemas/ServiceRef" + }, + "status": { + "deprecated": true, + "description": "deprecated", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "qualified_id" + ], + "type": "object" + }, + "OtherMemberUpdate": { + "description": "Update user properties of other members relative to a conversation", + "properties": { + "conversation_role": { + "$ref": "#/components/schemas/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": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "sender": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "text": { + "description": "The ciphertext for the recipient (Base64 in JSON)", + "type": "string" + } + }, + "required": [ + "sender", + "recipient", + "text" + ], + "type": "object" + }, + "OutlookCalIntegrationConfig.Feature": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "OutlookCalIntegrationConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "OwnConvMembers": { + "description": "Users of a conversation", + "properties": { + "others": { + "description": "All other current users of this conversation", + "items": { + "$ref": "#/components/schemas/OtherMember" + }, + "type": "array" + }, + "self": { + "$ref": "#/components/schemas/Member" + } + }, + "required": [ + "self", + "others" + ], + "type": "object" + }, + "OwnConversation": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/OwnConvMembers" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/Protocol" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch" + ], + "type": "object" + }, + "OwnConversationV2": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/components/schemas/AccessRoleLegacy" + }, + "access_role_v2": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/EpochTimestamp" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/OwnConvMembers" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/Protocol" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "members", + "group_id", + "epoch", + "epoch_timestamp", + "cipher_suite" + ], + "type": "object" + }, + "OwnConversationV3": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/EpochTimestamp" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/OwnConvMembers" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/Protocol" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch", + "epoch_timestamp", + "cipher_suite" + ], + "type": "object" + }, + "OwnConversationV6": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/OwnConvMembers" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/Protocol" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch" + ], + "type": "object" + }, + "OwnConversationV9": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "add_permission": { + "$ref": "#/components/schemas/AddPermission" + }, + "cells_state": { + "$ref": "#/components/schemas/CellsState" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_conv_type": { + "$ref": "#/components/schemas/GroupConvType" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/OwnConvMembers" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/UUID" + }, + "protocol": { + "$ref": "#/components/schemas/Protocol" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch" + ], + "type": "object" + }, + "OwnKeyPackages": { + "properties": { + "count": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "count" + ], + "type": "object" + }, + "PagingState": { + "description": "Paging state that should be supplied to retrieve the next page of results", + "type": "string" + }, + "PasswordChange": { + "properties": { + "new_password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "old_password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "old_password", + "new_password" + ], + "type": "object" + }, + "PasswordReqBody": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "PasswordReset": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "Permissions": { + "description": "This is just a complicated way of representing a team role. self and copy always have to contain the same integer, and only the following integers are allowed: 1025 (partner), 1587 (member), 5951 (admin), 8191 (owner). Unit tests of the galley-types package in wire-server contain an authoritative list.", + "properties": { + "copy": { + "description": "Permissions that this user is able to grant others", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "self": { + "description": "Permissions that the user has", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "self", + "copy" + ], + "type": "object" + }, + "PhoneNumber": { + "description": "A known phone number with a pending password reset.", + "type": "string" + }, + "Pict": { + "items": { + "type": "object" + }, + "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": "#/components/schemas/ClientPrekey" + }, + "type": "array" + }, + "user": { + "$ref": "#/components/schemas/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", + "mixed" + ], + "type": "string" + }, + "ProtocolUpdate": { + "properties": { + "protocol": { + "$ref": "#/components/schemas/Protocol" + } + }, + "type": "object" + }, + "Provider": { + "properties": { + "description": { + "type": "string" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "id", + "name", + "email", + "url", + "description" + ], + "type": "object" + }, + "ProviderActivationResponse": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "ProviderLogin": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "email", + "password" + ], + "type": "object" + }, + "PubClient": { + "properties": { + "class": { + "$ref": "#/components/schemas/ClientClass" + }, + "id": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "PublicSubConversation": { + "description": "An MLS subconversation", + "properties": { + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "members": { + "items": { + "$ref": "#/components/schemas/ClientIdentity" + }, + "type": "array" + }, + "parent_qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "subconv_id": { + "type": "string" + } + }, + "required": [ + "parent_qualified_id", + "subconv_id", + "group_id", + "epoch", + "members" + ], + "type": "object" + }, + "PushToken": { + "description": "Native Push Token", + "properties": { + "app": { + "description": "Application", + "type": "string" + }, + "client": { + "description": "Client ID", + "type": "string" + }, + "token": { + "description": "Access Token", + "type": "string" + }, + "transport": { + "$ref": "#/components/schemas/Transport" + } + }, + "required": [ + "transport", + "app", + "token", + "client" + ], + "type": "object" + }, + "PushTokenList": { + "description": "List of Native Push Tokens", + "properties": { + "tokens": { + "description": "Push tokens", + "items": { + "$ref": "#/components/schemas/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." + }, + "QualifiedUserClientPrekeyMapV4": { + "properties": { + "failed_to_list": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + }, + "qualified_user_client_prekeys": { + "additionalProperties": { + "$ref": "#/components/schemas/UserClientPrekeyMap" + }, + "type": "object" + } + }, + "required": [ + "qualified_user_client_prekeys" + ], + "type": "object" + }, + "QualifiedUserClients": { + "additionalProperties": { + "additionalProperties": { + "items": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "type": "array" + }, + "type": "object" + }, + "description": "Map of Domain to UserClients", + "example": { + "domain1.example.com": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ + "60f85e4b15ad3786", + "6e323ab31554353b" + ] + } + }, + "type": "object" + }, + "QualifiedUserIdList_with_EdMemberLeftReason": { + "properties": { + "qualified_user_ids": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + }, + "reason": { + "$ref": "#/components/schemas/EdMemberLeftReason" + }, + "user_ids": { + "deprecated": true, + "description": "Deprecated, use qualified_user_ids", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "reason", + "qualified_user_ids", + "user_ids" + ], + "type": "object" + }, + "QualifiedUserMap_Set_PubClient": { + "additionalProperties": { + "$ref": "#/components/schemas/UserMap_Set_PubClient" + }, + "description": "Map of Domain to (UserMap (Set_PubClient)).", + "example": { + "domain1.example.com": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ + { + "class": "legalhold", + "id": "d0" + } + ] + } + }, + "type": "object" + }, + "Qualified_ConvId": { + "properties": { + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "domain", + "id" + ], + "type": "object" + }, + "Qualified_Handle": { + "properties": { + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + } + }, + "required": [ + "domain", + "handle" + ], + "type": "object" + }, + "Qualified_UserId": { + "properties": { + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "domain", + "id" + ], + "type": "object" + }, + "QueuedNotification": { + "description": "A single notification", + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "payload": { + "description": "List of events", + "items": { + "$ref": "#/components/schemas/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": "#/components/schemas/QueuedNotification" + }, + "type": "array" + }, + "time": { + "$ref": "#/components/schemas/UTCTime" + } + }, + "required": [ + "notifications" + ], + "type": "object" + }, + "RTCConfiguration": { + "description": "A subset of the WebRTC 'RTCConfiguration' dictionary", + "properties": { + "ice_servers": { + "description": "Array of 'RTCIceServer' objects", + "items": { + "$ref": "#/components/schemas/RTCIceServer" + }, + "minItems": 1, + "type": "array" + }, + "is_federating": { + "description": "True if the client should connect to an SFT in the sft_servers_all and request it to federate", + "type": "boolean" + }, + "sft_servers": { + "description": "Array of 'SFTServer' objects (optional)", + "items": { + "$ref": "#/components/schemas/SftServer" + }, + "minItems": 1, + "type": "array" + }, + "sft_servers_all": { + "description": "Array of all SFT servers", + "items": { + "$ref": "#/components/schemas/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": "#/components/schemas/ASCII" + }, + "urls": { + "description": "Array of TURN server addresses of the form 'turn::'", + "items": { + "$ref": "#/components/schemas/TurnURI" + }, + "minItems": 1, + "type": "array" + }, + "username": { + "$ref": "#/components/schemas/TurnUsername" + } + }, + "required": [ + "urls", + "username", + "credential" + ], + "type": "object" + }, + "RedirectUrl": { + "description": "The URL must match the URL that was used to generate the authorization code.", + "type": "string" + }, + "RefreshAppCookieResponse": { + "properties": { + "cookie": { + "$ref": "#/components/schemas/SomeUserToken" + } + }, + "required": [ + "cookie" + ], + "type": "object" + }, + "RegisteredDomains": { + "properties": { + "registered_domains": { + "items": { + "$ref": "#/components/schemas/DomainRegistrationResponse" + }, + "type": "array" + } + }, + "required": [ + "registered_domains" + ], + "type": "object" + }, + "Relation": { + "enum": [ + "accepted", + "blocked", + "pending", + "ignored", + "sent", + "cancelled", + "missing-legalhold-consent" + ], + "type": "string" + }, + "RemoveBotResponse": { + "properties": { + "event": { + "$ref": "#/components/schemas/Event" + } + }, + "required": [ + "event" + ], + "type": "object" + }, + "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": { + "description": "json object with case-insensitive fields.", + "properties": { + "fields": { + "items": { + "$ref": "#/components/schemas/RichField" + }, + "type": "array" + }, + "version": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "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" + }, + "SFTUsername": { + "description": "String containing the SFT username", + "type": "string" + }, + "SSOConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "ScimTokenInfo": { + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTime" + }, + "description": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "idp": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + }, + "team": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "team", + "id", + "created_at", + "description", + "name" + ], + "type": "object" + }, + "ScimTokenList": { + "properties": { + "tokens": { + "items": { + "$ref": "#/components/schemas/ScimTokenInfo" + }, + "type": "array" + } + }, + "required": [ + "tokens" + ], + "type": "object" + }, + "ScimTokenName": { + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "SearchResult_Contact": { + "properties": { + "documents": { + "description": "List of contacts found", + "items": { + "$ref": "#/components/schemas/Contact" + }, + "type": "array" + }, + "found": { + "description": "Total number of hits", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "has_more": { + "description": "Indicates whether there are more results to be fetched", + "type": "boolean" + }, + "paging_state": { + "$ref": "#/components/schemas/PagingState" + }, + "returned": { + "description": "Total number of hits returned", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "search_policy": { + "$ref": "#/components/schemas/FederatedUserSearchPolicy" + }, + "took": { + "description": "Search time in ms", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "found", + "returned", + "took", + "documents", + "search_policy" + ], + "type": "object" + }, + "SearchResult_TeamContact": { + "properties": { + "documents": { + "description": "List of contacts found", + "items": { + "$ref": "#/components/schemas/TeamContact" + }, + "type": "array" + }, + "found": { + "description": "Total number of hits", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "has_more": { + "description": "Indicates whether there are more results to be fetched", + "type": "boolean" + }, + "paging_state": { + "$ref": "#/components/schemas/PagingState" + }, + "returned": { + "description": "Total number of hits returned", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "search_policy": { + "$ref": "#/components/schemas/FederatedUserSearchPolicy" + }, + "took": { + "description": "Search time in ms", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "found", + "returned", + "took", + "documents", + "search_policy" + ], + "type": "object" + }, + "SearchVisibilityAvailableConfig.Feature": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "SearchVisibilityAvailableConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "SearchVisibilityInboundConfig.Feature": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "SearchVisibilityInboundConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "SelfDeletingMessagesConfig": { + "properties": { + "enforcedTimeoutSeconds": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "enforcedTimeoutSeconds" + ], + "type": "object" + }, + "SelfDeletingMessagesConfig.Feature": { + "properties": { + "config": { + "$ref": "#/components/schemas/SelfDeletingMessagesConfig" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "SelfDeletingMessagesConfig.LockableFeature": { + "properties": { + "config": { + "$ref": "#/components/schemas/SelfDeletingMessagesConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "SendActivationCode": { + "description": "Data for requesting an email code to be sent. 'email' must be present.", + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + }, + "locale": { + "$ref": "#/components/schemas/Locale" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "SendVerificationCode": { + "properties": { + "action": { + "$ref": "#/components/schemas/VerificationAction" + }, + "email": { + "$ref": "#/components/schemas/Email" + } + }, + "required": [ + "action", + "email" + ], + "type": "object" + }, + "ServerTime": { + "description": "The current server time", + "properties": { + "time": { + "$ref": "#/components/schemas/UTCTime" + } + }, + "required": [ + "time" + ], + "type": "object" + }, + "Service": { + "properties": { + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "auth_tokens": { + "items": { + "$ref": "#/components/schemas/ASCII" + }, + "minItems": 1, + "type": "array" + }, + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "public_keys": { + "items": { + "$ref": "#/components/schemas/ServiceKey" + }, + "minItems": 1, + "type": "array" + }, + "summary": { + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/ServiceTag" + }, + "type": "array" + } + }, + "required": [ + "id", + "name", + "summary", + "description", + "base_url", + "auth_tokens", + "public_keys", + "assets", + "tags", + "enabled" + ], + "type": "object" + }, + "ServiceKey": { + "properties": { + "pem": { + "$ref": "#/components/schemas/ServiceKeyPEM" + }, + "size": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "type": { + "$ref": "#/components/schemas/ServiceKeyType" + } + }, + "required": [ + "type", + "size", + "pem" + ], + "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" + }, + "ServiceKeyType": { + "enum": [ + "rsa" + ], + "type": "string" + }, + "ServiceProfile": { + "properties": { + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "provider": { + "$ref": "#/components/schemas/UUID" + }, + "summary": { + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/ServiceTag" + }, + "type": "array" + } + }, + "required": [ + "id", + "provider", + "name", + "summary", + "description", + "assets", + "tags", + "enabled" + ], + "type": "object" + }, + "ServiceProfilePage": { + "properties": { + "has_more": { + "type": "boolean" + }, + "services": { + "items": { + "$ref": "#/components/schemas/ServiceProfile" + }, + "type": "array" + } + }, + "required": [ + "has_more", + "services" + ], + "type": "object" + }, + "ServiceRef": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "provider": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "id", + "provider" + ], + "type": "object" + }, + "ServiceTag": { + "enum": [ + "audio", + "books", + "business", + "design", + "education", + "entertainment", + "finance", + "fitness", + "food-drink", + "games", + "graphics", + "health", + "integration", + "lifestyle", + "media", + "medical", + "movies", + "music", + "news", + "photography", + "poll", + "productivity", + "quiz", + "rating", + "shopping", + "social", + "sports", + "travel", + "tutorial", + "video", + "weather" + ], + "type": "string" + }, + "ServiceTagList": { + "items": { + "$ref": "#/components/schemas/ServiceTag" + }, + "type": "array" + }, + "SetSearchable": { + "properties": { + "set_searchable": { + "type": "boolean" + } + }, + "required": [ + "set_searchable" + ], + "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": "#/components/schemas/HttpsUrl" + }, + "type": "array" + } + }, + "required": [ + "urls" + ], + "type": "object" + }, + "SimpleMember": { + "properties": { + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_UserId" + } + }, + "required": [ + "qualified_id" + ], + "type": "object" + }, + "SimplifiedUserConnectionRequestQRCode.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "SndFactorPasswordChallengeConfig.Feature": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "SndFactorPasswordChallengeConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "SomeKey": {}, + "SomeUserToken": { + "type": "string" + }, + "Sso": { + "properties": { + "issuer": { + "type": "string" + }, + "nameid": { + "type": "string" + } + }, + "required": [ + "issuer", + "nameid" + ], + "type": "object" + }, + "SsoSettings": { + "properties": { + "default_sso_code": { + "$ref": "#/components/schemas/UUID" + } + }, + "type": "object" + }, + "StealthUsersConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "SupportedProtocolUpdate": { + "properties": { + "supported_protocols": { + "items": { + "$ref": "#/components/schemas/BaseProtocol" + }, + "type": "array" + } + }, + "required": [ + "supported_protocols" + ], + "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": { + "description": "`binding` is deprecated, and should be ignored. The non-binding teams API is not used (and will not be supported from API version V4 onwards), and `binding` will always be `true`.", + "properties": { + "binding": { + "$ref": "#/components/schemas/TeamBinding" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "icon": { + "$ref": "#/components/schemas/Icon" + }, + "icon_key": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + }, + "splash_screen": { + "$ref": "#/components/schemas/Icon" + } + }, + "required": [ + "id", + "creator", + "name", + "icon" + ], + "type": "object" + }, + "TeamBinding": { + "deprecated": true, + "description": "Deprecated, please ignore.", + "enum": [ + true, + false + ], + "type": "boolean" + }, + "TeamCollaborator": { + "properties": { + "permissions": { + "items": { + "$ref": "#/components/schemas/CollaboratorPermission" + }, + "type": "array" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user", + "team", + "permissions" + ], + "type": "object" + }, + "TeamContact": { + "properties": { + "accent_id": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "email_unvalidated": { + "$ref": "#/components/schemas/Email" + }, + "handle": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "managed_by": { + "$ref": "#/components/schemas/ManagedBy" + }, + "name": { + "type": "string" + }, + "role": { + "$ref": "#/components/schemas/Role" + }, + "saml_idp": { + "type": "string" + }, + "scim_external_id": { + "type": "string" + }, + "searchable": { + "type": "boolean" + }, + "sso": { + "$ref": "#/components/schemas/Sso" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "user_groups": { + "description": "List of user group ids the user is a member of", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "id", + "name", + "user_groups", + "searchable" + ], + "type": "object" + }, + "TeamConversation": { + "description": "Team conversation data", + "properties": { + "conversation": { + "$ref": "#/components/schemas/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": "#/components/schemas/TeamConversation" + }, + "type": "array" + } + }, + "required": [ + "conversations" + ], + "type": "object" + }, + "TeamDeleteData": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "verification_code": { + "$ref": "#/components/schemas/ASCII" + } + }, + "type": "object" + }, + "TeamDomainRedirectTag": { + "enum": [ + "no-registration", + "none" + ], + "type": "string" + }, + "TeamInvite Tag": { + "enum": [ + "allowed", + "not-allowed", + "team" + ], + "type": "string" + }, + "TeamInviteConfig": { + "properties": { + "domain_redirect": { + "$ref": "#/components/schemas/TeamDomainRedirectTag" + }, + "sso": { + "example": "99db9768-04e3-4b5d-9268-831b6a25c4ab", + "format": "uuid", + "type": "string" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "team_invite": { + "$ref": "#/components/schemas/TeamInvite Tag" + } + }, + "required": [ + "team_invite", + "team" + ], + "type": "object" + }, + "TeamMember": { + "description": "team member data", + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "created_by": { + "$ref": "#/components/schemas/UUID" + }, + "legalhold_status": { + "$ref": "#/components/schemas/UserLegalHoldStatus" + }, + "permissions": { + "$ref": "#/components/schemas/Permissions" + }, + "user": { + "$ref": "#/components/schemas/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": "#/components/schemas/ListType" + }, + "members": { + "description": "the array of team members", + "items": { + "$ref": "#/components/schemas/TeamMember" + }, + "type": "array" + } + }, + "required": [ + "members", + "hasMore" + ], + "type": "object" + }, + "TeamMembersPage": { + "properties": { + "hasMore": { + "type": "boolean" + }, + "members": { + "items": { + "$ref": "#/components/schemas/TeamMember" + }, + "type": "array" + }, + "pagingState": { + "$ref": "#/components/schemas/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": "#/components/schemas/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": "#/components/schemas/Icon" + }, + "icon_key": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "splash_screen": { + "$ref": "#/components/schemas/Icon" + } + }, + "type": "object" + }, + "Time": { + "properties": { + "time": { + "$ref": "#/components/schemas/UTCTime" + } + }, + "required": [ + "time" + ], + "type": "object" + }, + "Token": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "TokenType": { + "enum": [ + "Bearer" + ], + "type": "string" + }, + "Transport": { + "description": "Transport", + "enum": [ + "GCM", + "APNS", + "APNS_SANDBOX", + "APNS_VOIP", + "APNS_VOIP_SANDBOX" + ], + "type": "string" + }, + "TurnURI": { + "type": "string" + }, + "TurnUsername": { + "description": "Username to use for authenticating against the given TURN servers", + "type": "string" + }, + "TypingData": { + "properties": { + "status": { + "$ref": "#/components/schemas/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:02Z", + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string" + }, + "UTCTimeMillis": { + "description": "The time when the session was created", + "example": "2021-05-12T10:52:02.671Z", + "format": "yyyy-mm-ddThh:MM:ss.qqqZ", + "type": "string" + }, + "UUID": { + "description": "The OAuth client's ID", + "example": "99db9768-04e3-4b5d-9268-831b6a25c4ab", + "format": "uuid", + "type": "string" + }, + "Unnamed": { + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "created_by": { + "$ref": "#/components/schemas/UUID" + }, + "permissions": { + "$ref": "#/components/schemas/Permissions" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user", + "permissions" + ], + "type": "object" + }, + "UpdateBotPrekeys": { + "properties": { + "prekeys": { + "items": { + "$ref": "#/components/schemas/Prekey" + }, + "type": "array" + } + }, + "required": [ + "prekeys" + ], + "type": "object" + }, + "UpdateClient": { + "properties": { + "capabilities": { + "$ref": "#/components/schemas/ClientCapabilityList" + }, + "label": { + "description": "A new name for this client.", + "type": "string" + }, + "lastkey": { + "$ref": "#/components/schemas/Prekey" + }, + "mls_public_keys": { + "$ref": "#/components/schemas/MLSPublicKeys" + }, + "prekeys": { + "description": "New prekeys for other clients to establish OTR sessions.", + "items": { + "$ref": "#/components/schemas/Prekey" + }, + "type": "array" + } + }, + "type": "object" + }, + "UpdateProvider": { + "properties": { + "description": { + "type": "string" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "type": "object" + }, + "UpdateService": { + "properties": { + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "description": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "summary": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/ServiceTag" + }, + "maxItems": 3, + "minItems": 1, + "type": "array" + } + }, + "type": "object" + }, + "UpdateServiceConn": { + "properties": { + "auth_tokens": { + "items": { + "$ref": "#/components/schemas/ASCII" + }, + "maxItems": 2, + "minItems": 1, + "type": "array" + }, + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "enabled": { + "type": "boolean" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "public_keys": { + "items": { + "$ref": "#/components/schemas/ServiceKeyPEM" + }, + "maxItems": 2, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "UpdateServiceWhitelist": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "provider": { + "$ref": "#/components/schemas/UUID" + }, + "whitelisted": { + "type": "boolean" + } + }, + "required": [ + "provider", + "id", + "whitelisted" + ], + "type": "object" + }, + "UpdateUserGroupChannels": { + "properties": { + "channels": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "channels" + ], + "type": "object" + }, + "UpdateUserGroupMembers": { + "properties": { + "members": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "members" + ], + "type": "object" + }, + "User": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "email_unvalidated": { + "$ref": "#/components/schemas/Email" + }, + "expires_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "locale": { + "$ref": "#/components/schemas/Locale" + }, + "managed_by": { + "$ref": "#/components/schemas/ManagedBy" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "searchable": { + "type": "boolean" + }, + "service": { + "$ref": "#/components/schemas/ServiceRef" + }, + "sso_id": { + "$ref": "#/components/schemas/UserSSOId" + }, + "status": { + "$ref": "#/components/schemas/AccountStatus" + }, + "supported_protocols": { + "items": { + "$ref": "#/components/schemas/BaseProtocol" + }, + "type": "array" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "text_status": { + "maxLength": 256, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "qualified_id", + "name", + "accent_id", + "status", + "locale" + ], + "type": "object" + }, + "UserAsset": { + "properties": { + "key": { + "$ref": "#/components/schemas/AssetKey" + }, + "size": { + "$ref": "#/components/schemas/AssetSize" + }, + "type": { + "$ref": "#/components/schemas/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": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": { + "44901fb0712e588f": { + "id": 1, + "key": "pQABAQECoQBYIOjl7hw0D8YRNq..." + } + } + }, + "type": "object" + }, + "UserClients": { + "additionalProperties": { + "items": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "type": "array" + }, + "description": "Map of user id to list of client ids.", + "example": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ + "60f85e4b15ad3786", + "6e323ab31554353b" + ] + }, + "type": "object" + }, + "UserConnection": { + "properties": { + "conversation": { + "$ref": "#/components/schemas/UUID" + }, + "from": { + "$ref": "#/components/schemas/UUID" + }, + "last_update": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "qualified_conversation": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "qualified_to": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "status": { + "$ref": "#/components/schemas/Relation" + }, + "to": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "from", + "qualified_to", + "status", + "last_update" + ], + "type": "object" + }, + "UserGroup": { + "properties": { + "channels": { + "items": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "type": "array" + }, + "channelsCount": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "createdAt": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "managedBy": { + "$ref": "#/components/schemas/ManagedBy" + }, + "members": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "membersCount": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "maxLength": 4000, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "id", + "name", + "members", + "managedBy", + "createdAt" + ], + "type": "object" + }, + "UserGroupAddUsers": { + "properties": { + "members": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "members" + ], + "type": "object" + }, + "UserGroupMeta": { + "properties": { + "channels": { + "items": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "type": "array" + }, + "channelsCount": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "createdAt": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "managedBy": { + "$ref": "#/components/schemas/ManagedBy" + }, + "membersCount": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "maxLength": 4000, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "id", + "name", + "managedBy", + "createdAt" + ], + "type": "object" + }, + "UserGroupNameAvailability": { + "properties": { + "name_available": { + "type": "boolean" + } + }, + "required": [ + "name_available" + ], + "type": "object" + }, + "UserGroupPage": { + "description": "This is the last page if it contains fewer rows than requested. There may be 0 rows on a page.", + "properties": { + "page": { + "items": { + "$ref": "#/components/schemas/UserGroupMeta" + }, + "type": "array" + }, + "total": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "page", + "total" + ], + "type": "object" + }, + "UserGroupUpdate": { + "properties": { + "name": { + "maxLength": 4000, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "UserIdList": { + "properties": { + "user_ids": { + "items": { + "$ref": "#/components/schemas/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": "#/components/schemas/Id" + }, + "last_prekey": { + "$ref": "#/components/schemas/Prekey" + }, + "status": { + "$ref": "#/components/schemas/UserLegalHoldStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "UserMap_Set_PubClient": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/PubClient" + }, + "type": "array", + "uniqueItems": true + }, + "description": "Map of UserId to (Set PubClient)", + "example": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ + { + "class": "legalhold", + "id": "d0" + } + ] + }, + "type": "object" + }, + "UserProfile": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "expires_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "legalhold_status": { + "$ref": "#/components/schemas/UserLegalHoldStatus" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "searchable": { + "type": "boolean" + }, + "service": { + "$ref": "#/components/schemas/ServiceRef" + }, + "supported_protocols": { + "items": { + "$ref": "#/components/schemas/BaseProtocol" + }, + "type": "array" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "text_status": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/UserType" + } + }, + "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" + }, + "UserType": { + "enum": [ + "regular", + "app", + "bot" + ], + "type": "string" + }, + "UserUpdate": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict" + }, + "text_status": { + "maxLength": 256, + "minLength": 1, + "type": "string" + } + }, + "type": "object" + }, + "ValidateSAMLEmailsConfig.LockableFeature": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "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": "#/components/schemas/ASCII" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "key", + "code" + ], + "type": "object" + }, + "VersionInfo": { + "example": { + "development": [ + 14 + ], + "domain": "example.com", + "federation": false, + "supported": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14 + ] + }, + "properties": { + "development": { + "items": { + "$ref": "#/components/schemas/VersionNumber" + }, + "type": "array" + }, + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "federation": { + "type": "boolean" + }, + "supported": { + "items": { + "$ref": "#/components/schemas/VersionNumber" + }, + "type": "array" + } + }, + "required": [ + "supported", + "development", + "federation", + "domain" + ], + "type": "object" + }, + "VersionNumber": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14 + ], + "type": "integer" + }, + "ViewLegalHoldService": { + "properties": { + "settings": { + "$ref": "#/components/schemas/ViewLegalHoldServiceInfo" + }, + "status": { + "$ref": "#/components/schemas/LHServiceStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "ViewLegalHoldServiceInfo": { + "properties": { + "auth_token": { + "$ref": "#/components/schemas/ASCII" + }, + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "fingerprint": { + "$ref": "#/components/schemas/Fingerprint" + }, + "public_key": { + "$ref": "#/components/schemas/ServiceKeyPEM" + }, + "team_id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "team_id", + "base_url", + "fingerprint", + "auth_token", + "public_key" + ], + "type": "object" + }, + "WireIdP": { + "properties": { + "apiVersion": { + "$ref": "#/components/schemas/WireIdPAPIVersion" + }, + "domain": { + "type": "string" + }, + "handle": { + "type": "string" + }, + "oldIssuers": { + "items": { + "type": "string" + }, + "type": "array" + }, + "replacedBy": { + "$ref": "#/components/schemas/UUID" + }, + "team": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "team", + "oldIssuers", + "handle" + ], + "type": "object" + }, + "WireIdPAPIVersion": { + "enum": [ + "WireIdPAPIV1", + "WireIdPAPIV2" + ], + "type": "string" + }, + "backend_config": { + "properties": { + "config_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "webapp_url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "config_url", + "webapp_url" + ], + "type": "object" + }, + "new-otr-message": { + "properties": { + "data": { + "type": "string" + }, + "native_priority": { + "$ref": "#/components/schemas/Priority" + }, + "native_push": { + "type": "boolean" + }, + "recipients": { + "$ref": "#/components/schemas/UserClientMap" + }, + "report_missing": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "sender": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "transient": { + "type": "boolean" + } + }, + "required": [ + "sender", + "recipients" + ], + "type": "object" + } + }, + "securitySchemes": { + "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" + } + } + }, + "info": { + "description": "## Authentication / Authorization\n\nThe end-points in this API support differing authorization protocols:\nsome are unauthenticated (`/api-version`, `/login`), some require\n[zauth](), and some support both [zauth]() and [oauth]().\n\nThe end-points that require zauth are labelled so in the description\nbelow. The end-points that support oauth as an alternative to zauth\nhave the required oauth scopes listed in the same description.\n\nFuther reading:\n- https://docs.wire.com/developer/reference/oauth.html\n- https://github.com/wireapp/wire-server/blob/develop/libs/wire-api/src/Wire/API/Routes/Public.hs (search for HasSwagger instances)\n- `curl https://staging-nginz-https.zinfra.io/v4/api/swagger.json | jq '.security, .securityDefinitions`\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: 422, 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": "" + }, + "openapi": "3.0.0", + "paths": { + "/access": { + "post": { + "description": " [internal route ID: \"access\"]\n\nYou 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.", + "operationId": "access", + "parameters": [ + { + "in": "query", + "name": "client_id", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessToken" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AccessToken" + } + } + }, + "description": "OK", + "headers": { + "Set-Cookie": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)" + } + }, + "summary": "Obtain an access tokens for a cookie" + } + }, + "/access/logout": { + "post": { + "description": " [internal route ID: \"logout\"]\n\nCalling this endpoint will effectively revoke the given cookie and subsequent calls to /access with the same cookie will result in a 403.", + "operationId": "logout", + "responses": { + "200": { + "description": "Logout" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)" + } + }, + "summary": "Log out in order to remove a cookie from the server" + } + }, + "/access/self/email": { + "put": { + "description": " [internal route ID: \"change-self-email\"]\n\n", + "operationId": "change-self-email", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/EmailUpdate" + } + } + }, + "required": true + }, + "responses": { + "202": { + "content": { + "application/json": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "Update accepted and pending activation of the new email" + }, + "204": { + "content": { + "application/json": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "No update, current and new email address are the same\n\nEmail address activated" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Invalid e-mail address. (label: `invalid-email`) or `body`" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "blacklisted-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address is in use. (label: `key-exists`)" + } + }, + "summary": "Change your email address" + } + }, + "/activate": { + "get": { + "description": " [internal route ID: \"get-activate\"]\n\nSee also 'POST /activate' which has a larger feature set.", + "operationId": "get-activate", + "parameters": [ + { + "description": "Activation key", + "in": "query", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Activation code", + "in": "query", + "name": "code", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActivationResponse" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ActivationResponse" + } + } + }, + "description": "Activation successful.\n\nActivation successful. (Dry run)\n\nActivation successful." + }, + "204": { + "description": "A recent activation was already successful." + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-phone", + "message": "Invalid mobile phone number" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-phone", + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `code` or `key`\n\nInvalid mobile phone number (label: `invalid-phone`)\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Invalid activation code (label: `invalid-code`)\n\nUser does not exist (label: `invalid-code`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address is in use. (label: `key-exists`)" + } + }, + "summary": "Activate (i.e. confirm) an email address." + }, + "post": { + "description": " [internal route ID: \"post-activate\"]\n\nActivation only succeeds once and the number of failed attempts for a valid key is limited.", + "operationId": "post-activate", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Activate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActivationResponse" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ActivationResponse" + } + } + }, + "description": "Activation successful.\n\nActivation successful. (Dry run)\n\nActivation successful." + }, + "204": { + "description": "A recent activation was already successful." + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-phone", + "message": "Invalid mobile phone number" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-phone", + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid mobile phone number (label: `invalid-phone`)\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Invalid activation code (label: `invalid-code`)\n\nUser does not exist (label: `invalid-code`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address is in use. (label: `key-exists`)" + } + }, + "summary": "Activate (i.e. confirm) an email address." + } + }, + "/activate/send": { + "post": { + "description": " [internal route ID: \"post-activate-send\"]\n\n", + "operationId": "post-activate-send", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SendActivationCode" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Activation code sent." + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Invalid `body`\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "blacklisted-email", + "message": "The given e-mail address has been blacklisted due to a permanent bounce or a complaint." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "blacklisted-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address is in use. (label: `key-exists`)" + }, + "451": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 451, + "label": "domain-blocked-for-registration", + "message": "[Customer extension] The email domain has been blocked for 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" + } + } + }, + "description": "[Customer extension] The email domain has been blocked for Wire users. Please contact your IT department. (label: `domain-blocked-for-registration`)" + } + }, + "summary": "Send (or resend) an email activation code." + } + }, + "/api-version": { + "get": { + "description": " [internal route ID: \"get-version\"]\n\n", + "operationId": "get-version", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/VersionInfo" + } + } + }, + "description": "" + } + } + } + }, + "/assets": { + "post": { + "description": " [internal route ID: \"assets-upload\"]\n\n

Construct the request as multipart/mixed; set header Content-Type: multipart/mixed; boundary=<boundary>.

Use exactly two parts in this order:

  1. application/json metadata (AssetSettings)
  2. application/octet-stream asset bytes

Each part must include Content-Type and Content-Length; the second part may include Content-MD5. Use CRLF between headers and bodies.

When asset audit logging is enabled, the JSON metadata must include:

  • convId: object { id: UUID, domain: String } (qualified conversation ID)
  • filename: String
  • filetype: String MIME type (e.g. image/png, application/pdf)

Optional metadata: public (Bool, default false), retention (one of eternal, persistent, volatile, eternal-infrequent_access, expiring).

For profile pictures or team icons without a conversation, set convId.id to 00000000-0000-0000-0000-000000000000 and convId.domain to the tenant’s domain; use any reasonable filename.

Note: the server treats the asset bytes as application/octet-stream; filetype is used for auditing only.

Example body (boundary=frontier):

Content-Type: multipart/mixed; boundary=frontier

--frontier
Content-Type: application/json
Content-Length: 191

{\"public\":false,\"retention\":\"volatile\",\"convId\":{\"id\":\"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\",\"domain\":\"example.com\"},\"filename\":\"report.pdf\",\"filetype\":\"application/pdf\"}
--frontier
Content-Type: application/octet-stream
Content-Length: 11

Hello Audit
--frontier--
", + "operationId": "assets-upload", + "requestBody": { + "content": { + "multipart/mixed": { + "schema": { + "$ref": "#/components/schemas/AssetSource" + } + } + }, + "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." + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Asset" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Asset" + } + } + }, + "description": "Asset posted", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "incomplete-body", + "message": "HTTP content-length header does not match body size" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "incomplete-body", + "invalid-length" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nHTTP content-length header does not match body size (label: `incomplete-body`)\n\nInvalid content length (label: `invalid-length`)" + }, + "413": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Asset too large (label: `client-error`)" + } + }, + "summary": "Upload an asset" + } + }, + "/assets/{key_domain}/{key}": { + "delete": { + "description": " [internal route ID: \"assets-delete\"]\n\n**Note**: only local assets can be deleted.", + "operationId": "assets-delete", + "parameters": [ + { + "in": "path", + "name": "key_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Unauthorised operation (label: `unauthorised`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`key_domain` or `key` not found\n\nAsset not found (label: `not-found`)" + } + }, + "summary": "Delete an asset" + }, + "get": { + "description": " [internal route ID: \"assets-download\"]\n\n**Note**: local assets result in a redirect, while remote assets are streamed directly.", + "operationId": "assets-download", + "parameters": [ + { + "in": "path", + "name": "key_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "Asset-Token", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "asset_token", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset returned directly with content type `application/octet-stream`" + }, + "302": { + "description": "Asset found", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`key_domain` or `key` or Asset not found (label: `not-found`)\n\nAsset not found (label: `not-found`)" + } + }, + "summary": "Download an asset" + } + }, + "/assets/{key}/token": { + "delete": { + "description": " [internal route ID: \"tokens-delete\"]\n\n**Note**: deleting the token makes the asset public.", + "operationId": "tokens-delete", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset token deleted" + } + }, + "summary": "Delete an asset token" + }, + "post": { + "description": " [internal route ID: \"tokens-renew\"]\n\n", + "operationId": "tokens-renew", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewAssetToken" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Unauthorised operation (label: `unauthorised`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`key` not found\n\nAsset not found (label: `not-found`)" + } + }, + "summary": "Renew an asset token" + } + }, + "/await": { + "get": { + "description": " [internal route ID: \"await-notifications\"]\n\n", + "externalDocs": { + "description": "RFC 6455", + "url": "https://datatracker.ietf.org/doc/html/rfc6455" + }, + "operationId": "await-notifications", + "parameters": [ + { + "description": "Client ID", + "in": "query", + "name": "client", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "101": { + "description": "Connection upgraded." + }, + "426": { + "description": "Upgrade required." + } + }, + "summary": "Establish websocket connection" + } + }, + "/bot/assets": { + "post": { + "description": " [internal route ID: (\"assets-upload-v3\", bot)]\n\n

Construct the request as multipart/mixed; set header Content-Type: multipart/mixed; boundary=<boundary>.

Use exactly two parts in this order:

  1. application/json metadata (AssetSettings)
  2. application/octet-stream asset bytes

Each part must include Content-Type and Content-Length; the second part may include Content-MD5. Use CRLF between headers and bodies.

When asset audit logging is enabled, the JSON metadata must include:

  • convId: object { id: UUID, domain: String } (qualified conversation ID)
  • filename: String
  • filetype: String MIME type (e.g. image/png, application/pdf)

Optional metadata: public (Bool, default false), retention (one of eternal, persistent, volatile, eternal-infrequent_access, expiring).

For profile pictures or team icons without a conversation, set convId.id to 00000000-0000-0000-0000-000000000000 and convId.domain to the tenant’s domain; use any reasonable filename.

Note: the server treats the asset bytes as application/octet-stream; filetype is used for auditing only.

Example body (boundary=frontier):

Content-Type: multipart/mixed; boundary=frontier

--frontier
Content-Type: application/json
Content-Length: 191

{\"public\":false,\"retention\":\"volatile\",\"convId\":{\"id\":\"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\",\"domain\":\"example.com\"},\"filename\":\"report.pdf\",\"filetype\":\"application/pdf\"}
--frontier
Content-Type: application/octet-stream
Content-Length: 11

Hello Audit
--frontier--
", + "operationId": "assets-upload-v3_bot", + "requestBody": { + "content": { + "multipart/mixed": { + "schema": { + "$ref": "#/components/schemas/AssetSource" + } + } + }, + "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." + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Asset" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Asset" + } + } + }, + "description": "Asset posted", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "incomplete-body", + "message": "HTTP content-length header does not match body size" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "incomplete-body", + "invalid-length" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nHTTP content-length header does not match body size (label: `incomplete-body`)\n\nInvalid content length (label: `invalid-length`)" + }, + "413": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Asset too large (label: `client-error`)" + } + }, + "summary": "Upload an asset" + } + }, + "/bot/assets/{key}": { + "delete": { + "description": " [internal route ID: (\"assets-delete-v3\", bot)]\n\n", + "operationId": "assets-delete-v3_bot", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Unauthorised operation (label: `unauthorised`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`key` not found\n\nAsset not found (label: `not-found`)" + } + }, + "summary": "Delete an asset" + }, + "get": { + "description": " [internal route ID: (\"assets-download-v3\", bot)]\n\n", + "operationId": "assets-download-v3_bot", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "Asset-Token", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "asset_token", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "302": { + "description": "Asset found", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "404": { + "content": { + "application/json": { + "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" + } + }, + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`key` or Asset not found (label: `not-found`)" + } + }, + "summary": "Download an asset" + } + }, + "/bot/client": { + "get": { + "description": " [internal route ID: \"bot-get-client\"]\n\n", + "operationId": "bot-get-client", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Client" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Client" + } + } + }, + "description": "Client found" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "client-not-found", + "message": "Client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "client-not-found", + "message": "Client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Client not found (label: `client-not-found`)\n\nClient not found (label: `client-not-found`)" + } + }, + "summary": "Get client for bot" + } + }, + "/bot/client/prekeys": { + "get": { + "description": " [internal route ID: \"bot-list-prekeys\"]\n\n", + "operationId": "bot-list-prekeys", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "List prekeys for bot" + }, + "post": { + "description": " [internal route ID: \"bot-update-prekeys\"]\n\n", + "operationId": "bot-update-prekeys", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateBotPrekeys" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "client-not-found", + "message": "Client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Client not found (label: `client-not-found`)" + } + }, + "summary": "Update prekeys for bot" + } + }, + "/bot/conversation": { + "get": { + "description": " [internal route ID: \"get-bot-conversation\"]\n\n", + "operationId": "get-bot-conversation", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/BotConvView" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)\n\nYou do not have permission to access this resource (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Team not found (label: `no-team`)\n\nConversation not found (label: `no-conversation`)" + } + } + } + }, + "/bot/conversations/{conv}": { + "post": { + "description": " [internal route ID: \"add-bot\"]\n\n", + "operationId": "add-bot", + "parameters": [ + { + "in": "path", + "name": "conv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AddBot" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddBotResponse" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AddBotResponse" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "service-disabled", + "message": "The desired service is currently disabled." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "service-disabled", + "too-many-members", + "invalid-conversation", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The desired service is currently disabled. (label: `service-disabled`)\n\nMaximum number of members per conversation reached. (label: `too-many-members`)\n\nThe operation is not allowed in this conversation. (label: `invalid-conversation`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Add bot" + } + }, + "/bot/conversations/{conv}/{bot}": { + "delete": { + "description": " [internal route ID: \"remove-bot\"]\n\n", + "operationId": "remove-bot", + "parameters": [ + { + "in": "path", + "name": "conv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "bot", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemoveBotResponse" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RemoveBotResponse" + } + } + }, + "description": "User found" + }, + "204": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-conversation", + "message": "The operation is not allowed in this conversation." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-conversation", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The operation is not allowed in this conversation. (label: `invalid-conversation`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Remove bot" + } + }, + "/bot/messages": { + "post": { + "description": " [internal route ID: \"post-bot-message-unqualified\"]\n\n", + "operationId": "post-bot-message-unqualified", + "parameters": [ + { + "in": "query", + "name": "ignore_missing", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "report_missing", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/new-otr-message" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + } + }, + "description": "Message sent" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "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 old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\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`)" + }, + "404": { + "content": { + "application/json": { + "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" + } + }, + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nConversation not found (label: `no-conversation`)" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + } + }, + "description": "Missing clients" + } + } + } + }, + "/bot/self": { + "delete": { + "description": " [internal route ID: \"bot-delete-self\"]\n\n", + "operationId": "bot-delete-self", + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-bot", + "message": "The targeted user is not a bot." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-bot", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The targeted user is not a bot. (label: `invalid-bot`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Delete self" + }, + "get": { + "description": " [internal route ID: \"bot-get-self\"]\n\n", + "operationId": "bot-get-self", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserProfile" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "User not found (label: `not-found`)" + } + }, + "summary": "Get self" + } + }, + "/bot/users": { + "get": { + "description": " [internal route ID: \"bot-list-users\"]\n\n", + "operationId": "bot-list-users", + "parameters": [ + { + "in": "query", + "name": "ids", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/BotUserView" + }, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "List users" + } + }, + "/bot/users/prekeys": { + "post": { + "description": " [internal route ID: \"bot-claim-users-prekeys\"]\n\n", + "operationId": "bot-claim-users-prekeys", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserClients" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserClientPrekeyMap" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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", + "missing-legalhold-consent-old-clients", + "too-many-clients", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "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\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nToo many clients (label: `too-many-clients`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Claim users prekeys" + } + }, + "/bot/users/{user}/clients": { + "get": { + "description": " [internal route ID: \"bot-get-user-clients\"]\n\n", + "operationId": "bot-get-user-clients", + "parameters": [ + { + "in": "path", + "name": "user", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/PubClient" + }, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "Get user clients" + } + }, + "/broadcast/otr/messages": { + "post": { + "description": " [internal route ID: \"post-otr-broadcast-unqualified\"]\n\nThis 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.", + "operationId": "post-otr-broadcast-unqualified", + "parameters": [ + { + "in": "query", + "name": "ignore_missing", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "report_missing", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/new-otr-message" + } + }, + "application/x-protobuf": { + "schema": { + "$ref": "#/components/schemas/new-otr-message" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + } + }, + "description": "Message sent" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "too-many-users-to-broadcast", + "message": "Too many users to fan out the broadcast event to" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-users-to-broadcast" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "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": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "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 old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\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`)" + }, + "404": { + "content": { + "application/json": { + "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" + } + }, + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + } + }, + "description": "Missing clients" + } + }, + "summary": "Broadcast an encrypted message to all team members and all contacts (accepts JSON or Protobuf)" + } + }, + "/broadcast/proteus/messages": { + "post": { + "description": " [internal route ID: \"post-proteus-broadcast\"]\n\nThis 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.", + "operationId": "post-proteus-broadcast", + "requestBody": { + "content": { + "application/x-protobuf": { + "schema": { + "$ref": "#/components/schemas/QualifiedNewOtrMessage" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus" + } + } + }, + "description": "Message sent" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "too-many-users-to-broadcast", + "message": "Too many users to fan out the broadcast event to" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-users-to-broadcast" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nToo many users to fan out the broadcast event to (label: `too-many-users-to-broadcast`)" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "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 old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\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`)" + }, + "404": { + "content": { + "application/json": { + "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" + } + }, + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus" + } + } + }, + "description": "Missing clients" + } + }, + "summary": "Post an encrypted message to all team members and all contacts (accepts only Protobuf)" + } + }, + "/calls/config/v2": { + "get": { + "description": " [internal route ID: \"get-calls-config-v2\"]\n\n", + "operationId": "get-calls-config-v2", + "parameters": [ + { + "description": "Limit resulting list. Allowed values [1..10]", + "in": "query", + "name": "limit", + "required": false, + "schema": { + "maximum": 10, + "minimum": 1, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RTCConfiguration" + } + } + }, + "description": "" + } + }, + "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": { + "description": " [internal route ID: \"list-clients\"]\n\n", + "operationId": "list-clients", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Client" + }, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/Client" + }, + "type": "array" + } + } + }, + "description": "List of clients" + } + }, + "summary": "List the registered clients" + }, + "post": { + "description": " [internal route ID: \"add-client\"]\n\n", + "operationId": "add-client", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewClient" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Client" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Client" + } + } + }, + "description": "Client registered", + "headers": { + "Location": { + "description": "Client ID", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "bad-request", + "message": "Malformed prekeys uploaded" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "bad-request" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nMalformed prekeys uploaded (label: `bad-request`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)" + } + }, + "summary": "Register a new client" + } + }, + "/clients/{cid}/access-token": { + "post": { + "description": " [internal route ID: \"create-access-token\"]\n\nCreate 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.", + "operationId": "create-access-token", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "cid", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "DPoP", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DPoPAccessTokenResponse" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DPoPAccessTokenResponse" + } + } + }, + "description": "Access token created", + "headers": { + "Cache-Control": { + "schema": { + "type": "string" + } + } + } + } + }, + "summary": "Create a JWT DPoP access token" + } + }, + "/clients/{client}": { + "delete": { + "description": " [internal route ID: \"delete-client\"]\n\n", + "operationId": "delete-client", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeleteClient" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Client deleted" + } + }, + "summary": "Delete an existing client" + }, + "get": { + "description": " [internal route ID: \"get-client\"]\n\n", + "operationId": "get-client", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Client" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Client" + } + } + }, + "description": "Client found" + }, + "404": { + "description": "`client` or Client not found(**Note**: This error has an empty body for legacy reasons)" + } + }, + "summary": "Get a registered client by ID" + }, + "put": { + "description": " [internal route ID: \"update-client\"]\n\n", + "operationId": "update-client", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateClient" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Client updated" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "bad-request", + "message": "Malformed prekeys uploaded" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "bad-request" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nMalformed prekeys uploaded (label: `bad-request`)" + } + }, + "summary": "Update a registered client" + } + }, + "/clients/{client}/capabilities": { + "get": { + "description": " [internal route ID: \"get-client-capabilities\"]\n\n", + "operationId": "get-client-capabilities", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientCapabilityList" + } + } + }, + "description": "" + } + }, + "summary": "Read back what the client has been posting about itself" + } + }, + "/clients/{client}/nonce": { + "get": { + "description": " [internal route ID: \"get-nonce\"]\n\nGet a new nonce for a client CSR, specified in the response header `Replay-Nonce` as a uuidv4 in base64url encoding.", + "operationId": "get-nonce", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content", + "headers": { + "Cache-Control": { + "schema": { + "type": "string" + } + }, + "Replay-Nonce": { + "schema": { + "type": "string" + } + } + } + } + }, + "summary": "Get a new nonce for a client CSR" + }, + "head": { + "description": " [internal route ID: \"head-nonce\"]\n\nGet a new nonce for a client CSR, specified in the response header `Replay-Nonce` as a uuidv4 in base64url encoding.", + "operationId": "head-nonce", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "No Content", + "headers": { + "Cache-Control": { + "schema": { + "type": "string" + } + }, + "Replay-Nonce": { + "schema": { + "type": "string" + } + } + } + } + }, + "summary": "Get a new nonce for a client CSR" + } + }, + "/clients/{client}/prekeys": { + "get": { + "description": " [internal route ID: \"get-client-prekeys\"]\n\n", + "operationId": "get-client-prekeys", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "summary": "List the remaining prekey IDs of a client" + } + }, + "/connections/{uid_domain}/{uid}": { + "get": { + "description": " [internal route ID: \"get-connection\"]\n\n", + "operationId": "get-connection", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserConnection" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserConnection" + } + } + }, + "description": "Connection found" + }, + "404": { + "description": "`uid_domain` or `uid` or 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": " [internal route ID: \"create-connection\"]\n\nYou can have no more than 1000 connections in accepted or sent state", + "operationId": "create-connection", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserConnection" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserConnection" + } + } + }, + "description": "Connection existed" + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserConnection" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserConnection" + } + } + }, + "description": "Connection was created" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-user", + "message": "Invalid user" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-user" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid user (label: `invalid-user`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-identity", + "message": "The user has no verified email" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-identity", + "connection-limit", + "missing-legalhold-consent", + "missing-legalhold-consent-old-clients" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The user has no verified email (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`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)" + } + }, + "summary": "Create a connection to another user" + }, + "put": { + "description": " [internal route ID: \"update-connection\"]\n\n", + "operationId": "update-connection", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConnectionUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserConnection" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserConnection" + } + } + }, + "description": "Connection updated" + }, + "204": { + "description": "Connection unchanged" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-user", + "message": "Invalid user" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-user" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid user (label: `invalid-user`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-identity", + "message": "The user has no verified email" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-identity", + "bad-conn-update", + "not-connected", + "connection-limit", + "missing-legalhold-consent", + "missing-legalhold-consent-old-clients" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The user has no verified email (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`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)" + } + }, + "summary": "Update a connection to another user" + } + }, + "/conversations": { + "post": { + "description": " [internal route ID: \"create-group-conversation\"]\n\nThis returns 201 when a new conversation is created, and 200 when the conversation already existed", + "operationId": "create-group-conversation", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewConv" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateGroupConversation" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateGroupConversation" + } + } + }, + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled", + "non-empty-member-list" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "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": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "channels-not-enabled", + "message": "The channels feature is not enabled for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "channels-not-enabled", + "not-mls-conversation", + "missing-legalhold-consent", + "operation-denied", + "no-team-member", + "not-connected", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The channels feature is not enabled for this team (label: `channels-not-enabled`)\n\nThis operation requires an MLS conversation (label: `not-mls-conversation`)\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`)\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`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "non_federating_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "non_federating_backends" + ], + "type": "object" + } + } + }, + "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Create a new conversation" + } + }, + "/conversations/code-check": { + "post": { + "description": " [internal route ID: \"code-check\"]\n\nIf 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.", + "operationId": "code-check", + "parameters": [ + { + "in": "header", + "name": "X-Forwarded-For", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationCode" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Valid" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-conversation-password", + "message": "Invalid conversation password" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-conversation-password" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid conversation password (label: `invalid-conversation-password`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)" + } + }, + "summary": "Check validity of a conversation code." + } + }, + "/conversations/join": { + "get": { + "description": " [internal route ID: \"get-conversation-by-reusable-code\"]\n\n", + "operationId": "get-conversation-by-reusable-code", + "parameters": [ + { + "in": "query", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "code", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationCoverView" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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", + "invalid-conversation-password" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nInvalid conversation password (label: `invalid-conversation-password`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)" + } + }, + "summary": "Get limited conversation information by key/code pair" + }, + "post": { + "description": " [internal route ID: \"join-conversation-by-code-unqualified\"]\n\nIf 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.", + "operationId": "join-conversation-by-code-unqualified", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/JoinConversationByCode" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Conversation joined" + }, + "204": { + "description": "Conversation unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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", + "invalid-conversation-password" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "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`)\n\nInvalid conversation password (label: `invalid-conversation-password`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)" + } + }, + "summary": "Join a conversation using a reusable code" + } + }, + "/conversations/list": { + "post": { + "description": " [internal route ID: \"list-conversations\"]\n\n", + "operationId": "list-conversations", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ListConversations" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationsResponse" + } + } + }, + "description": "" + } + }, + "summary": "Get conversation metadata for a list of conversation ids" + } + }, + "/conversations/list-ids": { + "post": { + "description": " [internal route ID: \"list-conversation-ids\"]\n\nThe 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.", + "operationId": "list-conversation-ids", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GetPaginated_ConversationIds" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationIds_Page" + } + } + }, + "description": "" + } + }, + "summary": "Get all conversation IDs." + } + }, + "/conversations/mls-self": { + "get": { + "description": " [internal route ID: \"get-mls-self-conversation\"]\n\n", + "operationId": "get-mls-self-conversation", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OwnConversationV9" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OwnConversationV9" + } + } + }, + "description": "The MLS self-conversation" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + } + }, + "summary": "Get the user's MLS self-conversation" + } + }, + "/conversations/self": { + "post": { + "description": " [internal route ID: \"create-self-conversation\"]\n\n", + "operationId": "create-self-conversation", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OwnConversationV6" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OwnConversationV6" + } + } + }, + "description": "Conversation existed", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OwnConversationV6" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OwnConversationV6" + } + } + }, + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + } + }, + "summary": "Create a self-conversation" + } + }, + "/conversations/{cnv_domain}/{cnv}": { + "get": { + "description": " [internal route ID: \"get-conversation\"]\n\n", + "operationId": "get-conversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Conversation" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get a conversation by ID" + } + }, + "/conversations/{cnv_domain}/{cnv}/access": { + "put": { + "description": " [internal route ID: \"update-conversation-access\"]\n\n", + "operationId": "update-conversation-access", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationAccessData" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Access updated" + }, + "204": { + "description": "Access unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update access modes for a conversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/add-permission": { + "put": { + "description": " [internal route ID: \"update-channel-add-permission\"]\n\n", + "operationId": "update-channel-add-permission", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AddPermissionUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Add permissions updated" + }, + "204": { + "description": "Add permissions unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid target access" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "not-connected", + "operation-denied", + "no-team-member", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid target access (label: `invalid-op`)\n\nUsers are not connected (label: `not-connected`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing modify_add_permissions) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nTeam not found (label: `no-team`)\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "non_federating_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "non_federating_backends" + ], + "type": "object" + } + } + }, + "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Update the permissions for adding members to a channel" + } + }, + "/conversations/{cnv_domain}/{cnv}/groupinfo": { + "get": { + "description": " [internal route ID: \"get-group-info\"]\n\n", + "operationId": "get-group-info", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "message/mls": { + "schema": { + "$ref": "#/components/schemas/GroupInfoData" + } + } + }, + "description": "The group information" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "mls-missing-group-info", + "message": "The conversation has no group information" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-missing-group-info", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nThe conversation has no group information (label: `mls-missing-group-info`)\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get MLS group information" + } + }, + "/conversations/{cnv_domain}/{cnv}/members": { + "post": { + "description": " [internal route ID: \"add-members-to-conversation\"]\n\n", + "operationId": "add-members-to-conversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/InviteQualified" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Conversation updated" + }, + "204": { + "description": "Conversation unchanged" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-group-id-not-supported", + "message": "The group ID version of the conversation is not supported by one of the federated backends" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-group-id-not-supported" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nThe group ID version of the conversation is not supported by one of the federated backends (label: `mls-group-id-not-supported`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "non_federating_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "non_federating_backends" + ], + "type": "object" + } + } + }, + "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Add qualified members to an existing conversation." + }, + "put": { + "description": " [internal route ID: \"replace-members-in-conversation\"]\n\nThis will add any members not already in the conversation, and remove any members not in the provided list except users that are associated via a user group. The given role in the request body will be applied to all added members. The roles of already existing members will not be changed even if these members are included in the request body and their role differs from the role provided in this request.", + "operationId": "replace-members-in-conversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/InviteQualified" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Conversation members replaced" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-group-id-not-supported", + "message": "The group ID version of the conversation is not supported by one of the federated backends" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-group-id-not-supported" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nThe group ID version of the conversation is not supported by one of the federated backends (label: `mls-group-id-not-supported`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "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 remove_conversation_member) (label: `action-denied`)\n\nInsufficient authorization (missing add_conversation_member) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "non_federating_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "non_federating_backends" + ], + "type": "object" + } + } + }, + "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Replace the members of a conversation." + } + }, + "/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}": { + "delete": { + "description": " [internal route ID: \"remove-member\"]\n\n", + "operationId": "remove-member", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "usr_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Target User ID", + "in": "path", + "name": "usr", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Member removed" + }, + "204": { + "description": "No change" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`cnv_domain` or `cnv` or `usr_domain` or `usr` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Remove a member from a conversation" + }, + "put": { + "description": " [internal route ID: \"update-other-member\"]\n\n**Note**: at least one field has to be provided.", + "operationId": "update-other-member", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "usr_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Target User ID", + "in": "path", + "name": "usr", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OtherMemberUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Membership updated" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nInvalid target (label: `invalid-op`)\n\nInsufficient authorization (missing modify_other_conversation_member) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`cnv_domain` or `cnv` or `usr_domain` or `usr` not found\n\nConversation member not found (label: `no-conversation-member`)\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update membership of the specified user" + } + }, + "/conversations/{cnv_domain}/{cnv}/message-timer": { + "put": { + "description": " [internal route ID: \"update-conversation-message-timer\"]\n\n", + "operationId": "update-conversation-message-timer", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationMessageTimerUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Message timer updated" + }, + "204": { + "description": "Message timer unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_message_timer) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update the message timer for a conversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/name": { + "put": { + "description": " [internal route ID: \"update-conversation-name\"]\n\n", + "operationId": "update-conversation-name", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationRename" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Name unchanged" + }, + "204": { + "description": "Name updated" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing modify_conversation_name) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update conversation name" + } + }, + "/conversations/{cnv_domain}/{cnv}/proteus/messages": { + "post": { + "description": " [internal route ID: \"post-proteus-message\"]\n\nThis 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.", + "operationId": "post-proteus-message", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/x-protobuf": { + "schema": { + "$ref": "#/components/schemas/QualifiedNewOtrMessage" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus" + } + } + }, + "description": "Message sent" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "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 old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\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`)" + }, + "404": { + "content": { + "application/json": { + "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" + } + }, + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`cnv_domain` or `cnv` or Conversation not found (label: `no-conversation`)" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus" + } + } + }, + "description": "Missing clients" + } + }, + "summary": "Post an encrypted message to a conversation (accepts only Protobuf)" + } + }, + "/conversations/{cnv_domain}/{cnv}/protocol": { + "put": { + "description": " [internal route ID: \"update-conversation-protocol\"]\n\n**Note**: Only proteus->mixed upgrade is supported.", + "operationId": "update-conversation-protocol", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ProtocolUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Conversation updated" + }, + "204": { + "description": "Conversation unchanged" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-migration-criteria-not-satisfied", + "message": "The migration criteria for mixed to MLS protocol transition are not satisfied for this conversation" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-migration-criteria-not-satisfied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nThe migration criteria for mixed to MLS protocol transition are not satisfied for this conversation (label: `mls-migration-criteria-not-satisfied`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member", + "invalid-op", + "action-denied", + "invalid-protocol-transition" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing leave_conversation) (label: `action-denied`)\n\nProtocol transition is invalid (label: `invalid-protocol-transition`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nTeam not found (label: `no-team`)\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update the protocol of the conversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/receipt-mode": { + "put": { + "description": " [internal route ID: \"update-conversation-receipt-mode\"]\n\n", + "operationId": "update-conversation-receipt-mode", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationReceiptModeUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Receipt mode updated" + }, + "204": { + "description": "Receipt mode unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "mls-receipts-not-allowed", + "message": "Read receipts on MLS conversations are not allowed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-receipts-not-allowed", + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Read receipts on MLS conversations are not allowed (label: `mls-receipts-not-allowed`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_receipt_mode) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update receipt mode for a conversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/self": { + "get": { + "description": " [internal route ID: \"get-conversation-self\"]\n\n", + "operationId": "get-conversation-self", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Member" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get self membership properties" + }, + "put": { + "description": " [internal route ID: \"update-conversation-self\"]\n\n**Note**: at least one field has to be provided.", + "operationId": "update-conversation-self", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MemberUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Update successful" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update self membership properties" + } + }, + "/conversations/{cnv_domain}/{cnv}/subconversations/{subconv}": { + "delete": { + "description": " [internal route ID: \"delete-subconversation\"]\n\n", + "operationId": "delete-subconversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "subconv", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSReset" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "Deletion successful" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "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`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`cnv_domain` or `cnv` or `subconv` not found\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "mls-stale-message", + "message": "The conversation epoch in a message is too old" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-stale-message" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The conversation epoch in a message is too old (label: `mls-stale-message`)" + } + }, + "summary": "Delete an MLS subconversation" + }, + "get": { + "description": " [internal route ID: \"get-subconversation\"]\n\n", + "operationId": "get-subconversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "subconv", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicSubConversation" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PublicSubConversation" + } + } + }, + "description": "Subconversation" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "mls-subconv-unsupported-convtype", + "message": "MLS subconversations are only supported for regular conversations" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-subconv-unsupported-convtype", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS subconversations are only supported for regular conversations (label: `mls-subconv-unsupported-convtype`)\n\nConversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`cnv_domain` or `cnv` or `subconv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get information about an MLS subconversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/subconversations/{subconv}/groupinfo": { + "get": { + "description": " [internal route ID: \"get-subconversation-group-info\"]\n\n", + "operationId": "get-subconversation-group-info", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "subconv", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "message/mls": { + "schema": { + "$ref": "#/components/schemas/GroupInfoData" + } + } + }, + "description": "The group information" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "mls-missing-group-info", + "message": "The conversation has no group information" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-missing-group-info", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` or `subconv` not found\n\nThe conversation has no group information (label: `mls-missing-group-info`)\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get MLS group information of subconversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/subconversations/{subconv}/self": { + "delete": { + "description": " [internal route ID: \"leave-subconversation\"]\n\n", + "operationId": "leave-subconversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "subconv", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled", + "mls-protocol-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nMLS protocol error (label: `mls-protocol-error`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`cnv_domain` or `cnv` or `subconv` not found\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "mls-stale-message", + "message": "The conversation epoch in a message is too old" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-stale-message" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The conversation epoch in a message is too old (label: `mls-stale-message`)" + } + }, + "summary": "Leave an MLS subconversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/typing": { + "post": { + "description": " [internal route ID: \"member-typing-qualified\"]\n\n", + "operationId": "member-typing-qualified", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TypingData" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Notification sent" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Sending typing notifications" + } + }, + "/conversations/{cnv}/code": { + "delete": { + "description": " [internal route ID: \"remove-code-unqualified\"]\n\n", + "operationId": "remove-code-unqualified", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Conversation code deleted." + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Delete conversation code" + }, + "get": { + "description": " [internal route ID: \"get-code\"]\n\n", + "operationId": "get-code", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationCodeInfo" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationCodeInfo" + } + } + }, + "description": "Conversation Code" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)" + } + }, + "summary": "Get existing conversation code" + }, + "post": { + "description": " [internal route ID: \"create-conversation-code-unqualified\"]\n\n\nOAuth scope: `write:conversations_code`", + "operationId": "create-conversation-code-unqualified", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateConversationCodeRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationCodeInfo" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationCodeInfo" + } + } + }, + "description": "Conversation code already exists." + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Conversation code created." + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "create-conv-code-conflict", + "message": "Conversation code already exists with a different password setting than the requested one." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "create-conv-code-conflict", + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation code already exists with a different password setting than the requested one. (label: `create-conv-code-conflict`)\n\nThe guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)" + } + }, + "summary": "Create or recreate a conversation code" + } + }, + "/conversations/{cnv}/features/conversationGuestLinks": { + "get": { + "description": " [internal route ID: \"get-conversation-guest-links-status\"]\n\n", + "operationId": "get-conversation-guest-links-status", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GuestLinksConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get the status of the guest links feature for a conversation that potentially has been created by someone from another team." + } + }, + "/conversations/{cnv}/otr/messages": { + "post": { + "description": " [internal route ID: \"post-otr-message-unqualified\"]\n\nThis 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.", + "operationId": "post-otr-message-unqualified", + "parameters": [ + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "ignore_missing", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "report_missing", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/new-otr-message" + } + }, + "application/x-protobuf": { + "schema": { + "$ref": "#/components/schemas/new-otr-message" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + } + }, + "description": "Message sent" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "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 old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\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`)" + }, + "404": { + "content": { + "application/json": { + "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" + } + }, + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`cnv` or Conversation not found (label: `no-conversation`)" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + } + }, + "description": "Missing clients" + } + }, + "summary": "Post an encrypted message to a conversation (accepts JSON or Protobuf)" + } + }, + "/conversations/{cnv}/roles": { + "get": { + "description": " [internal route ID: \"get-conversation-roles\"]\n\n", + "operationId": "get-conversation-roles", + "parameters": [ + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationRolesList" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get existing roles available for the given conversation" + } + }, + "/cookies": { + "get": { + "description": " [internal route ID: \"list-cookies\"]\n\n", + "operationId": "list-cookies", + "parameters": [ + { + "description": "Filter by label (comma-separated list)", + "in": "query", + "name": "labels", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CookieList" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CookieList" + } + } + }, + "description": "List of cookies" + } + }, + "summary": "Retrieve the list of cookies currently stored for the user" + } + }, + "/cookies/remove": { + "post": { + "description": " [internal route ID: \"remove-cookies\"]\n\n", + "operationId": "remove-cookies", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RemoveCookies" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Cookies revoked" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)" + } + }, + "summary": "Revoke stored cookies" + } + }, + "/custom-backend/by-domain/{domain}": { + "get": { + "description": " [internal route ID: \"get-custom-backend-by-domain\"]\n\n", + "operationId": "get-custom-backend-by-domain", + "parameters": [ + { + "description": "URL-encoded email domain", + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CustomBackend" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`domain` not found\n\nCustom backend not found (label: `custom-backend-not-found`)" + } + }, + "summary": "Shows information about custom backends related to a given email domain" + } + }, + "/delete": { + "post": { + "description": " [internal route ID: \"verify-delete\"]\n\n", + "operationId": "verify-delete", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/VerifyDeleteUser" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Deletion is initiated." + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Invalid verification code (label: `invalid-code`)" + } + }, + "summary": "Verify account deletion with a code." + } + }, + "/domain-verification/{domain}/authorize-team": { + "post": { + "description": " [internal route ID: \"domain-verification-authorize-team\"]\n\n", + "operationId": "domain-verification-authorize-team", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DomainOwnershipToken" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Authorized" + }, + "401": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 401, + "label": "domain-registration-update-auth-failure", + "message": "Domain registration updated auth failure" + }, + "properties": { + "code": { + "enum": [ + 401 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-auth-failure" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated auth failure (label: `domain-registration-update-auth-failure`)" + }, + "402": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 402, + "label": "domain-registration-update-payment-required", + "message": "Domain registration updated payment required" + }, + "properties": { + "code": { + "enum": [ + 402 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-payment-required" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated payment required (label: `domain-registration-update-payment-required`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-forbidden-for-domain-registration-state", + "message": "Invalid domain registration state update" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-forbidden-for-domain-registration-state" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid domain registration state update (label: `operation-forbidden-for-domain-registration-state`)" + } + }, + "summary": "Authorize a team to operate on a verified domain" + } + }, + "/domain-verification/{domain}/backend": { + "post": { + "description": " [internal route ID: \"update-domain-redirect\"]\n\n", + "operationId": "update-domain-redirect", + "parameters": [ + { + "in": "header", + "name": "Authorization", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DomainRedirectConfig" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Updated" + }, + "401": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 401, + "label": "domain-registration-update-auth-failure", + "message": "Domain registration updated auth failure" + }, + "properties": { + "code": { + "enum": [ + 401 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-auth-failure" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated auth failure (label: `domain-registration-update-auth-failure`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-forbidden-for-domain-registration-state", + "message": "Invalid domain registration state update" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-forbidden-for-domain-registration-state" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid domain registration state update (label: `operation-forbidden-for-domain-registration-state`)" + } + }, + "summary": "Update the domain redirect configuration" + } + }, + "/domain-verification/{domain}/challenges": { + "post": { + "description": " [internal route ID: \"domain-verification-challenge\"]\n\n", + "operationId": "domain-verification-challenge", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DomainVerificationChallenge" + } + } + }, + "description": "" + } + }, + "summary": "Get a DNS verification challenge" + } + }, + "/domain-verification/{domain}/challenges/{challengeId}": { + "post": { + "description": " [internal route ID: \"verify-challenge\"]\n\n", + "operationId": "verify-challenge", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "challengeId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ChallengeToken" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DomainOwnershipToken" + } + } + }, + "description": "" + }, + "401": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 401, + "label": "domain-registration-update-auth-failure", + "message": "Domain registration updated auth failure" + }, + "properties": { + "code": { + "enum": [ + 401 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-auth-failure" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated auth failure (label: `domain-registration-update-auth-failure`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "domain-verification-failed", + "message": "Domain verification failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-verification-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain verification failed (label: `domain-verification-failed`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "challenge-not-found", + "message": "Challenge not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "challenge-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`domain` or `challengeId` not found\n\nChallenge not found (label: `challenge-not-found`)" + } + }, + "summary": "Verify a DNS verification challenge" + } + }, + "/domain-verification/{domain}/team": { + "post": { + "description": " [internal route ID: \"update-team-invite\"]\n\n", + "operationId": "update-team-invite", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamInviteConfig" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Updated" + }, + "402": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 402, + "label": "domain-registration-update-payment-required", + "message": "Domain registration updated payment required" + }, + "properties": { + "code": { + "enum": [ + 402 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-payment-required" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated payment required (label: `domain-registration-update-payment-required`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-forbidden-for-domain-registration-state", + "message": "Invalid domain registration state update" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-forbidden-for-domain-registration-state" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid domain registration state update (label: `operation-forbidden-for-domain-registration-state`)" + } + }, + "summary": "Update the team-invite configuration" + } + }, + "/domain-verification/{domain}/team/challenges/{challengeId}": { + "post": { + "description": " [internal route ID: \"verify-challenge-team\"]\n\n", + "operationId": "verify-challenge-team", + "parameters": [ + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "challengeId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ChallengeToken" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DomainOwnershipToken" + } + } + }, + "description": "" + }, + "401": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 401, + "label": "domain-registration-update-auth-failure", + "message": "Domain registration updated auth failure" + }, + "properties": { + "code": { + "enum": [ + 401 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-auth-failure" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated auth failure (label: `domain-registration-update-auth-failure`)" + }, + "402": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 402, + "label": "domain-registration-update-payment-required", + "message": "Domain registration updated payment required" + }, + "properties": { + "code": { + "enum": [ + 402 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-payment-required" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated payment required (label: `domain-registration-update-payment-required`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-forbidden-for-domain-registration-state", + "message": "Invalid domain registration state update" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-forbidden-for-domain-registration-state" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid domain registration state update (label: `operation-forbidden-for-domain-registration-state`)" + } + }, + "summary": "Verify a DNS verification challenge for a team" + } + }, + "/events": { + "get": { + "description": " [internal route ID: \"consume-events\"]\n\nThis is the rabbitMQ-based variant of \"await-notifications\"", + "externalDocs": { + "description": "RFC 6455", + "url": "https://datatracker.ietf.org/doc/html/rfc6455" + }, + "operationId": "consume-events", + "parameters": [ + { + "description": "Client ID", + "in": "query", + "name": "client", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Synchronization marker ID", + "in": "query", + "name": "sync_marker", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "101": { + "description": "Connection upgraded." + }, + "426": { + "description": "Upgrade required." + } + }, + "summary": "Consume events over a websocket connection" + } + }, + "/feature-configs": { + "get": { + "description": " [internal route ID: \"get-all-feature-configs-for-user\"]\n\nGets 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).\nOAuth scope: `read:feature_configs`", + "operationId": "get-all-feature-configs-for-user", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AllTeamFeatures" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Team not found (label: `no-team`)" + } + }, + "summary": "Gets feature configs for a user" + } + }, + "/get-domain-registration": { + "post": { + "description": " [internal route ID: \"get-domain-registration\"]\n\n- `due_to_existing_account`: boolean (optional, only present if `domain_redirect` is `no-registration`)\n- `backend`: object (optional, must be present if `domain_redirect` is `backend`)\n - `config_url`: string (required)\n - `webapp_url`: string (optional)\n- `sso_code`: string (optional, must be present if `domain_redirect` is `sso`)", + "operationId": "get-domain-registration", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GetDomainRegistrationRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DomainRedirectResponseV10" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-domain", + "message": "Invalid domain" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-domain" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid domain (label: `invalid-domain`)" + } + }, + "summary": "Get domain registration configuration by email" + } + }, + "/handles": { + "post": { + "description": " [internal route ID: \"check-user-handles\"]\n\n", + "operationId": "check-user-handles", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CheckHandles" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Handle" + }, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/Handle" + }, + "type": "array" + } + } + }, + "description": "List of free handles" + } + }, + "summary": "Check availability of user handles" + } + }, + "/handles/{handle}": { + "head": { + "description": " [internal route ID: \"check-user-handle\"]\n\n", + "operationId": "check-user-handle", + "parameters": [ + { + "in": "path", + "name": "handle", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "Handle is taken" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-handle", + "message": "The given handle is invalid (less than 2 or more than 256 characters; chars not in \"a-z0-9_.-\"; or on the blocklist)" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-handle" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given handle is invalid (less than 2 or more than 256 characters; chars not in \"a-z0-9_.-\"; or on the blocklist) (label: `invalid-handle`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`handle` not found\n\nHandle not found (label: `not-found`)" + } + }, + "summary": "Check whether a user handle can be taken" + } + }, + "/identity-providers": { + "get": { + "description": " [internal route ID: \"idp-get-all\"]\n\n", + "operationId": "idp-get-all", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPList" + } + } + }, + "description": "" + } + } + }, + "post": { + "description": " [internal route ID: \"idp-create\"]\n\n", + "operationId": "idp-create", + "parameters": [ + { + "in": "query", + "name": "replaces", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "api_version", + "required": false, + "schema": { + "default": "v2", + "enum": [ + "v1", + "v2" + ], + "type": "string" + } + }, + { + "in": "query", + "name": "handle", + "required": false, + "schema": { + "maxLength": 32, + "minLength": 1, + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPMetadataInfo" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/IdPMetadataInfo" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPConfig_WireIdP" + } + } + }, + "description": "" + } + } + } + }, + "/identity-providers/{id}": { + "delete": { + "description": " [internal route ID: \"idp-delete\"]\n\n", + "operationId": "idp-delete", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "purge", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "204": { + "description": "" + } + } + }, + "get": { + "description": " [internal route ID: \"idp-get\"]\n\n", + "operationId": "idp-get", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPConfig_WireIdP" + } + } + }, + "description": "" + } + } + }, + "put": { + "description": " [internal route ID: \"idp-update\"]\n\n", + "operationId": "idp-update", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "handle", + "required": false, + "schema": { + "maxLength": 32, + "minLength": 1, + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPMetadataInfo" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/IdPMetadataInfo" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPConfig_WireIdP" + } + } + }, + "description": "" + } + } + } + }, + "/identity-providers/{id}/raw": { + "get": { + "description": " [internal route ID: \"idp-get-raw\"]\n\n", + "operationId": "idp-get-raw", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/list-connections": { + "post": { + "description": " [internal route ID: \"list-connections\"]\n\nThe 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.", + "operationId": "list-connections", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GetPaginated_Connections" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Connections_Page" + } + } + }, + "description": "" + } + }, + "summary": "List the connections to other users, including remote users" + } + }, + "/list-users": { + "post": { + "description": " [internal route ID: \"list-users-by-ids-or-handles\"]\n\nThe 'qualified_ids' and 'qualified_handles' parameters are mutually exclusive.", + "operationId": "list-users-by-ids-or-handles", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ListUsersQuery" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ListUsersById" + } + } + }, + "description": "" + } + }, + "summary": "List users" + } + }, + "/login": { + "post": { + "description": " [internal route ID: \"login\"]\n\nLogins are throttled at the server's discretion", + "operationId": "login", + "parameters": [ + { + "description": "Request a persistent cookie instead of a session cookie", + "in": "query", + "name": "persist", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Login" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessToken" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AccessToken" + } + } + }, + "description": "OK", + "headers": { + "Set-Cookie": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)" + } + }, + "summary": "Authenticate a user to obtain a cookie and first access token" + } + }, + "/mls/commit-bundles": { + "post": { + "description": " [internal route ID: \"mls-commit-bundle\"]\n\n\n\n**Note**: this endpoint can execute proposals, and therefore return all possible errors associated with adding or removing members to a conversation, in addition to the ones listed below. See the documentation of [POST /conversations/{cnv}/members/v2](#/default/post_conversations__cnv__members_v2) and [POST /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}](#/default/delete_conversations__cnv_domain___cnv__members__usr_domain___usr_) for more details on the possible error responses of each type of proposal.", + "operationId": "mls-commit-bundle", + "requestBody": { + "content": { + "message/mls": { + "schema": { + "$ref": "#/components/schemas/CommitBundle" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MLSMessageSendingStatus" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSMessageSendingStatus" + } + } + }, + "description": "Commit accepted and forwarded" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-invalid-leaf-node-signature", + "message": "Invalid leaf node signature" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-invalid-leaf-node-signature", + "mls-group-id-not-supported", + "mls-welcome-mismatch", + "mls-self-removal-not-allowed", + "mls-protocol-error", + "mls-not-enabled", + "mls-invalid-leaf-node-index", + "mls-group-conversation-mismatch", + "mls-commit-missing-references", + "mls-client-sender-user-mismatch" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid leaf node signature (label: `mls-invalid-leaf-node-signature`)\n\nThe group ID version of the conversation is not supported by one of the federated backends (label: `mls-group-id-not-supported`)\n\nSubmitted group info is inconsistent with the backend group state\n\nThe list of targets of a welcome message does not match the list of new clients in a group (label: `mls-welcome-mismatch`)\n\nSelf removal from group is not allowed (label: `mls-self-removal-not-allowed`)\n\nMLS protocol error (label: `mls-protocol-error`)\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nA referenced leaf node index points to a blank or non-existing node (label: `mls-invalid-leaf-node-index`)\n\nConversation ID resolved from Group ID does not match submitted Conversation ID (label: `mls-group-conversation-mismatch`)\n\nThe commit is not referencing all pending proposals (label: `mls-commit-missing-references`)\n\nUser ID resolved from Client ID does not match message's sender user ID (label: `mls-client-sender-user-mismatch`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "mls-identity-mismatch", + "message": "Leaf node signature key does not match the client's key" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-identity-mismatch", + "mls-subconv-join-parent-missing", + "missing-legalhold-consent", + "legalhold-not-enabled", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Leaf node signature key does not match the client's key (label: `mls-identity-mismatch`)\n\nMLS client cannot join the subconversation because it is not member of the parent conversation (label: `mls-subconv-join-parent-missing`)\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`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nConversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "mls-proposal-not-found", + "message": "A proposal referenced in a commit message could not be found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-proposal-not-found", + "no-conversation", + "no-conversation-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "A proposal referenced in a commit message could not be found (label: `mls-proposal-not-found`)\n\nConversation not found (label: `no-conversation`)\n\nConversation member not found (label: `no-conversation-member`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "missing_users": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + } + }, + "required": [ + "missing_users" + ], + "type": "object" + } + } + }, + "description": "Group is out of sync\n\nAdding members to the conversation is not possible because the backends involved do not form a fully connected graph\n\nA user who is under legal-hold may not participate in MLS conversations (label: `mls-legal-hold-not-allowed`)\n\nThe conversation epoch in a message is too old (label: `mls-stale-message`)\n\nA proposal of type Add or Remove does not apply to the full list of clients for a user (label: `mls-client-mismatch`)" + }, + "422": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 422, + "label": "mls-unsupported-proposal", + "message": "Unsupported proposal type" + }, + "properties": { + "code": { + "enum": [ + 422 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-unsupported-proposal", + "mls-unsupported-message" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unsupported proposal type (label: `mls-unsupported-proposal`)\n\nAttempted to send a message with an unsupported combination of content type and wire format (label: `mls-unsupported-message`)" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Post a MLS CommitBundle" + } + }, + "/mls/key-packages/claim/{user_domain}/{user}": { + "post": { + "description": " [internal route ID: \"mls-key-packages-claim\"]\n\nOnly key packages for the specified ciphersuite are claimed.", + "operationId": "mls-key-packages-claim", + "parameters": [ + { + "in": "path", + "name": "user_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "user", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Ciphersuite in hex format (e.g. 0xf031)", + "in": "query", + "name": "ciphersuite", + "required": true, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyPackageBundle" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/KeyPackageBundle" + } + } + }, + "description": "Claimed key packages" + } + }, + "summary": "Claim one key package for each client of the given user" + } + }, + "/mls/key-packages/self/{client}": { + "delete": { + "description": " [internal route ID: \"mls-key-packages-delete\"]\n\n", + "operationId": "mls-key-packages-delete", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Ciphersuite in hex format (e.g. 0xf031)", + "in": "query", + "name": "ciphersuite", + "required": true, + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeleteKeyPackages" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "OK" + } + }, + "summary": "Delete all key packages for a given ciphersuite and client" + }, + "post": { + "description": " [internal route ID: \"mls-key-packages-upload\"]\n\nThe request body should be a json object containing a list of base64-encoded key packages.", + "operationId": "mls-key-packages-upload", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/KeyPackageUpload" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Key packages uploaded" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-protocol-error", + "message": "MLS protocol error" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-protocol-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nMLS protocol error (label: `mls-protocol-error`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Key package credential does not match qualified client ID (label: `mls-identity-mismatch`)" + } + }, + "summary": "Upload a fresh batch of key packages" + }, + "put": { + "description": " [internal route ID: \"mls-key-packages-replace\"]\n\nThe request body should be a json object containing a list of base64-encoded key packages. Use this sparingly.", + "operationId": "mls-key-packages-replace", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Comma-separated list of ciphersuites in hex format (e.g. 0xf031)", + "in": "query", + "name": "ciphersuites", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/KeyPackageUpload" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Key packages replaced" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-protocol-error", + "message": "MLS protocol error" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-protocol-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body` or `ciphersuites`\n\nMLS protocol error (label: `mls-protocol-error`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Key package credential does not match qualified client ID (label: `mls-identity-mismatch`)" + } + }, + "summary": "Upload a fresh batch of key packages and replace the old ones" + } + }, + "/mls/key-packages/self/{client}/count": { + "get": { + "description": " [internal route ID: \"mls-key-packages-count\"]\n\n", + "operationId": "mls-key-packages-count", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Ciphersuite in hex format (e.g. 0xf031)", + "in": "query", + "name": "ciphersuite", + "required": true, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OwnKeyPackages" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OwnKeyPackages" + } + } + }, + "description": "Number of key packages" + } + }, + "summary": "Return the number of unclaimed key packages for a given ciphersuite and client" + } + }, + "/mls/messages": { + "post": { + "description": " [internal route ID: \"mls-message\"]\n\n\n\n**Note**: this endpoint can execute proposals, and therefore return all possible errors associated with adding or removing members to a conversation, in addition to the ones listed below. See the documentation of [POST /conversations/{cnv}/members/v2](#/default/post_conversations__cnv__members_v2) and [POST /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}](#/default/delete_conversations__cnv_domain___cnv__members__usr_domain___usr_) for more details on the possible error responses of each type of proposal.", + "operationId": "mls-message", + "requestBody": { + "content": { + "message/mls": { + "schema": { + "$ref": "#/components/schemas/MLSMessage" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MLSMessageSendingStatus" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSMessageSendingStatus" + } + } + }, + "description": "Message sent" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-invalid-leaf-node-signature", + "message": "Invalid leaf node signature" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-invalid-leaf-node-signature", + "mls-self-removal-not-allowed", + "mls-protocol-error", + "mls-not-enabled", + "mls-invalid-leaf-node-index", + "mls-group-conversation-mismatch", + "mls-commit-missing-references", + "mls-client-sender-user-mismatch" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid leaf node signature (label: `mls-invalid-leaf-node-signature`)\n\nSubmitted group info is inconsistent with the backend group state\n\nSelf removal from group is not allowed (label: `mls-self-removal-not-allowed`)\n\nMLS protocol error (label: `mls-protocol-error`)\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nA referenced leaf node index points to a blank or non-existing node (label: `mls-invalid-leaf-node-index`)\n\nConversation ID resolved from Group ID does not match submitted Conversation ID (label: `mls-group-conversation-mismatch`)\n\nThe commit is not referencing all pending proposals (label: `mls-commit-missing-references`)\n\nUser ID resolved from Client ID does not match message's sender user ID (label: `mls-client-sender-user-mismatch`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "mls-subconv-join-parent-missing", + "message": "MLS client cannot join the subconversation because it is not member of the parent conversation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-subconv-join-parent-missing", + "missing-legalhold-consent", + "legalhold-not-enabled", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS client cannot join the subconversation because it is not member of the parent conversation (label: `mls-subconv-join-parent-missing`)\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`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nConversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "mls-proposal-not-found", + "message": "A proposal referenced in a commit message could not be found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-proposal-not-found", + "no-conversation", + "no-conversation-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "A proposal referenced in a commit message could not be found (label: `mls-proposal-not-found`)\n\nConversation not found (label: `no-conversation`)\n\nConversation member not found (label: `no-conversation-member`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "missing_users": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + } + }, + "required": [ + "missing_users" + ], + "type": "object" + } + } + }, + "description": "Group is out of sync\n\nAdding members to the conversation is not possible because the backends involved do not form a fully connected graph\n\nThe conversation epoch in a message is too old (label: `mls-stale-message`)\n\nA proposal of type Add or Remove does not apply to the full list of clients for a user (label: `mls-client-mismatch`)" + }, + "422": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 422, + "label": "mls-unsupported-proposal", + "message": "Unsupported proposal type" + }, + "properties": { + "code": { + "enum": [ + 422 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-unsupported-proposal", + "mls-unsupported-message" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unsupported proposal type (label: `mls-unsupported-proposal`)\n\nAttempted to send a message with an unsupported combination of content type and wire format (label: `mls-unsupported-message`)" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Post an MLS message" + } + }, + "/mls/public-keys": { + "get": { + "description": " [internal route ID: \"mls-public-keys\"]\n\nThe format of the returned key is determined by the `format` query parameter:\n - raw (default): base64-encoded raw public keys\n - jwk: keys are nested objects in JWK format.", + "operationId": "mls-public-keys", + "parameters": [ + { + "in": "query", + "name": "format", + "required": false, + "schema": { + "enum": [ + "raw", + "jwk" + ], + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MLSKeysByPurpose" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSKeysByPurpose" + } + } + }, + "description": "Public keys" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `format`\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + } + }, + "summary": "Get public keys used by the backend to sign external proposals" + } + }, + "/mls/reset-conversation": { + "post": { + "description": " [internal route ID: \"mls-reset-conversation\"]\n\n", + "operationId": "mls-reset-conversation", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSReset" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Conversation reset" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-protocol-error", + "message": "MLS protocol error" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-protocol-error", + "mls-group-id-not-supported", + "mls-federated-reset-not-supported", + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS protocol error (label: `mls-protocol-error`)\n\nThe group ID version of the conversation is not supported by one of the federated backends (label: `mls-group-id-not-supported`)\n\nReset is not supported by the owning backend of the conversation (label: `mls-federated-reset-not-supported`)\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`) or `body`" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "action-denied", + "message": "Insufficient authorization (missing leave_conversation)" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "action-denied", + "invalid-op", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient authorization (missing leave_conversation) (label: `action-denied`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "mls-stale-message", + "message": "The conversation epoch in a message is too old" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-stale-message" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The conversation epoch in a message is too old (label: `mls-stale-message`)" + } + }, + "summary": "Reset an MLS conversation to epoch 0" + } + }, + "/notifications": { + "get": { + "description": " [internal route ID: \"get-notifications\"]\n\n", + "operationId": "get-notifications", + "parameters": [ + { + "description": "Only return notifications more recent than this", + "in": "query", + "name": "since", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Only return notifications targeted at the given client", + "in": "query", + "name": "client", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Maximum number of notifications to return", + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 10000, + "minimum": 100, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueuedNotificationList" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QueuedNotificationList" + } + } + }, + "description": "Notification list" + }, + "404": { + "content": { + "application/json": { + "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" + } + }, + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Some notifications not found (label: `not-found`)" + } + }, + "summary": "Fetch notifications" + } + }, + "/notifications/last": { + "get": { + "description": " [internal route ID: \"get-last-notification\"]\n\n", + "operationId": "get-last-notification", + "parameters": [ + { + "description": "Only return notifications targeted at the given client", + "in": "query", + "name": "client", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueuedNotification" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QueuedNotification" + } + } + }, + "description": "Notification found" + }, + "404": { + "content": { + "application/json": { + "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" + } + }, + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Some notifications not found (label: `not-found`)" + } + }, + "summary": "Fetch the last notification" + } + }, + "/notifications/{id}": { + "get": { + "description": " [internal route ID: \"get-notification-by-id\"]\n\n", + "operationId": "get-notification-by-id", + "parameters": [ + { + "description": "Notification ID", + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Only return notifications targeted at the given client", + "in": "query", + "name": "client", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueuedNotification" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QueuedNotification" + } + } + }, + "description": "Notification found" + }, + "404": { + "content": { + "application/json": { + "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" + } + }, + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`id` or Some notifications not found (label: `not-found`)" + } + }, + "summary": "Fetch a notification by ID" + } + }, + "/oauth/applications": { + "get": { + "description": " [internal route ID: \"get-oauth-applications\"]\n\nGet all OAuth applications with active account access for a user.", + "operationId": "get-oauth-applications", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/OAuthApplication" + }, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/OAuthApplication" + }, + "type": "array" + } + } + }, + "description": "OAuth applications found" + } + }, + "summary": "Get OAuth applications with account access" + } + }, + "/oauth/applications/{OAuthClientId}/sessions": { + "delete": { + "description": " [internal route ID: \"revoke-oauth-account-access\"]\n\n", + "operationId": "revoke-oauth-account-access", + "parameters": [ + { + "description": "The ID of the OAuth client", + "in": "path", + "name": "OAuthClientId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PasswordReqBody" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "OAuth application access revoked" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "Revoke account access from an OAuth application" + } + }, + "/oauth/applications/{OAuthClientId}/sessions/{RefreshTokenId}": { + "delete": { + "description": " [internal route ID: \"delete-oauth-refresh-token\"]\n\nRevoke an active OAuth session by providing the refresh token ID.", + "operationId": "delete-oauth-refresh-token", + "parameters": [ + { + "description": "The ID of the OAuth client", + "in": "path", + "name": "OAuthClientId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "The ID of the refresh token", + "in": "path", + "name": "RefreshTokenId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PasswordReqBody" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`OAuthClientId` or `RefreshTokenId` not found\n\nOAuth client not found (label: `not-found`)" + } + }, + "summary": "Revoke an active OAuth session" + } + }, + "/oauth/authorization/codes": { + "post": { + "description": " [internal route ID: \"create-oauth-auth-code\"]\n\nCurrently only supports the 'code' response type, which corresponds to the authorization code flow.", + "operationId": "create-oauth-auth-code", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateOAuthAuthorizationCodeRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 400, + "label": "redirect-url-miss-match", + "message": "The redirect URL does not match the one registered with the client" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "redirect-url-miss-match" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "redirect-url-miss-match", + "message": "The redirect URL does not match the one registered with the client" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "redirect-url-miss-match" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Bad Request\n\nThe redirect URL does not match the one registered with the client (label: `redirect-url-miss-match`) or `body`", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "description": "Forbidden", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "404": { + "description": "Not Found", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + } + }, + "summary": "Create an OAuth authorization code" + } + }, + "/oauth/clients/{OAuthClientId}": { + "get": { + "description": " [internal route ID: \"get-oauth-client\"]\n\n", + "operationId": "get-oauth-client", + "parameters": [ + { + "description": "The ID of the OAuth client", + "in": "path", + "name": "OAuthClientId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthClient" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OAuthClient" + } + } + }, + "description": "OAuth client found" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "forbidden", + "message": "OAuth is disabled" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "OAuth is disabled (label: `forbidden`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`OAuthClientId` or OAuth client not found (label: `not-found`)\n\nOAuth client not found (label: `not-found`)" + } + }, + "summary": "Get OAuth client information" + } + }, + "/oauth/revoke": { + "post": { + "description": " [internal route ID: \"revoke-oauth-refresh-token\"]\n\nRevoke an access token.", + "operationId": "revoke-oauth-refresh-token", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OAuthRevokeRefreshTokenRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "forbidden", + "message": "Invalid refresh token" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid refresh token (label: `forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "OAuth client not found (label: `not-found`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "jwt-error", + "message": "Internal error while handling JWT token" + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "jwt-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Internal error while handling JWT token (label: `jwt-error`)" + } + }, + "summary": "Revoke an OAuth refresh token" + } + }, + "/oauth/token": { + "post": { + "description": " [internal route ID: \"create-oauth-access-token\"]\n\nObtain a new access token from an authorization code or a refresh token.", + "operationId": "create-oauth-access-token", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Either_OAuthAccessTokenRequest_OAuthRefreshAccessTokenRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OAuthAccessTokenResponse" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid_grant", + "message": "Invalid grant" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid_grant", + "forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid grant (label: `invalid_grant`)\n\nInvalid client credentials (label: `forbidden`)\n\nInvalid grant type (label: `forbidden`)\n\nInvalid refresh token (label: `forbidden`)\n\nOAuth is disabled (label: `forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "OAuth client not found (label: `not-found`)\n\nOAuth authorization code not found (label: `not-found`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "jwt-error", + "message": "Internal error while handling JWT token" + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "jwt-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Internal error while handling JWT token (label: `jwt-error`)" + } + }, + "summary": "Create an OAuth access token" + } + }, + "/one2one-conversations": { + "post": { + "description": " [internal route ID: \"create-one-to-one-conversation\"]\n\n", + "operationId": "create-one-to-one-conversation", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewOne2OneConv" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OwnConversationV3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OwnConversationV3" + } + } + }, + "description": "Conversation existed", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OwnConversationV3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OwnConversationV3" + } + } + }, + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Team not found (label: `no-team`)\n\nNot a member of a binding team (label: `non-binding-team`)" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Create a 1:1 conversation" + } + }, + "/one2one-conversations/{usr_domain}/{usr}": { + "get": { + "description": " [internal route ID: \"get-one-to-one-mls-conversation\"]\n\n", + "operationId": "get-one-to-one-mls-conversation", + "parameters": [ + { + "in": "path", + "name": "usr_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "usr", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "format", + "required": false, + "schema": { + "enum": [ + "raw", + "jwk" + ], + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MLSOne2OneConversation_SomeKey" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSOne2OneConversation_SomeKey" + } + } + }, + "description": "MLS 1-1 conversation" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `format`\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "not-connected", + "message": "Users are not connected" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-connected" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Users are not connected (label: `not-connected`)" + } + }, + "summary": "Get an MLS 1:1 conversation" + } + }, + "/password-reset": { + "post": { + "description": " [internal route ID: \"post-password-reset\"]\n\n", + "operationId": "post-password-reset", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewPasswordReset" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Password reset code created and sent by email." + } + }, + "summary": "Initiate a password reset." + } + }, + "/password-reset/complete": { + "post": { + "description": " [internal route ID: \"post-password-reset-complete\"]\n\n", + "operationId": "post-password-reset-complete", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CompletePasswordReset" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Password reset successful." + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-code", + "message": "Invalid password reset code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid password reset code. (label: `invalid-code`)" + } + }, + "summary": "Complete a password reset." + } + }, + "/properties": { + "delete": { + "description": " [internal route ID: \"clear-properties\"]\n\n", + "operationId": "clear-properties", + "responses": { + "200": { + "description": "Properties cleared" + } + }, + "summary": "Clear all properties" + }, + "get": { + "description": " [internal route ID: \"list-property-keys\"]\n\n", + "operationId": "list-property-keys", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ASCII" + }, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/ASCII" + }, + "type": "array" + } + } + }, + "description": "List of property keys" + } + }, + "summary": "List all property keys" + } + }, + "/properties-values": { + "get": { + "description": " [internal route ID: \"list-properties\"]\n\n", + "operationId": "list-properties", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PropertyKeysAndValues" + } + } + }, + "description": "" + } + }, + "summary": "List all properties with key and value" + } + }, + "/properties/{key}": { + "delete": { + "description": " [internal route ID: \"delete-property\"]\n\n", + "operationId": "delete-property", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "format": "printable", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Property deleted" + } + }, + "summary": "Delete a property" + }, + "get": { + "description": " [internal route ID: \"get-property\"]\n\n", + "operationId": "get-property", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "format": "printable", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PropertyValue" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PropertyValue" + } + } + }, + "description": "The property value" + }, + "404": { + "description": "`key` or Property not found(**Note**: This error has an empty body for legacy reasons)" + } + }, + "summary": "Get a property value" + }, + "put": { + "description": " [internal route ID: \"set-property\"]\n\n", + "operationId": "set-property", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "format": "printable", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PropertyValue" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Property set" + } + }, + "summary": "Set a user property" + } + }, + "/provider": { + "delete": { + "description": " [internal route ID: \"provider-delete\"]\n\n", + "operationId": "provider-delete", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeleteProvider" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "invalid-provider", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nThe provider does not exist. (label: `invalid-provider`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Delete a provider" + }, + "get": { + "description": " [internal route ID: \"provider-get-account\"]\n\n", + "operationId": "provider-get-account", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Provider" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Provider" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Provider not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Provider not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Provider not found. (label: `not-found`)\n\nProvider not found. (label: `not-found`)" + } + }, + "summary": "Get account" + }, + "put": { + "description": " [internal route ID: \"provider-update\"]\n\n", + "operationId": "provider-update", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateProvider" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-provider", + "message": "The provider does not exist." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-provider", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The provider does not exist. (label: `invalid-provider`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Update a provider" + } + }, + "/provider/activate": { + "get": { + "description": " [internal route ID: \"provider-activate\"]\n\n", + "operationId": "provider-activate", + "parameters": [ + { + "in": "query", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "code", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProviderActivationResponse" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ProviderActivationResponse" + } + } + }, + "description": "" + }, + "204": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-code", + "message": "Invalid verification code" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid verification code (label: `invalid-code`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Activate a provider" + } + }, + "/provider/assets": { + "post": { + "description": " [internal route ID: (\"assets-upload-v3\", provider)]\n\n

Construct the request as multipart/mixed; set header Content-Type: multipart/mixed; boundary=<boundary>.

Use exactly two parts in this order:

  1. application/json metadata (AssetSettings)
  2. application/octet-stream asset bytes

Each part must include Content-Type and Content-Length; the second part may include Content-MD5. Use CRLF between headers and bodies.

When asset audit logging is enabled, the JSON metadata must include:

  • convId: object { id: UUID, domain: String } (qualified conversation ID)
  • filename: String
  • filetype: String MIME type (e.g. image/png, application/pdf)

Optional metadata: public (Bool, default false), retention (one of eternal, persistent, volatile, eternal-infrequent_access, expiring).

For profile pictures or team icons without a conversation, set convId.id to 00000000-0000-0000-0000-000000000000 and convId.domain to the tenant’s domain; use any reasonable filename.

Note: the server treats the asset bytes as application/octet-stream; filetype is used for auditing only.

Example body (boundary=frontier):

Content-Type: multipart/mixed; boundary=frontier

--frontier
Content-Type: application/json
Content-Length: 191

{\"public\":false,\"retention\":\"volatile\",\"convId\":{\"id\":\"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\",\"domain\":\"example.com\"},\"filename\":\"report.pdf\",\"filetype\":\"application/pdf\"}
--frontier
Content-Type: application/octet-stream
Content-Length: 11

Hello Audit
--frontier--
", + "operationId": "assets-upload-v3_provider", + "requestBody": { + "content": { + "multipart/mixed": { + "schema": { + "$ref": "#/components/schemas/AssetSource" + } + } + }, + "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." + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Asset" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Asset" + } + } + }, + "description": "Asset posted", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "incomplete-body", + "message": "HTTP content-length header does not match body size" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "incomplete-body", + "invalid-length" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nHTTP content-length header does not match body size (label: `incomplete-body`)\n\nInvalid content length (label: `invalid-length`)" + }, + "413": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Asset too large (label: `client-error`)" + } + }, + "summary": "Upload an asset" + } + }, + "/provider/assets/{key}": { + "delete": { + "description": " [internal route ID: (\"assets-delete-v3\", provider)]\n\n", + "operationId": "assets-delete-v3_provider", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Unauthorised operation (label: `unauthorised`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`key` not found\n\nAsset not found (label: `not-found`)" + } + }, + "summary": "Delete an asset" + }, + "get": { + "description": " [internal route ID: (\"assets-download-v3\", provider)]\n\n", + "operationId": "assets-download-v3_provider", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "Asset-Token", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "asset_token", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "302": { + "description": "Asset found", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "404": { + "content": { + "application/json": { + "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" + } + }, + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`key` or Asset not found (label: `not-found`)" + } + }, + "summary": "Download an asset" + } + }, + "/provider/email": { + "put": { + "description": " [internal route ID: \"provider-update-email\"]\n\n", + "operationId": "provider-update-email", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/EmailUpdate" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Invalid `body`\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-provider", + "message": "The provider does not exist." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-provider", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The provider does not exist. (label: `invalid-provider`)\n\nAccess denied. (label: `access-denied`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Too many request to generate a verification code." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Too many request to generate a verification code. (label: `too-many-requests`)" + } + }, + "summary": "Update a provider email" + } + }, + "/provider/login": { + "post": { + "description": " [internal route ID: \"provider-login\"]\n\n", + "operationId": "provider-login", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ProviderLogin" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "headers": { + "Set-Cookie": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Login as a provider" + } + }, + "/provider/password": { + "put": { + "description": " [internal route ID: \"provider-update-password\"]\n\n", + "operationId": "provider-update-password", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PasswordChange" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "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" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "For password reset, new and old password must be different. (label: `password-must-differ`)" + } + }, + "summary": "Update a provider password" + } + }, + "/provider/password-reset": { + "post": { + "description": " [internal route ID: \"provider-password-reset\"]\n\n", + "operationId": "provider-password-reset", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PasswordReset" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-code", + "message": "Invalid password reset code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code", + "invalid-key" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid password reset code. (label: `invalid-code`)\n\nInvalid email or mobile number for password reset. (label: `invalid-key`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)\n\nA password reset is already in progress. (label: `code-exists`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Too many request to generate a verification code." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Too many request to generate a verification code. (label: `too-many-requests`)" + } + }, + "summary": "Begin a password reset" + } + }, + "/provider/password-reset/complete": { + "post": { + "description": " [internal route ID: \"provider-password-reset-complete\"]\n\n", + "operationId": "provider-password-reset-complete", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CompletePasswordReset" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-code", + "message": "Invalid password reset code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid password reset code. (label: `invalid-code`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "invalid-code", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nInvalid verification code (label: `invalid-code`)\n\nAccess denied. (label: `access-denied`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "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" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "For password reset, new and old password must be different. (label: `password-must-differ`)" + } + }, + "summary": "Complete a password reset" + } + }, + "/provider/register": { + "post": { + "description": " [internal route ID: \"provider-register\"]\n\n", + "operationId": "provider-register", + "parameters": [ + { + "in": "header", + "name": "X-Forwarded-For", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewProvider" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewProviderResponse" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewProviderResponse" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Invalid `body` or `X-Forwarded-For`\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Too many request to generate a verification code." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Too many request to generate a verification code. (label: `too-many-requests`)" + } + }, + "summary": "Register a new provider" + } + }, + "/provider/services": { + "get": { + "description": " [internal route ID: \"get-provider-services\"]\n\n", + "operationId": "get-provider-services", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/Service" + }, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "List provider services" + }, + "post": { + "description": " [internal route ID: \"post-provider-services\"]\n\n", + "operationId": "post-provider-services", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewService" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewServiceResponse" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewServiceResponse" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-service-key", + "message": "Invalid service key." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-service-key" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid service key. (label: `invalid-service-key`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "Create a new service" + } + }, + "/provider/services/{service-id}": { + "delete": { + "description": " [internal route ID: \"delete-provider-services-by-service-id\"]\n\n", + "operationId": "delete-provider-services-by-service-id", + "parameters": [ + { + "in": "path", + "name": "service-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeleteService" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Service not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`service-id` not found\n\nService not found. (label: `not-found`)" + } + }, + "summary": "Delete service" + }, + "get": { + "description": " [internal route ID: \"get-provider-services-by-service-id\"]\n\n", + "operationId": "get-provider-services-by-service-id", + "parameters": [ + { + "in": "path", + "name": "service-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Service" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Service not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`service-id` not found\n\nService not found. (label: `not-found`)" + } + }, + "summary": "Get provider service by service id" + }, + "put": { + "description": " [internal route ID: \"put-provider-services-by-service-id\"]\n\n", + "operationId": "put-provider-services-by-service-id", + "parameters": [ + { + "in": "path", + "name": "service-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateService" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Provider service updated" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Provider not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`service-id` not found\n\nProvider not found. (label: `not-found`)\n\nService not found. (label: `not-found`)" + } + }, + "summary": "Update provider service" + } + }, + "/provider/services/{service-id}/connection": { + "put": { + "description": " [internal route ID: \"put-provider-services-connection-by-service-id\"]\n\n", + "operationId": "put-provider-services-connection-by-service-id", + "parameters": [ + { + "in": "path", + "name": "service-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateServiceConn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Provider service connection updated" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-service-key", + "message": "Invalid service key." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-service-key" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid service key. (label: `invalid-service-key`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Service not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`service-id` not found\n\nService not found. (label: `not-found`)" + } + }, + "summary": "Update provider service connection" + } + }, + "/providers/{pid}": { + "get": { + "description": " [internal route ID: \"provider-get-profile\"]\n\n", + "operationId": "provider-get-profile", + "parameters": [ + { + "in": "path", + "name": "pid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Provider" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Provider" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Provider not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Provider not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`pid` or Provider not found. (label: `not-found`)" + } + }, + "summary": "Get profile" + } + }, + "/providers/{provider-id}/services": { + "get": { + "description": " [internal route ID: \"get-provider-services-by-provider-id\"]\n\n", + "operationId": "get-provider-services-by-provider-id", + "parameters": [ + { + "in": "path", + "name": "provider-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/ServiceProfile" + }, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "Get provider services by provider id" + } + }, + "/providers/{provider-id}/services/{service-id}": { + "get": { + "description": " [internal route ID: \"get-provider-services-by-provider-id-and-service-id\"]\n\n", + "operationId": "get-provider-services-by-provider-id-and-service-id", + "parameters": [ + { + "in": "path", + "name": "provider-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "service-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ServiceProfile" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Service not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`provider-id` or `service-id` not found\n\nService not found. (label: `not-found`)" + } + }, + "summary": "Get provider service by provider id and service id" + } + }, + "/proxy/giphy/v1/gifs": {}, + "/proxy/googlemaps/api/staticmap": {}, + "/proxy/googlemaps/maps/api/geocode": {}, + "/proxy/soundcloud/resolve": {}, + "/proxy/soundcloud/stream": {}, + "/proxy/spotify/api/token": {}, + "/proxy/youtube/v3": {}, + "/push/tokens": { + "get": { + "description": " [internal route ID: \"get-push-tokens\"]\n\n", + "operationId": "get-push-tokens", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PushTokenList" + } + } + }, + "description": "" + } + }, + "summary": "List the user's registered push tokens" + }, + "post": { + "description": " [internal route ID: \"register-push-token\"]\n\n", + "operationId": "register-push-token", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PushToken" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PushToken" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PushToken" + } + } + }, + "description": "Push token registered", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 400, + "label": "apns-voip-not-supported", + "message": "Adding APNS_VOIP tokens is not supported" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "apns-voip-not-supported" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "apns-voip-not-supported", + "message": "Adding APNS_VOIP tokens is not supported" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "apns-voip-not-supported" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Adding APNS_VOIP tokens is not supported (label: `apns-voip-not-supported`) or `body`" + }, + "404": { + "content": { + "application/json": { + "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" + } + }, + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "App does not exist (label: `app-not-found`)\n\nInvalid push token (label: `invalid-token`)" + }, + "413": { + "content": { + "application/json": { + "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" + } + }, + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)" + } + }, + "summary": "Register a native push token" + } + }, + "/push/tokens/{pid}": { + "delete": { + "description": " [internal route ID: \"delete-push-token\"]\n\n", + "operationId": "delete-push-token", + "parameters": [ + { + "description": "The push token to delete", + "in": "path", + "name": "pid", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Push token unregistered" + }, + "404": { + "content": { + "application/json": { + "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" + } + }, + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`pid` or Push token not found (label: `not-found`)" + } + }, + "summary": "Unregister a native push token" + } + }, + "/register": { + "post": { + "description": " [internal route ID: \"register\"]\n\nIf the environment where the registration takes place is private and a registered email address is not whitelisted, a 403 error is returned.", + "operationId": "register", + "parameters": [ + { + "in": "header", + "name": "X-Forwarded-For", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewUser" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + }, + "description": "User created and pending activation", + "headers": { + "Location": { + "description": "UserId", + "schema": { + "format": "uuid", + "type": "string" + } + }, + "Set-Cookie": { + "description": "Cookie", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "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" + } + }, + "application/json;charset=utf-8": { + "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" + } + } + }, + "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` or `X-Forwarded-For`" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unauthorized", + "message": "Unauthorized e-mail address" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorized", + "missing-identity", + "blacklisted-email", + "too-many-team-members", + "user-creation-restricted", + "ephemeral-user-creation-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unauthorized", + "message": "Unauthorized e-mail address" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorized", + "missing-identity", + "blacklisted-email", + "too-many-team-members", + "user-creation-restricted", + "ephemeral-user-creation-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unauthorized e-mail address (label: `unauthorized`)\n\nUsing an invitation code requires registering the given email. (label: `missing-identity`)\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`)\n\nEphemeral user creation is disabled on this instance. (label: `ephemeral-user-creation-disabled`)" + }, + "404": { + "content": { + "application/json": { + "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" + } + }, + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "User does not exist (label: `invalid-code`)\n\nInvalid activation code (label: `invalid-code`)" + }, + "409": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address is in use. (label: `key-exists`)" + } + }, + "summary": "Register a new user." + } + }, + "/scim/auth-tokens": { + "delete": { + "description": " [internal route ID: \"auth-tokens-delete\"]\n\n", + "operationId": "auth-tokens-delete", + "parameters": [ + { + "in": "query", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)" + } + } + }, + "get": { + "description": " [internal route ID: \"auth-tokens-list\"]\n\n", + "operationId": "auth-tokens-list", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ScimTokenList" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)" + } + } + }, + "post": { + "description": " [internal route ID: \"auth-tokens-create\"]\n\n", + "operationId": "auth-tokens-create", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateScimToken" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateScimTokenResponse" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)" + } + } + } + }, + "/scim/auth-tokens/{id}": { + "put": { + "description": " [internal route ID: \"auth-tokens-put-name\"]\n\n", + "operationId": "auth-tokens-put-name", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ScimTokenName" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)" + } + } + } + }, + "/search/contacts": { + "get": { + "description": " [internal route ID: \"search-contacts\"]\n\n", + "operationId": "search-contacts", + "parameters": [ + { + "description": "Search query", + "in": "query", + "name": "q", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Searched domain. Note: This is optional only for backwards compatibility, future versions will mandate this.", + "in": "query", + "name": "domain", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Number of results to return (min: 1, max: 500, default 15)", + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 500, + "minimum": 1, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchResult_Contact" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `insufficient-permissions`)" + } + }, + "summary": "Search for users" + } + }, + "/self": { + "delete": { + "description": " [internal route ID: \"delete-self\"]\n\nif 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.", + "operationId": "delete-self", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeleteUser" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Deletion is initiated." + }, + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletionCodeTimeout" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeletionCodeTimeout" + } + } + }, + "description": "Deletion is pending verification with a code." + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-user", + "message": "Invalid user" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-user" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid user (label: `invalid-user`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)" + } + }, + "summary": "Initiate account deletion." + }, + "get": { + "description": " [internal route ID: \"get-self\"]\n\n\nOAuth scope: `read:self`", + "operationId": "get-self", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + }, + "description": "" + } + }, + "summary": "Get your own profile" + }, + "put": { + "description": " [internal route ID: \"put-self\"]\n\n", + "operationId": "put-self", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User updated" + } + }, + "summary": "Update your profile." + } + }, + "/self/email": { + "delete": { + "description": " [internal route ID: \"remove-email\"]\n\nYour email address can only be removed if you also have a phone number.", + "operationId": "remove-email", + "responses": { + "200": { + "description": "Identity Removed" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "last-identity", + "message": "The last user identity cannot be removed." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "last-identity", + "no-identity" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "last-identity", + "message": "The last user identity cannot be removed." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "last-identity", + "no-identity" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The last user identity cannot be removed. (label: `last-identity`)\n\nThe user has no verified email (label: `no-identity`)" + } + }, + "summary": "Remove your email address." + } + }, + "/self/handle": { + "put": { + "description": " [internal route ID: \"change-handle\"]\n\n", + "operationId": "change-handle", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/HandleUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Handle Changed" + } + }, + "summary": "Change your handle." + } + }, + "/self/locale": { + "put": { + "description": " [internal route ID: \"change-locale\"]\n\n", + "operationId": "change-locale", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LocaleUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Local Changed" + } + }, + "summary": "Change your locale." + } + }, + "/self/password": { + "head": { + "description": " [internal route ID: \"check-password-exists\"]\n\n", + "operationId": "check-password-exists", + "responses": { + "200": { + "description": "Password is set" + }, + "404": { + "description": "Password is not set" + } + }, + "summary": "Check that your password is set." + }, + "put": { + "description": " [internal route ID: \"change-password\"]\n\n", + "operationId": "change-password", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PasswordChange" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Password Changed" + }, + "403": { + "content": { + "application/json": { + "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" + } + }, + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nThe user has no verified email (label: `no-identity`)" + }, + "409": { + "content": { + "application/json": { + "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" + } + }, + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "For password change, new and old password must be different. (label: `password-must-differ`)" + } + }, + "summary": "Change your password." + } + }, + "/self/supported-protocols": { + "put": { + "description": " [internal route ID: \"change-supported-protocols\"]\n\n", + "operationId": "change-supported-protocols", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SupportedProtocolUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Supported protocols changed" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "mls-protocol-error", + "message": "MLS protocol cannot be removed" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-protocol-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS protocol cannot be removed (label: `mls-protocol-error`)" + } + }, + "summary": "Change your supported protocols" + } + }, + "/services": { + "get": { + "description": " [internal route ID: \"get-services\"]\n\n", + "operationId": "get-services", + "parameters": [ + { + "in": "query", + "name": "tags", + "required": false, + "schema": { + "enum": [ + "audio", + "books", + "business", + "design", + "education", + "entertainment", + "finance", + "fitness", + "food-drink", + "games", + "graphics", + "health", + "integration", + "lifestyle", + "media", + "medical", + "movies", + "music", + "news", + "photography", + "poll", + "productivity", + "quiz", + "rating", + "shopping", + "social", + "sports", + "travel", + "tutorial", + "video", + "weather" + ], + "type": "string" + } + }, + { + "in": "query", + "name": "start", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 100, + "minimum": 10, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ServiceProfilePage" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "List services" + } + }, + "/services/tags": { + "get": { + "description": " [internal route ID: \"get-services-tags\"]\n\n", + "operationId": "get-services-tags", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ServiceTagList" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "Get services tags" + } + }, + "/sso/finalize-login": { + "post": { + "deprecated": true, + "description": " [internal route ID: \"auth-resp-legacy\"]\n\nDEPRECATED! 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", + "operationId": "auth-resp-legacy", + "responses": { + "200": { + "content": { + "text/plain;charset=utf-8": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/sso/finalize-login/{team}": { + "post": { + "description": " [internal route ID: \"auth-resp\"]\n\n", + "operationId": "auth-resp", + "parameters": [ + { + "in": "path", + "name": "team", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/plain;charset=utf-8": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/sso/initiate-login/{idp}": { + "get": { + "description": " [internal route ID: \"auth-req\"]\n\n", + "operationId": "auth-req", + "parameters": [ + { + "in": "query", + "name": "success_redirect", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "error_redirect", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "idp", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/html": { + "schema": { + "$ref": "#/components/schemas/FormRedirect" + } + } + }, + "description": "" + } + } + }, + "head": { + "description": " [internal route ID: \"auth-req-precheck\"]\n\n", + "operationId": "auth-req-precheck", + "parameters": [ + { + "in": "query", + "name": "success_redirect", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "error_redirect", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "idp", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/plain;charset=utf-8": {} + }, + "description": "" + } + } + } + }, + "/sso/metadata": { + "get": { + "deprecated": true, + "description": " [internal route ID: \"sso-metadata\"]\n\nDEPRECATED! 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", + "operationId": "sso-metadata", + "responses": { + "200": { + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/sso/metadata/{team}": { + "get": { + "description": " [internal route ID: \"sso-team-metadata\"]\n\n", + "operationId": "sso-team-metadata", + "parameters": [ + { + "in": "path", + "name": "team", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/sso/settings": { + "get": { + "description": " [internal route ID: \"sso-settings\"]\n\n", + "operationId": "sso-settings", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SsoSettings" + } + } + }, + "description": "" + } + } + } + }, + "/system/settings": { + "get": { + "description": " [internal route ID: \"get-system-settings\"]\n\n", + "operationId": "get-system-settings", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SystemSettings" + } + } + }, + "description": "" + } + }, + "summary": "Returns a curated set of system configuration settings for authorized users." + } + }, + "/system/settings/unauthorized": { + "get": { + "description": " [internal route ID: \"get-system-settings-unauthorized\"]\n\n", + "operationId": "get-system-settings-unauthorized", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SystemSettingsPublic" + } + } + }, + "description": "" + } + }, + "summary": "Returns a curated set of system configuration settings." + } + }, + "/teams/invitations/accept": { + "post": { + "description": " [internal route ID: \"accept-team-invitation\"]\n\n", + "operationId": "accept-team-invitation", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AcceptTeamInvitation" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Team invitation accepted." + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "missing-auth", + "message": "Re-authentication via password required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-auth", + "invalid-credentials", + "missing-identity", + "too-many-team-members" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Re-authentication via password required (label: `missing-auth`)\n\nAuthentication failed (label: `invalid-credentials`)\n\nUsing an invitation code requires registering the given email. (label: `missing-identity`)\n\nToo many members in this team. (label: `too-many-team-members`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "Invalid activation code" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code", + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid activation code (label: `invalid-code`)\n\nUser does not exist (label: `invalid-code`)\n\nNo pending invitations exists. (label: `not-found`)" + } + }, + "summary": "Accept a team invitation, changing a personal account into a team member account." + } + }, + "/teams/invitations/by-email": { + "head": { + "description": " [internal route ID: \"head-team-invitations\"]\n\n", + "operationId": "head-team-invitations", + "parameters": [ + { + "description": "Email address", + "in": "query", + "name": "email", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Pending invitation exists." + }, + "404": { + "content": { + "application/json": { + "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" + } + }, + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "No pending invitations exists. (label: `not-found`)" + }, + "409": { + "content": { + "application/json": { + "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" + } + }, + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Multiple conflicting invitations to different teams exists. (label: `conflicting-invitations`)" + } + }, + "summary": "Check if there is an invitation pending given an email address." + } + }, + "/teams/invitations/info": { + "get": { + "description": " [internal route ID: \"get-team-invitation-info\"]\n\n", + "operationId": "get-team-invitation-info", + "parameters": [ + { + "description": "Invitation code", + "in": "query", + "name": "code", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvitationUserView" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/InvitationUserView" + } + } + }, + "description": "Invitation info" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-invitation-code", + "message": "Invalid invitation code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-invitation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `code`\n\nInvalid invitation code. (label: `invalid-invitation-code`)" + } + }, + "summary": "Get invitation info given a code." + } + }, + "/teams/notifications": { + "get": { + "description": " [internal route ID: \"get-team-notifications\"]\n\nThis 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'.", + "operationId": "get-team-notifications", + "parameters": [ + { + "description": "Notification id to start with in the response (UUIDv1)", + "in": "query", + "name": "since", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Maximum number of events to return (1..10000; default: 1000)", + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 10000, + "minimum": 1, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QueuedNotificationList" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-notification-id", + "message": "Could not parse notification id (must be UUIDv1)." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-notification-id" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `size` or `since`\n\nCould not parse notification id (must be UUIDv1). (label: `invalid-notification-id`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Team not found (label: `no-team`)" + } + }, + "summary": "Read recently added team members from team queue" + } + }, + "/teams/{team-id}/services/whitelist": { + "post": { + "description": " [internal route ID: \"post-team-whitelist-by-team-id\"]\n\n", + "operationId": "post-team-whitelist-by-team-id", + "parameters": [ + { + "in": "path", + "name": "team-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateServiceWhitelist" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "UpdateServiceWhitelistRespChanged" + }, + "204": { + "description": "UpdateServiceWhitelistRespUnchanged" + } + }, + "summary": "Update service whitelist" + } + }, + "/teams/{team-id}/services/whitelisted": { + "get": { + "description": " [internal route ID: \"get-whitelisted-services-by-team-id\"]\n\n", + "operationId": "get-whitelisted-services-by-team-id", + "parameters": [ + { + "in": "path", + "name": "team-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "prefix", + "required": false, + "schema": { + "maxLength": 128, + "minLength": 1, + "type": "string" + } + }, + { + "in": "query", + "name": "filter_disabled", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 100, + "minimum": 10, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ServiceProfilePage" + } + } + }, + "description": "" + } + }, + "summary": "Get whitelisted services by team id" + } + }, + "/teams/{teamId}/registered-domains": { + "get": { + "description": " [internal route ID: \"get-all-registered-domains\"]\n\n", + "operationId": "get-all-registered-domains", + "parameters": [ + { + "in": "path", + "name": "teamId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RegisteredDomains" + } + } + }, + "description": "" + } + }, + "summary": "Get all registered domains" + } + }, + "/teams/{teamId}/registered-domains/{domain}": { + "delete": { + "description": " [internal route ID: \"delete-registered-domain\"]\n\n", + "operationId": "delete-registered-domain", + "parameters": [ + { + "in": "path", + "name": "teamId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Deleted" + }, + "402": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 402, + "label": "domain-registration-update-payment-required", + "message": "Domain registration updated payment required" + }, + "properties": { + "code": { + "enum": [ + 402 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-registration-update-payment-required" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Domain registration updated payment required (label: `domain-registration-update-payment-required`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-forbidden-for-domain-registration-state", + "message": "Invalid domain registration state update" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-forbidden-for-domain-registration-state" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid domain registration state update (label: `operation-forbidden-for-domain-registration-state`)" + } + }, + "summary": "Delete a registered domain" + } + }, + "/teams/{tid}": { + "delete": { + "description": " [internal route ID: \"delete-team\"]\n\n", + "operationId": "delete-team", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamDeleteData" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "Team is scheduled for removal" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Please try again later." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Please try again later. (label: `too-many-requests`)" + }, + "503": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "The delete queue is full; no further delete requests can be processed at the moment (label: `queue-full`)" + } + }, + "summary": "Delete a team" + }, + "get": { + "description": " [internal route ID: \"get-team\"]\n\n", + "operationId": "get-team", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Team" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get a team by ID" + }, + "put": { + "description": " [internal route ID: \"update-team\"]\n\n", + "operationId": "update-team", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamUpdateData" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Team updated" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Insufficient permissions (missing SetTeamData) (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Update team properties" + } + }, + "/teams/{tid}/apps": { + "post": { + "description": " [internal route ID: \"create-app\"]\n\n", + "operationId": "create-app", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewApp" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreatedApp" + } + } + }, + "description": "" + } + }, + "summary": "Create a new app" + } + }, + "/teams/{tid}/apps/{app}/cookies": { + "post": { + "description": " [internal route ID: \"refresh-app-cookie\"]\n\n", + "operationId": "refresh-app-cookie", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "app", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RefreshAppCookieResponse" + } + } + }, + "description": "" + } + }, + "summary": "Get a new app authentication token" + } + }, + "/teams/{tid}/apps/{uid}": { + "get": { + "description": " [internal route ID: \"get-app\"]\n\n", + "operationId": "get-app", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GetApp" + } + } + }, + "description": "" + } + }, + "summary": "Get app" + } + }, + "/teams/{tid}/channels/search": { + "get": { + "description": " [internal route ID: \"search-channels\"]\n\n", + "operationId": "search-channels", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Search string", + "in": "query", + "name": "q", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "sort_order", + "required": false, + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "in": "query", + "name": "page_size", + "required": false, + "schema": { + "description": "integer from [1..500]", + "type": "number" + } + }, + { + "description": "`name` of the last seen channel of the current page, used to get the next page.", + "in": "query", + "name": "last_seen_name", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "`id` of the last seen channel, used to get the next page, used as a tie breaker. **Must** be sent to get the next page.", + "in": "query", + "name": "last_seen_id", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "allowEmptyValue": true, + "in": "query", + "name": "discoverable", + "schema": { + "default": false, + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationPage" + } + } + }, + "description": "" + } + }, + "summary": "Search channels" + } + }, + "/teams/{tid}/collaborators": { + "get": { + "description": " [internal route ID: \"get-team-collaborators\"]\n\n", + "operationId": "get-team-collaborators", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/TeamCollaborator" + }, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/TeamCollaborator" + }, + "type": "array" + } + } + }, + "description": "Return collaborators" + } + }, + "summary": "Get all collaborators of the team." + }, + "post": { + "description": " [internal route ID: \"add-team-collaborator\"]\n\n", + "operationId": "add-team-collaborator", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewTeamCollaborator" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + } + }, + "summary": "Add a collaborator to the team." + } + }, + "/teams/{tid}/collaborators/{uid}": { + "delete": { + "description": " [internal route ID: \"remove-team-collaborator\"]\n\n", + "operationId": "remove-team-collaborator", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + } + }, + "summary": "Remove a collaborator from the team." + }, + "put": { + "description": " [internal route ID: \"update-team-collaborator\"]\n\n", + "operationId": "update-team-collaborator", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/CollaboratorPermission" + }, + "type": "array", + "uniqueItems": true + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + } + }, + "summary": "Update a collaborator permissions from the team." + } + }, + "/teams/{tid}/conversations": { + "get": { + "description": " [internal route ID: \"get-team-conversations\"]\n\n", + "operationId": "get-team-conversations", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamConversationList" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + } + }, + "summary": "Get team conversations" + } + }, + "/teams/{tid}/conversations/roles": { + "get": { + "description": " [internal route ID: \"get-team-conversation-roles\"]\n\n", + "operationId": "get-team-conversation-roles", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationRolesList" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Get existing roles available for the given team" + } + }, + "/teams/{tid}/conversations/{cid}": { + "delete": { + "description": " [internal route ID: \"delete-team-conversation\"]\n\n", + "operationId": "delete-team-conversation", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "cid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Conversation deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` or `cid` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Remove a team conversation" + }, + "get": { + "description": " [internal route ID: \"get-team-conversation\"]\n\n", + "operationId": "get-team-conversation", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "cid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamConversation" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` or `cid` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get one team conversation" + } + }, + "/teams/{tid}/features": { + "get": { + "description": " [internal route ID: \"get-all-feature-configs-for-team\"]\n\nGets feature configs for a team. User must be a member of the team and have permission to view team features.", + "operationId": "get-all-feature-configs-for-team", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AllTeamFeatures" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Gets feature configs for a team" + } + }, + "/teams/{tid}/features/allowedGlobalOperations": { + "get": { + "description": " [internal route ID: (\"get\", AllowedGlobalOperationsConfig)]\n\n", + "operationId": "get_AllowedGlobalOperationsConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AllowedGlobalOperationsConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for allowedGlobalOperations" + } + }, + "/teams/{tid}/features/appLock": { + "get": { + "description": " [internal route ID: (\"get\", AppLockConfigB)]\n\n", + "operationId": "get_AppLockConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AppLockConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for appLock" + }, + "put": { + "description": " [internal route ID: (\"put\", AppLockConfigB)]\n\n", + "operationId": "put_AppLockConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AppLockConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AppLockConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for appLock" + } + }, + "/teams/{tid}/features/apps": { + "get": { + "description": " [internal route ID: (\"get\", AppsConfig)]\n\n", + "operationId": "get_AppsConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AppsConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for apps" + } + }, + "/teams/{tid}/features/assetAuditLog": { + "get": { + "description": " [internal route ID: (\"get\", AssetAuditLogConfig)]\n\n", + "operationId": "get_AssetAuditLogConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AssetAuditLogConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for assetAuditLog" + } + }, + "/teams/{tid}/features/cells": { + "get": { + "description": " [internal route ID: (\"get\", CellsConfigB)]\n\n", + "operationId": "get_CellsConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CellsConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for cells" + }, + "put": { + "description": " [internal route ID: (\"put\", CellsConfigB)]\n\n", + "operationId": "put_CellsConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CellsConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CellsConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for cells" + } + }, + "/teams/{tid}/features/cellsInternal": { + "get": { + "description": " [internal route ID: (\"get\", CellsInternalConfigB)]\n\n", + "operationId": "get_CellsInternalConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CellsInternalConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for cellsInternal" + } + }, + "/teams/{tid}/features/channels": { + "get": { + "description": " [internal route ID: (\"get\", ChannelsConfigB)]\n\n", + "operationId": "get_ChannelsConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ChannelsConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for channels" + }, + "put": { + "description": " [internal route ID: (\"put\", ChannelsConfigB)]\n\n", + "operationId": "put_ChannelsConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ChannelsConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ChannelsConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for channels" + } + }, + "/teams/{tid}/features/chatBubbles": { + "get": { + "description": " [internal route ID: (\"get\", ChatBubblesConfig)]\n\n", + "operationId": "get_ChatBubblesConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ChatBubblesConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for chatBubbles" + } + }, + "/teams/{tid}/features/classifiedDomains": { + "get": { + "description": " [internal route ID: (\"get\", ClassifiedDomainsConfig)]\n\n", + "operationId": "get_ClassifiedDomainsConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClassifiedDomainsConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for classifiedDomains" + } + }, + "/teams/{tid}/features/conferenceCalling": { + "get": { + "description": " [internal route ID: (\"get\", ConferenceCallingConfigB)]\n\n", + "operationId": "get_ConferenceCallingConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConferenceCallingConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for conferenceCalling" + }, + "put": { + "description": " [internal route ID: (\"put\", ConferenceCallingConfigB)]\n\n", + "operationId": "put_ConferenceCallingConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConferenceCallingConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConferenceCallingConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for conferenceCalling" + } + }, + "/teams/{tid}/features/consumableNotifications": { + "get": { + "description": " [internal route ID: (\"get\", ConsumableNotificationsConfig)]\n\n", + "operationId": "get_ConsumableNotificationsConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConsumableNotificationsConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for consumableNotifications" + } + }, + "/teams/{tid}/features/conversationGuestLinks": { + "get": { + "description": " [internal route ID: (\"get\", GuestLinksConfig)]\n\n", + "operationId": "get_GuestLinksConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GuestLinksConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for conversationGuestLinks" + }, + "put": { + "description": " [internal route ID: (\"put\", GuestLinksConfig)]\n\n", + "operationId": "put_GuestLinksConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GuestLinksConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GuestLinksConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for conversationGuestLinks" + } + }, + "/teams/{tid}/features/digitalSignatures": { + "get": { + "description": " [internal route ID: (\"get\", DigitalSignaturesConfig)]\n\n", + "operationId": "get_DigitalSignaturesConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DigitalSignaturesConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for digitalSignatures" + } + }, + "/teams/{tid}/features/domainRegistration": { + "get": { + "description": " [internal route ID: (\"get\", DomainRegistrationConfig)]\n\n", + "operationId": "get_DomainRegistrationConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DomainRegistrationConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for domainRegistration" + } + }, + "/teams/{tid}/features/enforceFileDownloadLocation": { + "get": { + "description": " [internal route ID: (\"get\", EnforceFileDownloadLocationConfigB)]\n\n

Custom feature: only supported on some dedicated on-prem systems.

", + "operationId": "get_EnforceFileDownloadLocationConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/EnforceFileDownloadLocation.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for enforceFileDownloadLocation" + }, + "put": { + "description": " [internal route ID: (\"put\", EnforceFileDownloadLocationConfigB)]\n\n

Custom feature: only supported on some dedicated on-prem systems.

", + "operationId": "put_EnforceFileDownloadLocationConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/EnforceFileDownloadLocation.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/EnforceFileDownloadLocation.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for enforceFileDownloadLocation" + } + }, + "/teams/{tid}/features/exposeInvitationURLsToTeamAdmin": { + "get": { + "description": " [internal route ID: (\"get\", ExposeInvitationURLsToTeamAdminConfig)]\n\n", + "operationId": "get_ExposeInvitationURLsToTeamAdminConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ExposeInvitationURLsToTeamAdminConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for exposeInvitationURLsToTeamAdmin" + }, + "put": { + "description": " [internal route ID: (\"put\", ExposeInvitationURLsToTeamAdminConfig)]\n\n", + "operationId": "put_ExposeInvitationURLsToTeamAdminConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ExposeInvitationURLsToTeamAdminConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ExposeInvitationURLsToTeamAdminConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for exposeInvitationURLsToTeamAdmin" + } + }, + "/teams/{tid}/features/fileSharing": { + "get": { + "description": " [internal route ID: (\"get\", FileSharingConfig)]\n\n", + "operationId": "get_FileSharingConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/FileSharingConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for fileSharing" + }, + "put": { + "description": " [internal route ID: (\"put\", FileSharingConfig)]\n\n", + "operationId": "put_FileSharingConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/FileSharingConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/FileSharingConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for fileSharing" + } + }, + "/teams/{tid}/features/legalhold": { + "get": { + "description": " [internal route ID: (\"get\", LegalholdConfig)]\n\n", + "operationId": "get_LegalholdConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LegalholdConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for legalhold" + }, + "put": { + "description": " [internal route ID: (\"put\", LegalholdConfig)]\n\n", + "operationId": "put_LegalholdConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LegalholdConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LegalholdConfig.LockableFeature" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-registered" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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", + "action-denied", + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "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\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`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)" + } + }, + "summary": "Put config for legalhold" + } + }, + "/teams/{tid}/features/limitedEventFanout": { + "get": { + "description": " [internal route ID: (\"get\", LimitedEventFanoutConfig)]\n\n", + "operationId": "get_LimitedEventFanoutConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LimitedEventFanoutConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for limitedEventFanout" + } + }, + "/teams/{tid}/features/meetings": { + "get": { + "description": " [internal route ID: (\"get\", MeetingsConfig)]\n\n", + "operationId": "get_MeetingsConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MeetingsConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for meetings" + }, + "put": { + "description": " [internal route ID: (\"put\", MeetingsConfig)]\n\n", + "operationId": "put_MeetingsConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MeetingsConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MeetingsConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for meetings" + } + }, + "/teams/{tid}/features/meetingsPremium": { + "get": { + "description": " [internal route ID: (\"get\", MeetingsPremiumConfig)]\n\n", + "operationId": "get_MeetingsPremiumConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MeetingsPremiumConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for meetingsPremium" + }, + "put": { + "description": " [internal route ID: (\"put\", MeetingsPremiumConfig)]\n\n", + "operationId": "put_MeetingsPremiumConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MeetingsPremiumConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MeetingsPremiumConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for meetingsPremium" + } + }, + "/teams/{tid}/features/mls": { + "get": { + "description": " [internal route ID: (\"get\", MLSConfigB)]\n\n", + "operationId": "get_MLSConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for mls" + }, + "put": { + "description": " [internal route ID: (\"put\", MLSConfigB)]\n\n", + "operationId": "put_MLSConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for mls" + } + }, + "/teams/{tid}/features/mlsE2EId": { + "get": { + "description": " [internal route ID: (\"get\", MlsE2EIdConfigB)]\n\n", + "operationId": "get_MlsE2EIdConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MlsE2EIdConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for mlsE2EId" + }, + "put": { + "description": " [internal route ID: (\"put\", MlsE2EIdConfigB)]\n\n", + "operationId": "put_MlsE2EIdConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MlsE2EIdConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MlsE2EIdConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for mlsE2EId" + } + }, + "/teams/{tid}/features/mlsMigration": { + "get": { + "description": " [internal route ID: (\"get\", MlsMigrationConfigB)]\n\n", + "operationId": "get_MlsMigrationConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MlsMigration.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for mlsMigration" + }, + "put": { + "description": " [internal route ID: (\"put\", MlsMigrationConfigB)]\n\n", + "operationId": "put_MlsMigrationConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MlsMigration.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MlsMigration.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for mlsMigration" + } + }, + "/teams/{tid}/features/outlookCalIntegration": { + "get": { + "description": " [internal route ID: (\"get\", OutlookCalIntegrationConfig)]\n\n", + "operationId": "get_OutlookCalIntegrationConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OutlookCalIntegrationConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for outlookCalIntegration" + }, + "put": { + "description": " [internal route ID: (\"put\", OutlookCalIntegrationConfig)]\n\n", + "operationId": "put_OutlookCalIntegrationConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OutlookCalIntegrationConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OutlookCalIntegrationConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for outlookCalIntegration" + } + }, + "/teams/{tid}/features/searchVisibility": { + "get": { + "description": " [internal route ID: (\"get\", SearchVisibilityAvailableConfig)]\n\n", + "operationId": "get_SearchVisibilityAvailableConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchVisibilityAvailableConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for searchVisibility" + }, + "put": { + "description": " [internal route ID: (\"put\", SearchVisibilityAvailableConfig)]\n\n", + "operationId": "put_SearchVisibilityAvailableConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchVisibilityAvailableConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchVisibilityAvailableConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for searchVisibility" + } + }, + "/teams/{tid}/features/searchVisibilityInbound": { + "get": { + "description": " [internal route ID: (\"get\", SearchVisibilityInboundConfig)]\n\n", + "operationId": "get_SearchVisibilityInboundConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchVisibilityInboundConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for searchVisibilityInbound" + }, + "put": { + "description": " [internal route ID: (\"put\", SearchVisibilityInboundConfig)]\n\n", + "operationId": "put_SearchVisibilityInboundConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchVisibilityInboundConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchVisibilityInboundConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for searchVisibilityInbound" + } + }, + "/teams/{tid}/features/selfDeletingMessages": { + "get": { + "description": " [internal route ID: (\"get\", SelfDeletingMessagesConfigB)]\n\n", + "operationId": "get_SelfDeletingMessagesConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SelfDeletingMessagesConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for selfDeletingMessages" + }, + "put": { + "description": " [internal route ID: (\"put\", SelfDeletingMessagesConfigB)]\n\n", + "operationId": "put_SelfDeletingMessagesConfigB", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SelfDeletingMessagesConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SelfDeletingMessagesConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for selfDeletingMessages" + } + }, + "/teams/{tid}/features/simplifiedUserConnectionRequestQRCode": { + "get": { + "description": " [internal route ID: (\"get\", SimplifiedUserConnectionRequestQRCodeConfig)]\n\n", + "operationId": "get_SimplifiedUserConnectionRequestQRCodeConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SimplifiedUserConnectionRequestQRCode.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for simplifiedUserConnectionRequestQRCode" + } + }, + "/teams/{tid}/features/sndFactorPasswordChallenge": { + "get": { + "description": " [internal route ID: (\"get\", SndFactorPasswordChallengeConfig)]\n\n", + "operationId": "get_SndFactorPasswordChallengeConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for sndFactorPasswordChallenge" + }, + "put": { + "description": " [internal route ID: (\"put\", SndFactorPasswordChallengeConfig)]\n\n", + "operationId": "put_SndFactorPasswordChallengeConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for sndFactorPasswordChallenge" + } + }, + "/teams/{tid}/features/sso": { + "get": { + "description": " [internal route ID: (\"get\", SSOConfig)]\n\n", + "operationId": "get_SSOConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SSOConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for sso" + } + }, + "/teams/{tid}/features/stealthUsers": { + "get": { + "description": " [internal route ID: (\"get\", StealthUsersConfig)]\n\n", + "operationId": "get_StealthUsersConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/StealthUsersConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for stealthUsers" + } + }, + "/teams/{tid}/features/validateSAMLemails": { + "get": { + "description": " [internal route ID: (\"get\", ValidateSAMLEmailsConfig)]\n\n", + "operationId": "get_ValidateSAMLEmailsConfig", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ValidateSAMLEmailsConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for validateSAMLemails" + } + }, + "/teams/{tid}/get-members-by-ids-using-post": { + "post": { + "description": " [internal route ID: \"get-team-members-by-ids\"]\n\nThe `has_more` field in the response body is always `false`.", + "operationId": "get-team-members-by-ids", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Maximum results to be returned", + "in": "query", + "name": "maxResults", + "required": false, + "schema": { + "format": "int32", + "maximum": 2000, + "minimum": 1, + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserIdList" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamMemberList" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "too-many-uids", + "message": "Can only process 2000 user ids per request." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-uids" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body` or `maxResults`\n\nCan only process 2000 user ids per request. (label: `too-many-uids`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Get team members by user id list" + } + }, + "/teams/{tid}/invitations": { + "get": { + "description": " [internal route ID: \"get-team-invitations\"]\n\n", + "operationId": "get-team-invitations", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Invitation id to start from (ascending).", + "in": "query", + "name": "start", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Number of results to return (default 100, max 500).", + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 500, + "minimum": 1, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvitationList" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/InvitationList" + } + } + }, + "description": "List of sent invitations" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Insufficient team permissions (label: `insufficient-permissions`)" + } + }, + "summary": "List the sent team invitations" + }, + "post": { + "description": " [internal route ID: \"send-team-invitation\"]\n\nInvitations are sent by email. The maximum allowed number of pending team invitations is equal to the team size.", + "operationId": "send-team-invitation", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/InvitationRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invitation" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Invitation" + } + } + }, + "description": "Invitation was created and sent.", + "headers": { + "Location": { + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "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" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid invitation code. (label: `invalid-invitation-code`)\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "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 email (label: `no-identity`)\n\nThis operation requires the user to have a verified email address. (label: `no-email`)" + } + }, + "summary": "Create and send a new team invitation." + } + }, + "/teams/{tid}/invitations/{iid}": { + "delete": { + "description": " [internal route ID: \"delete-team-invitation\"]\n\n", + "operationId": "delete-team-invitation", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "iid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Invitation deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Insufficient team permissions (label: `insufficient-permissions`)" + } + }, + "summary": "Delete a pending team invitation by ID." + }, + "get": { + "description": " [internal route ID: \"get-team-invitation\"]\n\n", + "operationId": "get-team-invitation", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "iid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invitation" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Invitation" + } + } + }, + "description": "Invitation" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Insufficient team permissions (label: `insufficient-permissions`)" + }, + "404": { + "content": { + "application/json": { + "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" + } + }, + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` or `iid` or Notification not found. (label: `not-found`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "duplicate-entry", + "message": "Entry already exists" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "duplicate-entry" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Entry already exists (label: `duplicate-entry`)" + } + }, + "summary": "Get a pending team invitation by ID." + } + }, + "/teams/{tid}/legalhold/consent": { + "post": { + "description": " [internal route ID: \"consent-to-legal-hold\"]\n\n", + "operationId": "consent-to-legal-hold", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Grant consent successful" + }, + "204": { + "description": "Consent already granted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam member not found (label: `no-team-member`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)" + } + }, + "summary": "Consent to legal hold" + } + }, + "/teams/{tid}/legalhold/settings": { + "delete": { + "description": " [internal route ID: \"delete-legal-hold-settings\"]\n\nThis 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)", + "operationId": "delete-legal-hold-settings", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RemoveLegalHoldSettingsRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Legal hold service settings deleted" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-registered" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Please try again later." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Please try again later. (label: `too-many-requests`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)" + } + }, + "summary": "Delete legal hold service settings" + }, + "get": { + "description": " [internal route ID: \"get-legal-hold-settings\"]\n\n", + "operationId": "get-legal-hold-settings", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ViewLegalHoldService" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Get legal hold service settings" + }, + "post": { + "description": " [internal route ID: \"create-legal-hold-settings\"]\n\n", + "operationId": "create-legal-hold-settings", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewLegalHoldService" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ViewLegalHoldService" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ViewLegalHoldService" + } + } + }, + "description": "Legal hold service settings created" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-status-bad", + "message": "legal hold service: invalid response" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-status-bad", + "legalhold-invalid-key" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nlegal hold service: invalid response (label: `legalhold-status-bad`)\n\nlegal hold service pubkey is invalid (label: `legalhold-invalid-key`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)" + } + }, + "summary": "Create legal hold service settings" + } + }, + "/teams/{tid}/legalhold/{uid}": { + "delete": { + "description": " [internal route ID: \"disable-legal-hold-for-user\"]\n\nThis 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)", + "operationId": "disable-legal-hold-for-user", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DisableLegalHoldForUserRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Disable legal hold successful" + }, + "204": { + "description": "Legal hold was not enabled" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-registered" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Please try again later." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Please try again later. (label: `too-many-requests`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)" + } + }, + "summary": "Disable legal hold for user" + }, + "get": { + "description": " [internal route ID: \"get-legal-hold\"]\n\n", + "operationId": "get-legal-hold", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserLegalHoldStatusResponse" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` or `uid` not found\n\nTeam member not found (label: `no-team-member`)" + } + }, + "summary": "Get legal hold status" + }, + "post": { + "description": " [internal route ID: \"request-legal-hold-device\"]\n\nThis 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)", + "operationId": "request-legal-hold-device", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Request device successful" + }, + "204": { + "description": "Request device already pending" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-registered", + "legalhold-status-bad" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold service has not been registered for this team (label: `legalhold-not-registered`)\n\nlegal hold service: invalid response (label: `legalhold-status-bad`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` or `uid` not found\n\nTeam member not found (label: `no-team-member`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "mls-legal-hold-not-allowed", + "message": "A user who is under legal-hold may not participate in MLS conversations" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-legal-hold-not-allowed", + "legalhold-no-consent", + "legalhold-already-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "A user who is under legal-hold may not participate in MLS conversations (label: `mls-legal-hold-not-allowed`)\n\nuser 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`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)" + } + }, + "summary": "Request legal hold device" + } + }, + "/teams/{tid}/legalhold/{uid}/approve": { + "put": { + "description": " [internal route ID: \"approve-legal-hold-device\"]\n\nThis 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)", + "operationId": "approve-legal-hold-device", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ApproveLegalHoldForUserRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Legal hold approved" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-registered" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` or `uid` not found\n\nno legal hold device is registered for this user. POST /teams/:tid/legalhold/:uid/ to start the flow. (label: `legalhold-no-device-allocated`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "legal hold is already enabled for this user (label: `legalhold-already-enabled`)" + }, + "412": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "legal hold cannot be approved without being in a pending state (label: `legalhold-not-pending`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Please try again later." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Please try again later. (label: `too-many-requests`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)" + } + }, + "summary": "Approve legal hold device" + } + }, + "/teams/{tid}/members": { + "get": { + "description": " [internal route ID: \"get-team-members\"]\n\n", + "operationId": "get-team-members", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Maximum results to be returned", + "in": "query", + "name": "maxResults", + "required": false, + "schema": { + "format": "int32", + "maximum": 2000, + "minimum": 1, + "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, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamMembersPage" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Get team members" + }, + "put": { + "description": " [internal route ID: \"update-team-member\"]\n\n", + "operationId": "update-team-member", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewTeamMember" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member", + "too-many-team-admins", + "invalid-permissions", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nMaximum number of admins per team reached (label: `too-many-team-admins`)\n\nThe specified permissions are invalid (label: `invalid-permissions`)\n\nYou do not have permission to access this resource (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam member not found (label: `no-team-member`)\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Update an existing team member" + } + }, + "/teams/{tid}/members/csv": { + "get": { + "description": " [internal route ID: \"get-team-members-csv\"]\n\nThe endpoint returns data in chunked transfer encoding. Internal server errors might result in a failed transfer instead of a 500 response.", + "operationId": "get-team-members-csv", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/csv": {} + }, + "description": "CSV of team members" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "You do not have permission to access this resource (label: `access-denied`)" + } + }, + "summary": "Get all members of the team as a CSV file" + } + }, + "/teams/{tid}/members/{uid}": { + "delete": { + "description": " [internal route ID: \"delete-team-member\"]\n\n", + "operationId": "delete-team-member", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamMemberDeleteData" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "202": { + "description": "Team member scheduled for deletion" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` or `uid` not found\n\nTeam not found (label: `no-team`)\n\nTeam member not found (label: `no-team-member`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Please try again later." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Please try again later. (label: `too-many-requests`)" + } + }, + "summary": "Remove an existing team member" + }, + "get": { + "description": " [internal route ID: \"get-team-member\"]\n\n", + "operationId": "get-team-member", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamMember" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` or `uid` not found\n\nTeam member not found (label: `no-team-member`)" + } + }, + "summary": "Get single team member" + } + }, + "/teams/{tid}/search": { + "get": { + "description": " [internal route ID: \"browse-team\"]\n\n", + "operationId": "browse-team", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Search expression", + "in": "query", + "name": "q", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Role filter, eg. `member,partner`. Empty list means do not filter.", + "in": "query", + "name": "frole", + "required": false, + "schema": { + "items": { + "enum": [ + "owner", + "admin", + "member", + "partner" + ], + "type": "string" + }, + "type": "array" + } + }, + { + "description": "Can be one of name, handle, email, saml_idp, managed_by, role, created_at.", + "in": "query", + "name": "sortby", + "required": false, + "schema": { + "enum": [ + "name", + "handle", + "email", + "saml_idp", + "managed_by", + "role", + "created_at" + ], + "type": "string" + } + }, + { + "description": "Can be one of asc, desc.", + "in": "query", + "name": "sortorder", + "required": false, + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "description": "Number of results to return (min: 1, max: 500, default: 15)", + "in": "query", + "name": "size", + "required": false, + "schema": { + "maximum": 500, + "minimum": 1, + "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, + "schema": { + "type": "string" + } + }, + { + "description": "Filter for (un-)verified email", + "in": "query", + "name": "email", + "required": false, + "schema": { + "enum": [ + "unverified", + "verified" + ], + "type": "string" + } + }, + { + "description": "Optional, return only non-searchable members when false.", + "in": "query", + "name": "searchable", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchResult_TeamContact" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchResult_TeamContact" + } + } + }, + "description": "Search results" + } + }, + "summary": "Browse team for members (requires add-user permission)" + } + }, + "/teams/{tid}/search-visibility": { + "get": { + "description": " [internal route ID: \"get-search-visibility\"]\n\n", + "operationId": "get-search-visibility", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamSearchVisibilityView" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Shows the value for search visibility" + }, + "put": { + "description": " [internal route ID: \"set-search-visibility\"]\n\n", + "operationId": "set-search-visibility", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamSearchVisibilityView" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Search visibility set" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "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`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Sets the search visibility for the whole team" + } + }, + "/teams/{tid}/size": { + "get": { + "description": " [internal route ID: \"get-team-size\"]\n\nCan be out of sync by roughly the `refresh_interval` of the ES index.", + "operationId": "get-team-size", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamSize" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamSize" + } + } + }, + "description": "Number of team members" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-invitation-code", + "message": "Invalid invitation code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-invitation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid invitation code. (label: `invalid-invitation-code`)" + } + }, + "summary": "Get the number of team members as an integer" + } + }, + "/time": { + "get": { + "description": " [internal route ID: \"get-server-time\"]\n\nReturns the current server time in UTC with seconds precision.", + "operationId": "get-server-time", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ServerTime" + } + } + }, + "description": "" + } + }, + "summary": "Get the current server time" + } + }, + "/upgrade-personal-to-team": { + "post": { + "description": " [internal route ID: \"upgrade-personal-to-team\"]\n\n", + "operationId": "upgrade-personal-to-team", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/BindingNewTeamUser" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUserTeam" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateUserTeam" + } + } + }, + "description": "Team created" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "user-already-in-a-team", + "message": "Switching teams is not allowed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-already-in-a-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "user-already-in-a-team", + "message": "Switching teams is not allowed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-already-in-a-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Switching teams is not allowed (label: `user-already-in-a-team`)" + }, + "404": { + "content": { + "application/json": { + "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" + } + }, + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "User not found (label: `not-found`)" + } + }, + "summary": "Upgrade personal user to team owner" + } + }, + "/user-groups": { + "get": { + "description": " [internal route ID: \"get-user-groups\"]\n\n", + "operationId": "get-user-groups", + "parameters": [ + { + "description": "Search string", + "in": "query", + "name": "q", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "sort_by", + "required": false, + "schema": { + "enum": [ + "name", + "created_at" + ], + "type": "string" + } + }, + { + "in": "query", + "name": "sort_order", + "required": false, + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "in": "query", + "name": "page_size", + "required": false, + "schema": { + "description": "integer from [1..500]", + "type": "number" + } + }, + { + "description": "`name` of the last seen user group, used to get the next page when sorting by name.", + "in": "query", + "name": "last_seen_name", + "required": false, + "schema": { + "maxLength": 4000, + "minLength": 1, + "type": "string" + } + }, + { + "description": "`created_at` field of the last seen user group, used to get the next page when sorting by created_at.", + "in": "query", + "name": "last_seen_created_at", + "required": false, + "schema": { + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string" + } + }, + { + "description": "`id` of the last seen group, used to get the next page. **Must** be sent to get the next page.", + "in": "query", + "name": "last_seen_id", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "allowEmptyValue": true, + "in": "query", + "name": "include_channels", + "schema": { + "default": false, + "type": "boolean" + } + }, + { + "allowEmptyValue": true, + "in": "query", + "name": "include_member_count", + "schema": { + "default": false, + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserGroupPage" + } + } + }, + "description": "" + } + }, + "summary": "Fetch groups accessible to the logged-in user" + }, + "post": { + "description": " [internal route ID: \"create-user-group\"]\n\n", + "operationId": "create-user-group", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewUserGroup" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserGroup" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "user-group-invalid", + "message": "Only team members of the same team can be added to a user group." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-invalid" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nOnly team members of the same team can be added to a user group. (label: `user-group-invalid`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-write-forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" + } + } + } + }, + "/user-groups/check-name": { + "post": { + "description": " [internal route ID: \"check-user-group-name-available\"]\n\n", + "operationId": "check-user-group-name-available", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CheckUserGroupName" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserGroupNameAvailability" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserGroupNameAvailability" + } + } + }, + "description": "OK" + } + }, + "summary": "[STUB] Check if a user group name is available" + } + }, + "/user-groups/{gid}": { + "delete": { + "description": " [internal route ID: \"delete-user-group\"]\n\n", + "operationId": "delete-user-group", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "User group deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-write-forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`gid` not found\n\nUser group not found (label: `user-group-not-found`)" + } + } + }, + "get": { + "description": " [internal route ID: \"get-user-group\"]\n\n", + "operationId": "get-user-group", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "allowEmptyValue": true, + "in": "query", + "name": "include_channels", + "schema": { + "default": false, + "type": "boolean" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserGroup" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserGroup" + } + } + }, + "description": "User Group Found" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`gid` or User group not found (label: `user-group-not-found`)\n\nUser group not found (label: `user-group-not-found`)" + } + }, + "summary": "Fetch a group accessible to the logged-in user" + }, + "put": { + "description": " [internal route ID: \"update-user-group\"]\n\n", + "operationId": "update-user-group", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserGroupUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User added updated" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-write-forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`gid` not found\n\nUser group not found (label: `user-group-not-found`)" + } + } + } + }, + "/user-groups/{gid}/channels": { + "put": { + "description": " [internal route ID: \"update-user-group-channels\"]\n\n", + "operationId": "update-user-group-channels", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "allowEmptyValue": true, + "in": "query", + "name": "append_only", + "schema": { + "default": false, + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateUserGroupChannels" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User group channels updated" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-write-forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`gid` not found\n\nUser group not found (label: `user-group-not-found`)" + } + }, + "summary": "Replaces the channels with the given list." + } + }, + "/user-groups/{gid}/users": { + "post": { + "description": " [internal route ID: \"add-users-to-group-bulk\"]\n\n", + "operationId": "add-users-to-group-bulk", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserGroupAddUsers" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Users added to group" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "user-group-invalid", + "message": "Only team members of the same team can be added to a user group." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-invalid" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nOnly team members of the same team can be added to a user group. (label: `user-group-invalid`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-write-forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`gid` not found\n\nUser group not found (label: `user-group-not-found`)" + } + } + }, + "put": { + "description": " [internal route ID: \"update-user-group-members\"]\n\n", + "operationId": "update-user-group-members", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateUserGroupMembers" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User group members updated" + } + }, + "summary": "[STUB] Update user group members. Replaces the users with the given list." + } + }, + "/user-groups/{gid}/users/{uid}": { + "delete": { + "description": " [internal route ID: \"remove-user-from-group\"]\n\n", + "operationId": "remove-user-from-group", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "User removed from group" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "user-group-invalid", + "message": "Only team members of the same team can be added to a user group." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-invalid" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team members of the same team can be added to a user group. (label: `user-group-invalid`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-write-forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`gid` or `uid` not found\n\nUser group not found (label: `user-group-not-found`)" + } + } + }, + "post": { + "description": " [internal route ID: \"add-user-to-group\"]\n\n", + "operationId": "add-user-to-group", + "parameters": [ + { + "in": "path", + "name": "gid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "User added to group" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "user-group-invalid", + "message": "Only team members of the same team can be added to a user group." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-invalid" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team members of the same team can be added to a user group. (label: `user-group-invalid`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "user-group-write-forbidden", + "message": "Only team admins can create, update, or delete user groups." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-write-forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Only team admins can create, update, or delete user groups. (label: `user-group-write-forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "user-group-not-found", + "message": "User group not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "user-group-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`gid` or `uid` not found\n\nUser group not found (label: `user-group-not-found`)" + } + } + } + }, + "/users/list-clients": { + "post": { + "description": " [internal route ID: \"list-clients-bulk@v2\"]\n\nIf a backend is unreachable, the clients from that backend will be omitted from the response", + "operationId": "list-clients-bulk@v2", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LimitedQualifiedUserIdList_500" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "qualified_user_map": { + "$ref": "#/components/schemas/QualifiedUserMap_Set_PubClient" + } + }, + "type": "object" + } + } + }, + "description": "" + } + }, + "summary": "List all clients for a set of user ids" + } + }, + "/users/list-prekeys": { + "post": { + "description": " [internal route ID: \"get-multi-user-prekey-bundle-qualified\"]\n\nYou can't request information for more users than maximum conversation size.", + "operationId": "get-multi-user-prekey-bundle-qualified", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QualifiedUserClients" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QualifiedUserClientPrekeyMapV4" + } + } + }, + "description": "" + } + }, + "summary": "(deprecated) Given a map of user IDs to client IDs return a prekey for each one." + } + }, + "/users/{uid_domain}/{uid}": { + "get": { + "description": " [internal route ID: \"get-user-qualified\"]\n\n", + "operationId": "get-user-qualified", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserProfile" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserProfile" + } + } + }, + "description": "User found" + }, + "404": { + "content": { + "application/json": { + "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" + } + }, + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "`uid_domain` or `uid` or User not found (label: `not-found`)" + } + }, + "summary": "Get a user by Domain and UserId" + } + }, + "/users/{uid_domain}/{uid}/clients": { + "get": { + "description": " [internal route ID: \"get-user-clients-qualified\"]\n\n", + "operationId": "get-user-clients-qualified", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/PubClient" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "summary": "Get all of a user's clients" + } + }, + "/users/{uid_domain}/{uid}/clients/{client}": { + "get": { + "description": " [internal route ID: \"get-user-client-qualified\"]\n\n", + "operationId": "get-user-client-qualified", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PubClient" + } + } + }, + "description": "" + } + }, + "summary": "Get a specific client of a user" + } + }, + "/users/{uid_domain}/{uid}/prekeys": { + "get": { + "description": " [internal route ID: \"get-users-prekey-bundle-qualified\"]\n\n", + "operationId": "get-users-prekey-bundle-qualified", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PrekeyBundle" + } + } + }, + "description": "" + } + }, + "summary": "Get a prekey for each client of a user." + } + }, + "/users/{uid_domain}/{uid}/prekeys/{client}": { + "get": { + "description": " [internal route ID: \"get-users-prekeys-client-qualified\"]\n\n", + "operationId": "get-users-prekeys-client-qualified", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientPrekey" + } + } + }, + "description": "" + } + }, + "summary": "Get a prekey for a specific client of a user." + } + }, + "/users/{uid_domain}/{uid}/supported-protocols": { + "get": { + "description": " [internal route ID: \"get-supported-protocols\"]\n\n", + "operationId": "get-supported-protocols", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/BaseProtocol" + }, + "type": "array", + "uniqueItems": true + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/BaseProtocol" + }, + "type": "array", + "uniqueItems": true + } + } + }, + "description": "Protocols supported by the user" + } + }, + "summary": "Get a user's supported protocols" + } + }, + "/users/{uid}/email": { + "put": { + "description": " [internal route ID: \"update-user-email\"]\n\nIf the user has a pending email validation, the validation email will be resent.", + "operationId": "update-user-email", + "parameters": [ + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/EmailUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "" + } + }, + "summary": "Resend email address validation email." + } + }, + "/users/{uid}/rich-info": { + "get": { + "description": " [internal route ID: \"get-rich-info\"]\n\n", + "operationId": "get-rich-info", + "parameters": [ + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RichInfoAssocList" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RichInfoAssocList" + } + } + }, + "description": "Rich info about the user" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "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" + } + } + }, + "description": "Insufficient team permissions (label: `insufficient-permissions`)" + } + }, + "summary": "Get a user's rich info" + } + }, + "/users/{uid}/searchable": { + "post": { + "description": " [internal route ID: \"set-user-searchable\"]\n\n", + "operationId": "set-user-searchable", + "parameters": [ + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SetSearchable" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "" + } + }, + "summary": "Set user's visibility in search" + } + }, + "/verification-code/send": { + "post": { + "description": " [internal route ID: \"send-verification-code\"]\n\n", + "operationId": "send-verification-code", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SendVerificationCode" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Verification code sent." + } + }, + "summary": "Send a verification code to a given email address." + } + } + }, + "security": [ + { + "ZAuth": [] + } + ], + "servers": [ + { + "url": "/v14" + } + ] +} diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index c64ccdbcb7e..51100f5867a 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -65,7 +65,6 @@ import Brig.IO.Intra (guardLegalhold) import Brig.IO.Intra qualified as Intra import Brig.Options qualified as Opt import Brig.Types.Intra -import Brig.Types.Team.LegalHold (LegalHoldClientRequest (..)) import Brig.User.Auth qualified as UserAuth import Brig.User.Auth.Cookie qualified as Auth import Cassandra (MonadClient) @@ -98,6 +97,7 @@ import Wire.API.MLS.Credential (ClientIdentity (..)) import Wire.API.MLS.Epoch (addToEpoch) import Wire.API.Message qualified as Message import Wire.API.Team.LegalHold (LegalholdProtectee (..)) +import Wire.API.Team.LegalHold.Internal import Wire.API.User import Wire.API.User qualified as Code import Wire.API.User.Client diff --git a/services/brig/src/Brig/API/Connection.hs b/services/brig/src/Brig/API/Connection.hs index b6426f86243..63a9526e42f 100644 --- a/services/brig/src/Brig/API/Connection.hs +++ b/services/brig/src/Brig/API/Connection.hs @@ -194,7 +194,10 @@ createConnectionToLocalUser self conn target = do change :: UserConnection -> RelationWithHistory -> ExceptT ConnectionError (AppT r) (ResponseForExistedCreated UserConnection) change c s = Existed <$> lift (wrapClient $ Data.updateConnection c s) --- | Throw error if one user has a LH device and the other status `no_consent` or vice versa. +-- | Guard local connection creation against legal-hold consent conflicts. +-- Rejects when one user is `no_consent` while the other has LH enabled. +-- See also: "Galley.API.LegalHold.Conflicts.guardLegalholdPolicyConflictsUid" +-- and "Galley.API.Action.checkLHPolicyConflictsLocal". -- -- 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. diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 43636d59dda..fc0d6a62e90 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -44,7 +44,6 @@ import Brig.Provider.API qualified as Provider import Brig.Team.API qualified as Team import Brig.Types.Connection import Brig.Types.Intra -import Brig.Types.Team.LegalHold (LegalHoldClientRequest (..)) import Brig.Types.User import Brig.User.EJPD qualified import Brig.User.Search.Index qualified as Search @@ -89,6 +88,7 @@ import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Named import Wire.API.Team.Export import Wire.API.Team.Feature +import Wire.API.Team.LegalHold.Internal import Wire.API.User import Wire.API.User.Activation import Wire.API.User.Client @@ -358,8 +358,7 @@ authAPI = ) federationRemotesAPI :: - ( Member FederationConfigStore r - ) => + (Member FederationConfigStore r) => ServerT BrigIRoutes.FederationRemotesAPI (Handler r) federationRemotesAPI = Named @"add-federation-remotes" addFederationRemote @@ -378,8 +377,7 @@ getFederationRemoteTeams domain = lift $ liftSem $ E.getFederationRemoteTeams domain addFederationRemoteTeam :: - ( Member FederationConfigStore r - ) => + (Member FederationConfigStore r) => Domain -> FederationRemoteTeam -> (Handler r) () @@ -398,8 +396,7 @@ getFederationRemotes :: (Member FederationConfigStore r) => (Handler r) Federati getFederationRemotes = lift $ liftSem $ E.getFederationConfigs addFederationRemote :: - ( Member FederationConfigStore r - ) => + (Member FederationConfigStore r) => FederationDomainConfig -> (Handler r) () addFederationRemote fedDomConf = do @@ -775,15 +772,13 @@ getActivationCode email = do maybe (throwStd activationKeyNotFound) (pure . GetActivationCodeResp) apair getPasswordResetCodeH :: - ( Member AuthenticationSubsystem r - ) => + (Member AuthenticationSubsystem r) => EmailAddress -> Handler r GetPasswordResetCodeResp getPasswordResetCodeH email = getPasswordResetCode email getPasswordResetCode :: - ( Member AuthenticationSubsystem r - ) => + (Member AuthenticationSubsystem r) => EmailAddress -> Handler r GetPasswordResetCodeResp getPasswordResetCode email = @@ -1003,8 +998,7 @@ getAccountsByInternalH getByData = do lift . liftSem $ getAccountsBy (qualifyAs loc getByData) createGroupInternalH :: - ( Member UserGroupSubsystem r - ) => + (Member UserGroupSubsystem r) => CreateGroupInternalRequest -> Handler r UserGroup createGroupInternalH req = @@ -1016,8 +1010,7 @@ createGroupInternalH req = req.newGroup getGroupInternalH :: - ( Member UserGroupSubsystem r - ) => + (Member UserGroupSubsystem r) => TeamId -> UserGroupId -> Bool -> @@ -1026,25 +1019,25 @@ getGroupInternalH tid uid includeChannels = lift . liftSem $ getGroupInternal tid uid includeChannels getGroupsInternalH :: - ( Member UserGroupSubsystem r - ) => + (Member UserGroupSubsystem r) => TeamId -> Maybe T.Text -> + Maybe ManagedBy -> + Word -> + Maybe Word -> Handler r UserGroupPageWithMembers -getGroupsInternalH tid nameContains = - lift . liftSem $ getGroupsInternal tid nameContains +getGroupsInternalH tid nameContains managedBy startIndex mbCount = + lift . liftSem $ getGroupsInternal tid nameContains managedBy startIndex mbCount updateGroupInternalH :: - ( Member UserGroupSubsystem r - ) => + (Member UserGroupSubsystem r) => UpdateGroupInternalRequest -> Handler r () updateGroupInternalH req = lift . liftSem $ resetUserGroupInternal req deleteGroupManagedInternalH :: - ( Member UserGroupSubsystem r - ) => + (Member UserGroupSubsystem r) => TeamId -> UserGroupId -> ManagedBy -> diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index ce0031a70ef..e4fbf8ed6c2 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -237,22 +237,23 @@ internalEndpointsSwaggerDocsAPIs = -- -- Dual to `internalEndpointsSwaggerDocsAPI`. versionedSwaggerDocsAPI :: Servant.Server VersionedSwaggerDocsAPI -versionedSwaggerDocsAPI (Just (VersionNumber V14)) = +versionedSwaggerDocsAPI (Just (VersionNumber V15)) = swaggerSchemaUIServer $ - ( serviceSwagger @VersionAPITag @'V14 - <> serviceSwagger @BrigAPITag @'V14 - <> serviceSwagger @GalleyAPITag @'V14 - <> serviceSwagger @SparAPITag @'V14 - <> serviceSwagger @CargoholdAPITag @'V14 - <> serviceSwagger @CannonAPITag @'V14 - <> serviceSwagger @GundeckAPITag @'V14 - <> serviceSwagger @ProxyAPITag @'V14 - <> serviceSwagger @OAuthAPITag @'V14 + ( serviceSwagger @VersionAPITag @'V15 + <> serviceSwagger @BrigAPITag @'V15 + <> serviceSwagger @GalleyAPITag @'V15 + <> serviceSwagger @SparAPITag @'V15 + <> serviceSwagger @CargoholdAPITag @'V15 + <> serviceSwagger @CannonAPITag @'V15 + <> serviceSwagger @GundeckAPITag @'V15 + <> serviceSwagger @ProxyAPITag @'V15 + <> serviceSwagger @OAuthAPITag @'V15 ) & S.info . S.title .~ "Wire-Server API" & S.info . S.description ?~ $((unTypeCode . embedText) =<< makeRelativeToProject "docs/swagger.md") - & S.servers .~ [S.Server ("/" <> toUrlPiece V14) Nothing mempty] + & S.servers .~ [S.Server ("/" <> toUrlPiece V15) Nothing mempty] & cleanupSwagger +versionedSwaggerDocsAPI (Just (VersionNumber V14)) = swaggerPregenUIServer $(pregenSwagger V14) versionedSwaggerDocsAPI (Just (VersionNumber V13)) = swaggerPregenUIServer $(pregenSwagger V13) versionedSwaggerDocsAPI (Just (VersionNumber V12)) = swaggerPregenUIServer $(pregenSwagger V12) versionedSwaggerDocsAPI (Just (VersionNumber V11)) = swaggerPregenUIServer $(pregenSwagger V11) @@ -309,7 +310,7 @@ versionedSwaggerDocsAPI Nothing = tocPage renderLink "swagger.json" ("/" <> v <> "/api/swagger.json"), "
" ] - | v <- versionToLByteString <$> [minBound :: Version ..] + | v <- versionToLByteString <$> [minBound :: Version ..] ] internal :: [LByteString] @@ -325,7 +326,7 @@ versionedSwaggerDocsAPI Nothing = tocPage renderLink "swagger.json" ("/api-internal/swagger-ui/" <> s <> "-swagger.json"), "
" ] - | s <- ["brig", "galley", "spar", "cargohold", "gundeck", "cannon", "proxy"] + | s <- ["brig", "galley", "spar", "cargohold", "gundeck", "cannon", "proxy"] ] federated :: [LByteString] @@ -338,10 +339,10 @@ versionedSwaggerDocsAPI Nothing = tocPage renderLink "swagger.json" ("/" <> v <> "/api-federation/swagger-ui/" <> s <> "-swagger.json"), "
" ] - | v <- versionToLByteString <$> [minBound :: Fed.Version ..] + | v <- versionToLByteString <$> [minBound :: Fed.Version ..] ] <> "
" - | s <- ["brig", "galley", "cargohold"] + | s <- ["brig", "galley", "cargohold"] ] versionToLByteString :: (ToHttpApiData v) => v -> LByteString @@ -631,6 +632,7 @@ servantSitemap = appsAPI :: ServerT AppsAPI (Handler r) appsAPI = Named @"create-app" createApp + :<|> Named @"get-app" getApp :<|> Named @"refresh-app-cookie" refreshAppCookie --------------------------------------------------------------------------- @@ -1248,8 +1250,7 @@ beginPasswordReset (Public.NewPasswordReset target) = lift (liftSem $ createPasswordResetCode $ mkEmailKey target) completePasswordReset :: - ( Member AuthenticationSubsystem r - ) => + (Member AuthenticationSubsystem r) => Public.CompletePasswordReset -> Handler r () completePasswordReset req = do @@ -1706,18 +1707,17 @@ getUserGroups :: Bool -> Bool -> Handler r UserGroupPage -getUserGroups lusr q sortBy sortOrder pageSize lastName lastCreatedAt lastId includeChannels includeMemberCount = +getUserGroups lusr searchString sortBy sortOrder pageSize lastName lastCreatedAt lastId includeChannels includeMemberCount = lift . liftSem $ - UserGroup.getGroups - (tUnqualified lusr) - UserGroup.GroupSearch - { query = q, - sortBy, - sortOrder, - pageSize, - lastName = fmap userGroupNameToText lastName, - lastCreatedAt = fmap fromUTCTimeMillis lastCreatedAt, - lastId, + UserGroup.getGroups (tUnqualified lusr) $ + UserGroupPageRequest + { pageSize = fromMaybe def pageSize, + managedByFilter = Nothing, + sortOrder = fromMaybe Desc sortOrder, + paginationState = case fromMaybe def sortBy of + SortByName -> PaginationSortByName $ (,) <$> fmap userGroupNameToText lastName <*> lastId + SortByCreatedAt -> PaginationSortByCreatedAt $ (,) <$> fmap fromUTCTimeMillis lastCreatedAt <*> lastId, + searchString, includeMemberCount, includeChannels } @@ -1753,6 +1753,9 @@ checkUserGroupNameAvailable _ _ = pure $ UserGroupNameAvailability True createApp :: (_) => Local UserId -> TeamId -> NewApp -> Handler r CreatedApp createApp lusr tid new = lift . liftSem $ AppSubsystem.createApp lusr tid new +getApp :: (_) => Local UserId -> TeamId -> UserId -> Handler r GetApp +getApp lusr tid uid = lift . liftSem $ AppSubsystem.getApp lusr tid uid + refreshAppCookie :: (_) => Local UserId -> TeamId -> UserId -> Handler r RefreshAppCookieResponse refreshAppCookie lusr tid appId = do mc <- lift . liftSem $ AppSubsystem.refreshAppCookie lusr tid appId @@ -1766,8 +1769,7 @@ deprecatedOnboarding :: UserId -> JsonValue -> (Handler r) DeprecatedMatchingRes deprecatedOnboarding _ _ = pure DeprecatedMatchingResult deprecatedCompletePasswordReset :: - ( Member AuthenticationSubsystem r - ) => + (Member AuthenticationSubsystem r) => Public.PasswordResetKey -> Public.PasswordReset -> (Handler r) () diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index b2efc000f97..7c0397cefee 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -397,8 +397,7 @@ createUser rateLimitKey new = do lift $ do mHashedPassword <- traverse - ( liftSem . HashPassword.hashPassword8 rateLimitKey - ) + (liftSem . HashPassword.hashPassword8 rateLimitKey) new'.newUserPassword newStoredUser new' {newUserPassword = mHashedPassword} mbInv tid mbHandle domain <- viewFederationDomain @@ -1101,8 +1100,7 @@ lookupActivationCode email = do pure $ (k,) <$> c lookupPasswordResetCode :: - ( Member AuthenticationSubsystem r - ) => + (Member AuthenticationSubsystem r) => EmailAddress -> (AppT r) (Maybe PasswordResetPair) lookupPasswordResetCode = diff --git a/services/brig/src/Brig/AWS.hs b/services/brig/src/Brig/AWS.hs index cce8eb2b8f0..5bcbf90ecd5 100644 --- a/services/brig/src/Brig/AWS.hs +++ b/services/brig/src/Brig/AWS.hs @@ -49,6 +49,7 @@ import Amazonka.SES qualified as SES import Amazonka.SQS qualified as SQS import Amazonka.SQS.Lens qualified as SQS import Brig.Options qualified as Opt +import Control.Exception.Lens import Control.Lens hiding ((.=)) import Control.Monad.Catch import Control.Monad.Trans.Resource @@ -67,7 +68,7 @@ import System.Logger.Class import UnliftIO.Async import UnliftIO.Exception import Util.Options -import Wire.AWS +import Wire.AWS (canRetry, sendCatch) data Env = Env { _logger :: !Logger, @@ -181,7 +182,7 @@ listen throttleMillis url callback = forever . handleAny unexpectedError $ do liftIO $ callback n for_ (m ^. SQS.message_receiptHandle) (void . send . SQS.newDeleteMessage url) unexpectedError x = do - err $ "error" .= show x ~~ msg (val "Failed to read or process message from SQS") + err $ "error" .= displayException x ~~ msg (val "Failed to read or process message from SQS") threadDelay 3000000 enqueueStandard :: Text -> BL.ByteString -> Amazon SQS.SendMessageResponse @@ -201,14 +202,14 @@ enqueueFIFO url group dedup m = retrying retry5x (const $ pure . canRetry) (cons -- Utilities send :: - (AWSRequest r, Typeable r, Typeable (AWSResponse r)) => + (AWSRequest r) => r -> Amazon (AWSResponse r) send r = throwA =<< sendCatchAmazon r -- | Temporary helper to translate polysemy to Amazon monad, it should go away -- with more polysemisation -sendCatchAmazon :: (AWSRequest req, Typeable req, Typeable (AWSResponse req)) => req -> Amazon (Either AWS.Error (AWS.AWSResponse req)) +sendCatchAmazon :: (AWSRequest req) => req -> Amazon (Either AWS.Error (AWS.AWSResponse req)) sendCatchAmazon req = do env <- view amazonkaEnv liftIO . runM . runInputConst env $ sendCatch req @@ -218,9 +219,7 @@ throwA = either (throwM . GeneralError) pure execCatch :: ( AWSRequest a, - Typeable a, MonadUnliftIO m, - Typeable (AWSResponse a), MonadCatch m ) => AWS.Env -> @@ -228,13 +227,11 @@ execCatch :: m (Either AWS.Error (AWSResponse a)) execCatch e cmd = runResourceT $ - AWS.trying AWS._Error $ + trying AWS._Error $ AWS.send e cmd exec :: ( AWSRequest a, - Typeable a, - Typeable (AWSResponse a), MonadCatch m, MonadIO m ) => diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 72b5eb6a152..13bef0bfbc6 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -392,7 +392,7 @@ runBrigToIO e (AppT ma) = do . emailSubsystemInterpreter e.userTemplates e.teamTemplates e.templateBranding . interpretAppStoreToPostgres . interpretTeamCollaboratorsStoreToPostgres - . intepreterTeamSubsystemToGalleyAPI + . interpretTeamSubsystemToGalleyAPI . interpretTeamCollaboratorsSubsystem . userSubsystemInterpreter . interpretUserGroupSubsystem diff --git a/services/brig/src/Brig/Data/Client.hs b/services/brig/src/Brig/Data/Client.hs index ce9640f9746..b94e0dd00df 100644 --- a/services/brig/src/Brig/Data/Client.hs +++ b/services/brig/src/Brig/Data/Client.hs @@ -55,7 +55,6 @@ import Amazonka.DynamoDB.Lens qualified as AWS import Bilge.Retry (httpHandlers) import Brig.AWS import Brig.App -import Brig.Types.Instances () import Cassandra as C hiding (Client) import Cassandra.Settings as C hiding (Client) import Control.Error @@ -118,8 +117,7 @@ reAuthForNewClients :: ReAuthPolicy reAuthForNewClients count upsert = count > 0 && not upsert addClient :: - ( Member AuthenticationSubsystem r - ) => + (Member AuthenticationSubsystem r) => Local UserId -> ClientId -> NewClient -> @@ -555,7 +553,7 @@ withOptLock u c ma = go (10 :: Int) Prom.incCounter optimisticLockFailedCounter execDyn :: forall r x. - (AWS.AWSRequest r, Typeable r, Typeable (AWS.AWSResponse r)) => + (AWS.AWSRequest r) => (AWS.AWSResponse r -> Maybe x) -> (Text -> r) -> m (Maybe x) @@ -566,7 +564,7 @@ withOptLock u c ma = go (10 :: Int) where execDyn' :: forall y p. - (AWS.AWSRequest p, Typeable (AWS.AWSResponse p), Typeable p) => + (AWS.AWSRequest p) => AWS.Env -> (AWS.AWSResponse p -> Maybe y) -> p -> diff --git a/services/brig/src/Brig/DeleteQueue/Interpreter.hs b/services/brig/src/Brig/DeleteQueue/Interpreter.hs index 8b78c9c5ef4..6c644839652 100644 --- a/services/brig/src/Brig/DeleteQueue/Interpreter.hs +++ b/services/brig/src/Brig/DeleteQueue/Interpreter.hs @@ -29,7 +29,7 @@ import Control.Lens import Data.Aeson import Data.ByteString.Base16 qualified as B16 import Data.ByteString.Lazy qualified as BL -import Data.Text as T +import Data.Text as T hiding (show) import Data.Text.Encoding qualified as T import Imports import OpenSSL.EVP.Digest hiding (digest) diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index b32f4b44863..603aeaa5e48 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -725,8 +725,7 @@ updateServiceWhitelist uid con tid upd = do .| C.mapM_ ( pooledMapConcurrentlyN_ 16 - ( uncurry (deleteBot uid (Just con)) - ) + (uncurry (deleteBot uid (Just con))) ) wrapClientE $ DB.deleteServiceWhitelist (Just tid) pid sid pure UpdateServiceWhitelistRespChanged @@ -852,8 +851,7 @@ addBot zuid zcon cid add = do bcl newClt maxPermClients - ( Just $ ClientCapabilityList $ Set.singleton Public.ClientSupportsLegalholdImplicitConsent - ) + (Just $ ClientCapabilityList $ Set.singleton Public.ClientSupportsLegalholdImplicitConsent) ) !>> const (StdError $ badGatewayWith "MalformedPrekeys") diff --git a/services/brig/src/Brig/Provider/DB.hs b/services/brig/src/Brig/Provider/DB.hs index 2f0aecc3703..5279fcd9923 100644 --- a/services/brig/src/Brig/Provider/DB.hs +++ b/services/brig/src/Brig/Provider/DB.hs @@ -17,7 +17,6 @@ module Brig.Provider.DB where -import Brig.Types.Instances () import Brig.Types.Provider.Tag import Cassandra as C import Control.Arrow ((&&&)) @@ -33,7 +32,7 @@ import UnliftIO (mapConcurrently) import Wire.API.Password as Password import Wire.API.Provider import Wire.API.Provider.Service hiding (updateServiceTags) -import Wire.API.Provider.Service.Tag +import Wire.API.Provider.Service.Tag (QueryAllTags (..), QueryAnyTags (..)) import Wire.API.User import Wire.UserKeyStore diff --git a/services/brig/src/Brig/Provider/RPC.hs b/services/brig/src/Brig/Provider/RPC.hs index f01d8cbcab9..c039bc7f5c9 100644 --- a/services/brig/src/Brig/Provider/RPC.hs +++ b/services/brig/src/Brig/Provider/RPC.hs @@ -46,6 +46,7 @@ import Imports import Network.HTTP.Client qualified as Http import Network.HTTP.Types.Method import Network.HTTP.Types.Status +import Network.Wai.Utilities.Exception import Ssl.Util (withVerifiedSslConnection) import System.Logger.Class (MonadLogger, field, msg, val, (~~)) import System.Logger.Class qualified as Log @@ -98,7 +99,7 @@ createBot scon new = do extReq scon ["bots"] . method POST . Bilge.json new - onExc ex = lift (extLogError scon ex) >> throwE (ServiceUnavailableWith $ displayException ex) + onExc ex = lift (extLogError scon ex) >> throwE (ServiceUnavailableWith $ displayExceptionNoBacktrace ex) extReq :: ServiceConn -> [ByteString] -> Request -> Request extReq scon ps = diff --git a/services/brig/src/Brig/Queue/Stomp.hs b/services/brig/src/Brig/Queue/Stomp.hs index 631f790013a..8fa9b04336a 100644 --- a/services/brig/src/Brig/Queue/Stomp.hs +++ b/services/brig/src/Brig/Queue/Stomp.hs @@ -34,7 +34,7 @@ import Control.Retry hiding (retryPolicy) import Data.Aeson as Aeson import Data.ByteString.Lazy qualified as BL import Data.Conduit.Network.TLS -import Data.Text +import Data.Text hiding (show) import Data.Text.Encoding import Network.Mom.Stompl.Client.Queue hiding (try) import System.Logger.Class as Log @@ -166,7 +166,7 @@ listen b q callback = Log.err $ msg (val "Exception when listening to a STOMP queue") ~~ field "queue" (show q) - ~~ field "error" (show e) + ~~ field "error" (displayException e) pure True -- Note [exception handling] diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index 77db6d20f27..5b5d0f96818 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -133,7 +133,7 @@ mkApp opts = do . requestIdMiddleware e.appLogger defaultRequestIdHeaderName . Metrics.servantPrometheusMiddleware (Proxy @ServantCombinedAPI) . GZip.gunzip - . GZip.gzip GZip.def + . GZip.gzip GZip.defaultGzipSettings . catchErrors e.appLogger defaultRequestIdHeaderName servantApp :: Env -> Wai.Application @@ -241,7 +241,7 @@ pendingActivationCleanup = do safeForever funName action = forever $ action `catchAny` \exc -> do - err $ "error" .= show exc ~~ msg (val $ UTF8.fromString funName <> " failed") + err $ "error" .= displayException exc ~~ msg (val $ UTF8.fromString funName <> " failed") -- pause to keep worst-case noise in logs manageable threadDelay 60_000_000 diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 2af1e1e3444..5b8022812ef 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -421,7 +421,7 @@ changeTeamAccountStatuses tid s = do team <- Team.tdTeam <$> lift (liftSem $ GalleyAPIAccess.getTeam tid) unless (team ^. teamBinding == Binding) $ throwStd noBindingTeam - uids <- toNonEmpty =<< lift (fmap (view Teams.userId) . view teamMembers <$> liftSem (TeamSubsystem.internalGetTeamMembers tid Nothing)) + uids <- toNonEmpty =<< lift (fmap (view Teams.userId) . view teamMembers <$> liftSem (TeamSubsystem.internalGetTeamMembersWithLimit tid Nothing)) API.changeAccountStatus uids s !>> accountStatusError where toNonEmpty (x : xs) = pure $ x :| xs diff --git a/services/brig/src/Brig/User/EJPD.hs b/services/brig/src/Brig/User/EJPD.hs index a44e2923d0b..a4ee60709e8 100644 --- a/services/brig/src/Brig/User/EJPD.hs +++ b/services/brig/src/Brig/User/EJPD.hs @@ -104,7 +104,7 @@ ejpdRequest (fromMaybe False -> includeContacts) (EJPDRequestBody handles) = do mbTeamContacts <- case (reallyIncludeContacts, userTeam target) of (True, Just tid) -> do - memberList <- liftSem $ TeamSubsystem.internalGetTeamMembers tid Nothing + memberList <- liftSem $ TeamSubsystem.internalGetTeamMembersWithLimit tid Nothing let members = (view Team.userId <$> (memberList ^. Team.teamMembers)) \\ [uid] contactsFull <- diff --git a/services/brig/test/integration/API/User/Auth.hs b/services/brig/test/integration/API/User/Auth.hs index b898f49bed6..e1c169b527e 100644 --- a/services/brig/test/integration/API/User/Auth.hs +++ b/services/brig/test/integration/API/User/Auth.hs @@ -401,8 +401,7 @@ testLoginFailure brig = do let badmail = unsafeEmailAddress "wrong" "wire.com" login brig - ( MkLogin (LoginByEmail badmail) defPassword Nothing Nothing - ) + (MkLogin (LoginByEmail badmail) defPassword Nothing Nothing) PersistentCookie !!! const 403 === statusCode diff --git a/services/brig/test/integration/Federation/End2end.hs b/services/brig/test/integration/Federation/End2end.hs index f01cb06cb71..a49dace7efe 100644 --- a/services/brig/test/integration/Federation/End2end.hs +++ b/services/brig/test/integration/Federation/End2end.hs @@ -461,8 +461,7 @@ testSendMessage brig1 brig2 galley2 cannon1 = do evtFrom e @?= EventFromUser (userQualifiedId bob) evtData e @?= EdOtrMessage - ( OtrMessage bobClient.clientId aliceClient.clientId (toBase64Text msgText) (Just "") - ) + (OtrMessage bobClient.clientId aliceClient.clientId (toBase64Text msgText) (Just "")) -- alice creates a conversation on domain 1 with bob on domain 2, then bob -- sends a message to alice @@ -524,8 +523,7 @@ testSendMessageToRemoteConv brig1 brig2 galley1 galley2 cannon1 = do evtFrom e @?= EventFromUser (userQualifiedId bob) evtData e @?= EdOtrMessage - ( OtrMessage bobClient.clientId aliceClient.clientId (toBase64Text msgText) (Just "") - ) + (OtrMessage bobClient.clientId aliceClient.clientId (toBase64Text msgText) (Just "")) testDeleteUser :: Brig -> Brig -> Galley -> Galley -> Cannon -> Http () testDeleteUser brig1 brig2 galley1 galley2 cannon1 = do diff --git a/services/cannon/src/Cannon/App.hs b/services/cannon/src/Cannon/App.hs index 2ad956a087c..ed7daa0784c 100644 --- a/services/cannon/src/Cannon/App.hs +++ b/services/cannon/src/Cannon/App.hs @@ -159,8 +159,8 @@ rejectOnError p x = do 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), - Handler $ \(x :: IOException) -> f (show x) + in [ Handler $ \(x :: HandshakeException) -> f (displayException x), + Handler $ \(x :: IOException) -> f (displayException x) ] ping :: Message diff --git a/services/cannon/src/Cannon/RabbitMqConsumerApp.hs b/services/cannon/src/Cannon/RabbitMqConsumerApp.hs index 16c3c0e53fd..13f7f1950cb 100644 --- a/services/cannon/src/Cannon/RabbitMqConsumerApp.hs +++ b/services/cannon/src/Cannon/RabbitMqConsumerApp.hs @@ -30,7 +30,7 @@ import Control.Lens hiding ((#)) import Control.Monad.Codensity import Data.Aeson hiding (Key) import Data.Id -import Data.Text +import Data.Text hiding (show) import Data.Text qualified as Text import Data.Text.Lazy qualified as TL import Data.Text.Lazy.Encoding qualified as TLE diff --git a/services/cannon/src/Cannon/Run.hs b/services/cannon/src/Cannon/Run.hs index 1583072882d..42faacdd3db 100644 --- a/services/cannon/src/Cannon/Run.hs +++ b/services/cannon/src/Cannon/Run.hs @@ -101,7 +101,7 @@ run o = lowerCodensity $ do . requestIdMiddleware g defaultRequestIdHeaderName . servantPrometheusMiddleware (Proxy @CombinedAPI) . otelMiddleWare - . Gzip.gzip Gzip.def + . Gzip.gzip Gzip.defaultGzipSettings . catchErrors g defaultRequestIdHeaderName app :: Application app = middleware (serve (Proxy @CombinedAPI) server) diff --git a/services/cargohold/src/CargoHold/AWS.hs b/services/cargohold/src/CargoHold/AWS.hs index 4984561ba43..83cbb168dc4 100644 --- a/services/cargohold/src/CargoHold/AWS.hs +++ b/services/cargohold/src/CargoHold/AWS.hs @@ -41,6 +41,7 @@ import CargoHold.API.Error import CargoHold.CloudFront import CargoHold.Options hiding (cloudFront, s3Bucket) import Conduit +import Control.Exception.Lens import Control.Lens hiding ((.=)) import Control.Monad.Catch import Control.Retry @@ -151,16 +152,14 @@ instance Exception Error -- Utilities sendCatch :: - (MonadCatch m, AWSRequest r, MonadResource m, Typeable r, Typeable (AWSResponse r)) => + (MonadCatch m, AWSRequest r, MonadResource m) => AWS.Env -> r -> m (Either AWS.Error (AWSResponse r)) -sendCatch env = AWS.trying AWS._Error . AWS.send env +sendCatch env = trying AWS._Error . AWS.send env exec :: ( AWSRequest r, - Typeable r, - Typeable (AWSResponse r), Show r, MonadLogger m, MonadIO m, @@ -176,7 +175,7 @@ exec env request = do Left err -> do Logger.info env.logger $ Log.field "remote" (Log.val "S3") - ~~ Log.msg (show err) + ~~ Log.msg (displayException err) ~~ Log.msg (show req) -- We re-throw the error, but distinguish between user errors and server -- errors. Logging it here also gives us the request that caused it. @@ -190,8 +189,6 @@ rethrowError e = case e of execStream :: ( AWSRequest r, - Typeable r, - Typeable (AWSResponse r), Show r ) => Env -> @@ -204,7 +201,7 @@ execStream env request = do Left err -> do Logger.info env.logger $ Log.field "remote" (Log.val "S3") - ~~ Log.msg (show err) + ~~ Log.msg (displayException err) ~~ Log.msg (show req) -- We just re-throw the error, but logging it here also gives us the request -- that caused it. @@ -213,8 +210,6 @@ execStream env request = do execCatch :: ( AWSRequest r, - Typeable r, - Typeable (AWSResponse r), Show r, MonadLogger m, MonadIO m @@ -229,7 +224,7 @@ execCatch env request = do Left err -> do Log.info $ Log.field "remote" (Log.val "S3") - ~~ Log.msg (show err) + ~~ Log.msg (displayException err) ~~ Log.msg (show req) pure Nothing Right r -> pure $ Just r diff --git a/services/cargohold/src/CargoHold/Run.hs b/services/cargohold/src/CargoHold/Run.hs index beeba087634..77eb3628cf4 100644 --- a/services/cargohold/src/CargoHold/Run.hs +++ b/services/cargohold/src/CargoHold/Run.hs @@ -94,7 +94,7 @@ mkApp o = Codensity $ \k -> versionMiddleware (foldMap expandVersionExp o.settings.disabledAPIVersions) . requestIdMiddleware e.appLogger defaultRequestIdHeaderName . servantPrometheusMiddleware (Proxy @CombinedAPI) - . GZip.gzip GZip.def + . GZip.gzip GZip.defaultGzipSettings . catchErrors e.appLogger defaultRequestIdHeaderName servantApp :: Env -> Application servantApp e0 r cont = do diff --git a/services/cargohold/src/CargoHold/S3.hs b/services/cargohold/src/CargoHold/S3.hs index 08bcdda6594..79126f484d3 100644 --- a/services/cargohold/src/CargoHold/S3.hs +++ b/services/cargohold/src/CargoHold/S3.hs @@ -427,8 +427,6 @@ octets = MIME.Type (MIME.Application "octet-stream") [] exec :: ( AWSRequest r, - Typeable r, - Typeable (AWSResponse r), Show r ) => (Text -> r) -> @@ -439,8 +437,6 @@ exec req = do execCatch :: ( AWSRequest r, - Typeable r, - Typeable (AWSResponse r), Show r ) => (Text -> r) -> diff --git a/services/federator/src/Federator/Interpreter.hs b/services/federator/src/Federator/Interpreter.hs index 31c6f913948..7d21e74380b 100644 --- a/services/federator/src/Federator/Interpreter.hs +++ b/services/federator/src/Federator/Interpreter.hs @@ -209,4 +209,4 @@ getFederationDomainConfigs env = do clientEnv = mkClientEnv mgr baseurl FedUp.getFederationDomainConfigs clientEnv >>= \case Right v -> pure v - Left e -> error $ show e + Left e -> error $ displayException e diff --git a/services/federator/src/Federator/MockServer.hs b/services/federator/src/Federator/MockServer.hs index f3a7eb05c6e..828a87bcb1a 100644 --- a/services/federator/src/Federator/MockServer.hs +++ b/services/federator/src/Federator/MockServer.hs @@ -137,7 +137,7 @@ mockInternalRequest :: mockInternalRequest remoteCalls mock targetDomain component (RPC path) req cont = do domainTxt <- note NoOriginDomain $ lookup originDomainHeaderName (Wai.requestHeaders req) originDomain <- parseDomain domainTxt - reqBody <- embed $ Wai.lazyRequestBody req + reqBody <- embed $ Wai.strictRequestBody req let fedRequest = ( FederatedRequest { frOriginDomain = originDomain, diff --git a/services/federator/src/Federator/Monitor/Internal.hs b/services/federator/src/Federator/Monitor/Internal.hs index 6f37abdc80f..d696c6e18e5 100644 --- a/services/federator/src/Federator/Monitor/Internal.hs +++ b/services/federator/src/Federator/Monitor/Internal.hs @@ -28,6 +28,7 @@ import Federator.Options (RunSettings (..)) import GHC.Foreign (peekCStringLen, withCStringLen) import GHC.IO.Encoding (getFileSystemEncoding) import Imports +import Network.Wai.Utilities.Exception import OpenSSL.Session (SSLContext) import OpenSSL.Session qualified as SSL import Polysemy (Embed, Member, Members, Sem, embed) @@ -332,7 +333,7 @@ showFederationSetupError (InvalidCAStore path msg) = "invalid CA store: " <> Tex 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 +showFederationSetupError (SSLException exc) = Text.pack $ "Unexpected SSL Exception: " <> displayExceptionNoBacktrace exc mkSSLContext :: ( Member (Embed IO) r, @@ -343,10 +344,10 @@ mkSSLContext :: mkSSLContext settings = do ctx <- mkSSLContextWithoutCert settings - Polysemy.fromExceptionVia @SomeException (InvalidClientCertificate . displayException) $ + Polysemy.fromExceptionVia @SomeException (InvalidClientCertificate . displayExceptionNoBacktrace) $ SSL.contextSetCertificateChainFile ctx (clientCertificate settings) - Polysemy.fromExceptionVia @SomeException (InvalidClientPrivateKey . displayException) $ + Polysemy.fromExceptionVia @SomeException (InvalidClientPrivateKey . displayExceptionNoBacktrace) $ SSL.contextSetPrivateKeyFile ctx (clientPrivateKey settings) privateKeyCheck <- Polysemy.fromExceptionVia @SSL.SomeSSLException SSLException $ SSL.contextCheckPrivateKey ctx @@ -378,7 +379,7 @@ mkSSLContextWithoutCert settings = do SSL.vpCallback = Nothing } forM_ (remoteCAStore settings) $ \caStorePath -> - Polysemy.fromExceptionVia @SomeException (InvalidCAStore caStorePath . displayException) $ + Polysemy.fromExceptionVia @SomeException (InvalidCAStore caStorePath . displayExceptionNoBacktrace) $ SSL.contextSetCAFile ctx caStorePath when (useSystemCAStore settings) $ diff --git a/services/federator/test/unit/Test/Federator/Client.hs b/services/federator/test/unit/Test/Federator/Client.hs index fa0ce039fb2..8bc976fe6a9 100644 --- a/services/federator/test/unit/Test/Federator/Client.hs +++ b/services/federator/test/unit/Test/Federator/Client.hs @@ -221,7 +221,7 @@ testClientConnectionError = do result <- runFederatorClient env (fedClient @'Brig @"get-user-by-handle" handle) case result of Left (FederatorClientHTTP2Error (FederatorClientConnectionError _)) -> pure () - Left x -> assertFailure $ "Expected connection error, got: " <> show x + Left x -> assertFailure $ "Expected connection error, got: " <> displayException x Right _ -> assertFailure "Expected connection with the server to fail" testResponseHeaders :: IO () diff --git a/services/federator/test/unit/Test/Federator/Options.hs b/services/federator/test/unit/Test/Federator/Options.hs index 137b6d411e5..ffb63d7977e 100644 --- a/services/federator/test/unit/Test/Federator/Options.hs +++ b/services/federator/test/unit/Test/Federator/Options.hs @@ -136,7 +136,7 @@ testSettings = Left e -> assertFailure $ "expected invalid client certificate exception, got: " - <> show e + <> displayException e Right _ -> assertFailure "expected failure for non-existing client certificate, got success", testCase "failToStartWithInvalidServerCredentials" failToStartWithInvalidServerCredentials, @@ -158,7 +158,7 @@ testSettings = Left e -> assertFailure $ "expected invalid client certificate exception, got: " - <> show e + <> displayException e Right _ -> assertFailure "expected failure for invalid private key, got success" ] @@ -184,7 +184,7 @@ failToStartWithInvalidServerCredentials = do Left e -> assertFailure $ "expected invalid client certificate exception, got: " - <> show e + <> displayException e Right _ -> assertFailure "expected failure for invalid client certificate, got success" diff --git a/services/galley/default.nix b/services/galley/default.nix index e16ec2e48dc..a6dbd9161ea 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -6,7 +6,6 @@ , aeson , aeson-qq , amazonka -, amazonka-sqs , amqp , asn1-encoding , asn1-types @@ -32,7 +31,6 @@ , currency-codes , data-default , data-timeout -, enclosed-exceptions , errors , exceptions , extended @@ -68,6 +66,7 @@ , pem , polysemy , polysemy-conc +, polysemy-plugin , polysemy-wire-zoo , process , prometheus-client @@ -77,7 +76,6 @@ , quickcheck-instances , random , raw-strings-qq -, resourcet , retry , safe-exceptions , servant @@ -101,7 +99,6 @@ , text , time , tinylog -, tls , transformers , types-common , types-common-aws @@ -135,13 +132,11 @@ mkDerivation { libraryHaskellDepends = [ aeson amazonka - amazonka-sqs amqp asn1-encoding asn1-types async base - base64-bytestring bilge brig-types bytestring @@ -153,10 +148,7 @@ mkDerivation { containers crypton crypton-x509 - currency-codes data-default - data-timeout - enclosed-exceptions errors exceptions extended @@ -181,11 +173,10 @@ mkDerivation { pem polysemy polysemy-conc + polysemy-plugin polysemy-wire-zoo prometheus-client - proto-lens raw-strings-qq - resourcet retry safe-exceptions servant @@ -201,11 +192,9 @@ mkDerivation { text time tinylog - tls transformers types-common types-common-aws - types-common-journal unliftio unordered-containers uri-bytestring diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 1207da8f177..8f8d45f27d6 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -136,13 +136,10 @@ library Galley.API.Update Galley.API.Util Galley.App - Galley.Aws Galley.Cassandra Galley.Cassandra.Client Galley.Cassandra.Code Galley.Cassandra.CustomBackend - Galley.Cassandra.LegalHold - Galley.Cassandra.Proposal Galley.Cassandra.Queries Galley.Cassandra.SearchVisibility Galley.Cassandra.Store @@ -157,23 +154,14 @@ library Galley.Effects.ClientStore Galley.Effects.CodeStore Galley.Effects.CustomBackendStore - Galley.Effects.FederatorAccess - Galley.Effects.LegalHoldStore - Galley.Effects.ProposalStore Galley.Effects.Queue Galley.Effects.SearchVisibilityStore - Galley.Effects.SparAccess Galley.Effects.TeamFeatureStore Galley.Effects.TeamMemberStore Galley.Effects.TeamNotificationStore - Galley.Effects.TeamStore Galley.Env Galley.External.LegalHoldService Galley.External.LegalHoldService.Internal - Galley.Intra.Effects - Galley.Intra.Federator - Galley.Intra.Journal - Galley.Intra.Spar Galley.Intra.Util Galley.Keys Galley.Monad @@ -182,6 +170,7 @@ library Galley.Run Galley.Schema.Run Galley.Schema.V100_OutOfSync + Galley.Schema.V101_ConversationLowerGCGracePeriod Galley.Schema.V20 Galley.Schema.V21 Galley.Schema.V22 @@ -262,23 +251,20 @@ library Galley.Schema.V97_CellsConversation Galley.Schema.V98_ChannelAddPermission Galley.Schema.V99_ConversationAddParent - Galley.TeamSubsystem Galley.Types.Clients Galley.Validation - ghc-options: + ghc-options: -fplugin=Polysemy.Plugin other-modules: Paths_galley hs-source-dirs: src build-depends: , aeson >=2.0.1.0 , amazonka >=1.4.5 - , amazonka-sqs >=1.4.5 , amqp , asn1-encoding , asn1-types , async >=2.0 , base >=4.6 && <5 - , base64-bytestring >=1.0 , bilge >=0.21.1 , brig-types >=0.73.1 , bytestring >=0.9 @@ -290,10 +276,7 @@ library , containers >=0.5 , crypton , crypton-x509 - , currency-codes >=2.0 , data-default - , data-timeout - , enclosed-exceptions >=1.0 , errors >=2.0 , exceptions >=0.4 , extended @@ -318,11 +301,10 @@ library , pem , polysemy , polysemy-conc + , polysemy-plugin , polysemy-wire-zoo , prometheus-client - , proto-lens >=0.2 , raw-strings-qq >=1.0 - , resourcet >=1.1 , retry >=0.5 , safe-exceptions >=0.1 , servant @@ -338,11 +320,9 @@ library , text >=0.11 , time >=1.4 , tinylog >=0.10 - , tls >=1.7.0 , transformers , types-common >=0.16 , types-common-aws - , types-common-journal >=0.1 , unliftio >=0.2 , unordered-containers , uri-bytestring >=0.2 diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index 5cc259464b6..f4dce07f1b7 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -163,6 +163,49 @@ settings: defaults: status: enabled lockStatus: unlocked + config: + channels: + enabled: true + default: enabled + groups: + enabled: true + default: enabled + one2one: + enabled: true + default: enabled + users: + externals: true + guests: false + collabora: + enabled: false + publicLinks: + enableFiles: true + enableFolders: true + enforcePassword: false + enforceExpirationMax: 0 + enforceExpirationDefault: 0 + storage: + perFileQuotaBytes: "100000000" + recycle: + autoPurgeDays: 30 + disable: false + allowSkip: false + metadata: + namespaces: + usermetaTags: + defaultValues: [] + allowFreeValues: true + cellsInternal: + defaults: + status: enabled + lockStatus: unlocked + config: + backend: + url: https://cells-beta.wire.com + collabora: + edition: COOL + storage: + perUserQuotaBytes: "1000000000000" allowedGlobalOperations: status: enabled config: @@ -185,6 +228,14 @@ settings: defaults: status: disabled lockStatus: locked + meetings: + defaults: + status: enabled + lockStatus: unlocked + meetingsPremium: + defaults: + status: disabled + lockStatus: locked logLevel: Warn logNetStrings: false diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index f096a394a86..a29eb18d893 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -78,11 +78,8 @@ import Galley.API.Util import Galley.Data.Scope (Scope (ReusableCode)) import Galley.Effects import Galley.Effects.CodeStore qualified as E -import Galley.Effects.FederatorAccess qualified as E -import Galley.Effects.ProposalStore qualified as E -import Galley.Effects.TeamStore qualified as E import Galley.Env (Env) -import Galley.Options +import Galley.Options (Opts) import Galley.Validation import Imports hiding ((\\)) import Polysemy @@ -106,6 +103,7 @@ import Wire.API.Federation.API import Wire.API.Federation.API.Brig import Wire.API.Federation.API.Galley import Wire.API.Federation.API.Galley qualified as F +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.FederationStatus import Wire.API.MLS.Group.Serialisation qualified as Serialisation @@ -120,18 +118,24 @@ import Wire.API.User as User import Wire.BrigAPIAccess qualified as E import Wire.ConversationStore qualified as E import Wire.ConversationSubsystem +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig (..)) +import Wire.FederationAPIAccess qualified as E import Wire.FireAndForget qualified as E import Wire.NotificationSubsystem +import Wire.ProposalStore qualified as E import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now import Wire.StoredConversation import Wire.StoredConversation qualified as Data import Wire.TeamCollaboratorsSubsystem +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem import Wire.UserList type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Constraint where HasConversationActionEffects 'ConversationJoinTag r = - ( Member BrigAPIAccess r, + ( -- TODO: Replace with subsystems + Member BrigAPIAccess r, Member (Error FederationError) r, Member (Error InternalError) r, Member (ErrorS 'NotATeamMember) r, @@ -147,10 +151,9 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con Member (Error NonFederatingBackends) r, Member (Error UnreachableBackends) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, - Member (Input Env) r, - Member (Input Opts) r, + Member (Input ConversationSubsystemConfig) r, Member Now r, Member LegalHoldStore r, Member ConversationStore r, @@ -165,10 +168,11 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con ( Member (Error InternalError) r, Member (Error NoChanges) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member Now r, Member (Input Env) r, + Member (Input ConversationSubsystemConfig) r, Member ProposalStore r, Member ConversationStore r, Member Random r, @@ -179,9 +183,10 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con Member ConversationStore r, Member ProposalStore r, Member (Input Env) r, + Member (Input ConversationSubsystemConfig) r, Member Now r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member (Error InternalError) r, Member Random r, @@ -198,7 +203,7 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS 'NotATeamMember) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member ProposalStore r, Member TeamStore r ) @@ -217,10 +222,11 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con Member (ErrorS 'InvalidTargetAccess) r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member FireAndForget r, Member NotificationSubsystem r, Member (Input Env) r, + Member (Input ConversationSubsystemConfig) r, Member ProposalStore r, Member TeamStore r, Member TinyLog r, @@ -244,7 +250,7 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con Member (Error NoChanges) r, Member BrigAPIAccess r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member (Input Env) r, Member (Input Opts) r, @@ -266,7 +272,7 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con Member (ErrorS InvalidOperation) r, Member ConversationStore r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ProposalStore r, Member Random r, @@ -362,21 +368,21 @@ type family HasConversationActionGalleyErrors (tag :: ConversationActionTag) :: enforceFederationProtocol :: ( Member (Error FederationError) r, - Member (Input Opts) r + Member (Input ConversationSubsystemConfig) r ) => ProtocolTag -> [Remote ()] -> Sem r () enforceFederationProtocol proto domains = do unless (null domains) $ do - mAllowedProtos <- view (settings . federationProtocols) <$> input + mAllowedProtos <- federationProtocols <$> input unless (maybe True (elem proto) mAllowedProtos) $ throw FederationDisabledForProtocol checkFederationStatus :: ( Member (Error UnreachableBackends) r, Member (Error NonFederatingBackends) r, - Member FederatorAccess r + Member (FederationAPIAccess FederatorClient) r ) => RemoteDomains -> Sem r () @@ -388,7 +394,7 @@ checkFederationStatus req = do getFederationStatus :: ( Member (Error UnreachableBackends) r, - Member FederatorAccess r + Member (FederationAPIAccess FederatorClient) r ) => RemoteDomains -> Sem r FederationStatus @@ -424,7 +430,8 @@ ensureAllowed :: ( IsConvMember mem, HasConversationActionEffects tag r, Member (ErrorS ConvNotFound) r, - Member (Error FederationError) r + Member (Error FederationError) r, + Member TeamSubsystem r ) => Sing tag -> Local x -> @@ -450,7 +457,7 @@ ensureAllowed tag loc action conv (ActorContext (Just origUser) mTm) = do SConversationDeleteTag -> for_ (convTeam conv) $ \tid -> do lusr <- ensureLocal loc (convMemberId loc origUser) - void $ E.getTeamMember tid (tUnqualified lusr) >>= noteS @'NotATeamMember + void $ TeamSubsystem.internalGetTeamMember (tUnqualified lusr) tid >>= noteS @'NotATeamMember SConversationAccessDataTag -> do -- 'PrivateAccessRole' is for self-conversations, 1:1 conversations and -- so on; users not supposed to be able to make other conversations @@ -501,7 +508,9 @@ performAction :: Member TeamCollaboratorsSubsystem r, Member (Error FederationError) r, Member ConversationSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Sing tag -> Qualified UserId -> @@ -556,7 +565,7 @@ performAction tag origUser lconv action = do pure $ mkPerformActionResult action SConversationRenameTag -> do - zusrMembership <- join <$> forM conv.metadata.cnvmTeam (flip E.getTeamMember (qUnqualified origUser)) + zusrMembership <- join <$> forM conv.metadata.cnvmTeam (TeamSubsystem.internalGetTeamMember (qUnqualified origUser)) for_ zusrMembership $ \tm -> unless (tm `hasPermission` ModifyConvName) $ throwS @'InvalidOperation cn <- rangeChecked (cupName action) E.setConversationName (tUnqualified lcnv) cn @@ -618,7 +627,8 @@ performConversationJoin :: ( HasConversationActionEffects 'ConversationJoinTag r, Member BackendNotificationQueueAccess r, Member ConversationSubsystem r, - Member TeamCollaboratorsSubsystem r + Member TeamCollaboratorsSubsystem r, + Member TeamSubsystem r ) => Qualified UserId -> Local StoredConversation -> @@ -651,7 +661,7 @@ performConversationJoin qusr lconv (ConversationJoin invited role joinType) = do then checkFederationStatus (RemoteDomains (invitedRemoteDomains <> existingRemoteDomains)) else -- even if there are no new remotes, we still need to check they are reachable void . (ensureNoUnreachableBackends =<<) $ - E.runFederatedConcurrentlyEither @_ @'Brig invitedRemoteUsers $ \_ -> + E.runFederatedConcurrentlyEither @_ @_ @'Brig invitedRemoteUsers $ \_ -> pure () conv :: StoredConversation @@ -665,7 +675,7 @@ performConversationJoin qusr lconv (ConversationJoin invited role joinType) = do checkLocals lusr (Just tid) newUsers = do tms <- Map.fromList . map (view Wire.API.Team.Member.userId &&& Imports.id) - <$> E.selectTeamMembers tid newUsers + <$> TeamSubsystem.internalSelectTeamMembers tid newUsers let userMembershipMap = map (Imports.id &&& flip Map.lookup tms) newUsers ensureAccessRole (convAccessRoles conv) userMembershipMap ensureConnectedToLocalsOrSameTeam lusr newUsers @@ -685,6 +695,14 @@ performConversationJoin qusr lconv (ConversationJoin invited role joinType) = do throw FederationNotConfigured ensureConnectedToRemotes lusr remotes + -- \| Guard conversation member additions against legal-hold consent conflicts: + -- - if any conv member has LH enabled then all new users must give consent + -- - if any new user has LH enabled then all new users must give consent + -- - if new users have LH enabled then + -- - ensure that a consented conv admin exists + -- - and kick all existing members that do not consent to LH from the conversation + -- See also: "Brig.API.Connection.checkLegalholdPolicyConflict" + -- and "Galley.API.LegalHold.Conflicts.guardLegalholdPolicyConflictsUid". checkLHPolicyConflictsLocal :: [UserId] -> Sem r () @@ -730,7 +748,7 @@ performConversationJoin qusr lconv (ConversationJoin invited role joinType) = do checkTeamMemberAddPermission lusr = do case conv.metadata.cnvmTeam of Just tid -> do - maybeTeamMember <- E.getTeamMember tid (tUnqualified lusr) + maybeTeamMember <- TeamSubsystem.internalGetTeamMember (tUnqualified lusr) tid case maybeTeamMember of Just tm -> do let isChannel = conv.metadata.cnvmGroupConvType == Just Channel @@ -764,7 +782,8 @@ performConversationAccessData :: ( HasConversationActionEffects 'ConversationAccessDataTag r, Member (Error FederationError) r, Member BackendNotificationQueueAccess r, - Member ConversationSubsystem r + Member ConversationSubsystem r, + Member TeamSubsystem r ) => Qualified UserId -> Local StoredConversation -> @@ -822,23 +841,23 @@ performConversationAccessData qusr lconv action = do -- FUTUREWORK: should we also remove non-activated remote users? pure $ bm {bmLocals = Set.fromList activated} - maybeRemoveNonTeamMembers :: (Member TeamStore r) => BotsAndMembers -> Sem r BotsAndMembers + maybeRemoveNonTeamMembers :: (Member TeamSubsystem r) => BotsAndMembers -> Sem r BotsAndMembers maybeRemoveNonTeamMembers bm = if Set.member NonTeamMemberAccessRole (cupAccessRoles action) then pure bm else case convTeam conv of Just tid -> do - onlyTeamUsers <- filterM (fmap isJust . E.getTeamMember tid) (toList (bmLocals bm)) + onlyTeamUsers <- filterM (fmap isJust . flip TeamSubsystem.internalGetTeamMember tid) (toList (bmLocals bm)) pure $ bm {bmLocals = Set.fromList onlyTeamUsers, bmRemotes = mempty} Nothing -> pure bm - maybeRemoveTeamMembers :: (Member TeamStore r) => BotsAndMembers -> Sem r BotsAndMembers + maybeRemoveTeamMembers :: (Member TeamSubsystem r) => BotsAndMembers -> Sem r BotsAndMembers maybeRemoveTeamMembers bm = if Set.member TeamMemberAccessRole (cupAccessRoles action) then pure bm else case convTeam conv of Just tid -> do - noTeamMembers <- filterM (fmap isNothing . E.getTeamMember tid) (toList (bmLocals bm)) + noTeamMembers <- filterM (fmap isNothing . flip TeamSubsystem.internalGetTeamMember tid) (toList (bmLocals bm)) pure $ bm {bmLocals = Set.fromList noTeamMembers} Nothing -> pure bm @@ -853,9 +872,10 @@ updateLocalConversation :: Member ConversationSubsystem r, HasConversationActionEffects tag r, SingI tag, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local ConvId -> Qualified UserId -> @@ -888,9 +908,10 @@ updateLocalConversationUnchecked :: Member (ErrorS 'InvalidOperation) r, Member ConversationSubsystem r, HasConversationActionEffects tag r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local StoredConversation -> Qualified UserId -> @@ -914,7 +935,7 @@ updateLocalConversationUnchecked lconv qusr con action = do par.extraConversationData where getTeamMembership :: StoredConversation -> Local UserId -> Sem r (Maybe TeamMember) - getTeamMembership conv luid = maybe (pure Nothing) (`E.getTeamMember` tUnqualified luid) conv.metadata.cnvmTeam + getTeamMembership conv luid = maybe (pure Nothing) (TeamSubsystem.internalGetTeamMember (tUnqualified luid)) conv.metadata.cnvmTeam ensureConversationActionAllowed :: Sing tag -> Local x -> StoredConversation -> Maybe TeamMember -> Sem r () ensureConversationActionAllowed tag loc conv mTeamMember = do @@ -1114,7 +1135,7 @@ notifyTypingIndicator :: ( Member Now r, Member (Input (Local ())) r, Member NotificationSubsystem r, - Member FederatorAccess r + Member (FederationAPIAccess FederatorClient) r ) => StoredConversation -> Qualified UserId -> diff --git a/services/galley/src/Galley/API/Action/Kick.hs b/services/galley/src/Galley/API/Action/Kick.hs index b9527adb718..4224dfe4a02 100644 --- a/services/galley/src/Galley/API/Action/Kick.hs +++ b/services/galley/src/Galley/API/Action/Kick.hs @@ -25,7 +25,6 @@ import Galley.API.Action.Leave import Galley.API.Action.Notify import Galley.API.Util import Galley.Effects -import Galley.Env (Env) import Imports hiding ((\\)) import Polysemy import Polysemy.Error @@ -36,6 +35,7 @@ import Wire.API.Conversation.Action import Wire.API.Event.LeaveReason import Wire.API.Federation.Error import Wire.ConversationSubsystem +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.StoredConversation @@ -53,7 +53,7 @@ kickMember :: Member NotificationSubsystem r, Member ProposalStore r, Member Now r, - Member (Input Env) r, + Member (Input ConversationSubsystemConfig) r, Member ConversationStore r, Member TinyLog r, Member Random r diff --git a/services/galley/src/Galley/API/Action/Leave.hs b/services/galley/src/Galley/API/Action/Leave.hs index 4e4001c7499..8d717b9cccf 100644 --- a/services/galley/src/Galley/API/Action/Leave.hs +++ b/services/galley/src/Galley/API/Action/Leave.hs @@ -23,13 +23,13 @@ import Data.Qualified import Galley.API.MLS.Removal import Galley.API.Util import Galley.Effects -import Galley.Env (Env) import Imports hiding ((\\)) import Polysemy import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog import Wire.API.Federation.Error +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.StoredConversation @@ -44,7 +44,7 @@ leaveConversation :: Member NotificationSubsystem r, Member ProposalStore r, Member Random r, - Member (Input Env) r, + Member (Input ConversationSubsystemConfig) r, Member Now r ) => Qualified UserId -> diff --git a/services/galley/src/Galley/API/Action/Notify.hs b/services/galley/src/Galley/API/Action/Notify.hs index edcb90cfc03..2e65778f8bf 100644 --- a/services/galley/src/Galley/API/Action/Notify.hs +++ b/services/galley/src/Galley/API/Action/Notify.hs @@ -33,8 +33,7 @@ import Wire.StoredConversation sendConversationActionNotifications :: forall tag r. - ( Member ConversationSubsystem r - ) => + (Member ConversationSubsystem r) => Sing tag -> Qualified UserId -> Bool -> diff --git a/services/galley/src/Galley/API/Action/Reset.hs b/services/galley/src/Galley/API/Action/Reset.hs index c1ca56064e1..b483f293044 100644 --- a/services/galley/src/Galley/API/Action/Reset.hs +++ b/services/galley/src/Galley/API/Action/Reset.hs @@ -26,8 +26,6 @@ import Galley.API.Action.Kick import Galley.API.MLS.Util import Galley.API.Util import Galley.Effects -import Galley.Effects.FederatorAccess -import Galley.Env import Imports import Polysemy import Polysemy.Error @@ -40,6 +38,7 @@ import Wire.API.Conversation.Protocol import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Federation.API +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.Federation.Version import Wire.API.MLS.Group.Serialisation as GroupId @@ -49,18 +48,19 @@ import Wire.API.Routes.Public.Galley.MLS import Wire.API.VersionInfo import Wire.ConversationStore import Wire.ConversationSubsystem +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) +import Wire.FederationAPIAccess import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.StoredConversation as Data resetLocalMLSMainConversation :: - ( Member (Input Env) r, - Member Now r, + ( Member Now r, Member (ErrorS MLSStaleMessage) r, Member (ErrorS ConvNotFound) r, Member (ErrorS InvalidOperation) r, Member BackendNotificationQueueAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member ExternalAccess r, Member ConversationSubsystem r, Member NotificationSubsystem r, @@ -69,7 +69,8 @@ resetLocalMLSMainConversation :: Member Resource r, Member ConversationStore r, Member P.TinyLog r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member (Input ConversationSubsystemConfig) r ) => Qualified UserId -> Local StoredConversation -> @@ -106,7 +107,7 @@ resetLocalMLSMainConversation qusr lcnv reset = do let remoteUsers = map (.id_) cnv.remoteMembers let targets = convBotsAndMembers cnv results <- - runFederatedConcurrentlyEither @_ @Brig remoteUsers $ + runFederatedConcurrentlyEither @_ @_ @Brig remoteUsers $ \_ -> do guardVersion $ \fedV -> fedV >= groupIdFedVersion GroupIdVersion2 let kick qvictim = do diff --git a/services/galley/src/Galley/API/Clients.hs b/services/galley/src/Galley/API/Clients.hs index 1fda288d627..60dae17eaaf 100644 --- a/services/galley/src/Galley/API/Clients.hs +++ b/services/galley/src/Galley/API/Clients.hs @@ -47,6 +47,7 @@ import Wire.API.Federation.Error import Wire.API.Routes.MultiTablePaging import Wire.BackendNotificationQueueAccess import Wire.ConversationStore (getConversation) +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) import Wire.NotificationSubsystem import Wire.Sem.Now (Now) @@ -75,7 +76,8 @@ rmClient :: Member (Error InternalError) r, Member ProposalStore r, Member Random r, - Member P.TinyLog r + Member P.TinyLog r, + Member (Input ConversationSubsystemConfig) r ) => UserId -> ClientId -> diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index 9e14293b642..6de6c71e7c3 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -51,9 +51,7 @@ import Galley.API.Teams.Features.Get (getFeatureForTeam) import Galley.API.Util import Galley.App (Env) import Galley.Effects -import Galley.Effects.FederatorAccess qualified as E -import Galley.Effects.TeamStore qualified as E -import Galley.Options +import Galley.Options (Opts) import Galley.Types.Teams (notTeamMember) import Galley.Validation import Imports hiding ((\\)) @@ -68,6 +66,7 @@ import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Event.Conversation +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.FederationStatus import Wire.API.Push.V2 qualified as PushV2 @@ -83,6 +82,8 @@ import Wire.API.Team.Permission hiding (self) import Wire.API.User import Wire.BrigAPIAccess import Wire.ConversationStore qualified as E +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) +import Wire.FederationAPIAccess qualified as E import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now @@ -90,6 +91,9 @@ import Wire.Sem.Random qualified as Random import Wire.StoredConversation hiding (convTeam, localOne2OneConvId) import Wire.StoredConversation qualified as Data import Wire.TeamCollaboratorsSubsystem +import Wire.TeamStore qualified as TeamStore +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem import Wire.UserList ---------------------------------------------------------------------------- @@ -114,7 +118,7 @@ createGroupConversationUpToV3 :: Member (ErrorS ChannelsNotEnabled) r, Member (ErrorS NotAnMlsConversation) r, Member (Error UnreachableBackendsLegacy) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member (Input Env) r, Member (Input Opts) r, @@ -124,7 +128,9 @@ createGroupConversationUpToV3 :: Member P.TinyLog r, Member TeamFeatureStore r, Member TeamCollaboratorsSubsystem r, - Member Random r + Member Random r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> Maybe ConnId -> @@ -160,17 +166,19 @@ createGroupOwnConversation :: Member (ErrorS ChannelsNotEnabled) r, Member (ErrorS NotAnMlsConversation) r, Member (Error UnreachableBackends) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member (Input Env) r, Member (Input Opts) r, + Member (Input ConversationSubsystemConfig) r, Member Now r, Member LegalHoldStore r, Member TeamStore r, Member P.TinyLog r, Member TeamFeatureStore r, Member TeamCollaboratorsSubsystem r, - Member Random r + Member Random r, + Member TeamSubsystem r ) => Local UserId -> Maybe ConnId -> @@ -206,17 +214,19 @@ createGroupConversation :: Member (ErrorS ChannelsNotEnabled) r, Member (ErrorS NotAnMlsConversation) r, Member (Error UnreachableBackends) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member (Input Env) r, Member (Input Opts) r, + Member (Input ConversationSubsystemConfig) r, Member Now r, Member LegalHoldStore r, Member TeamStore r, Member P.TinyLog r, Member TeamFeatureStore r, Member TeamCollaboratorsSubsystem r, - Member Random r + Member Random r, + Member TeamSubsystem r ) => Local UserId -> Maybe ConnId -> @@ -254,7 +264,7 @@ createGroupConvAndMkResponse :: Member (Error InternalError) r, Member (Error InvalidInput) r, Member P.TinyLog r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member BackendNotificationQueueAccess r, Member BrigAPIAccess r, Member ConversationStore r, @@ -263,7 +273,9 @@ createGroupConvAndMkResponse :: Member TeamStore r, Member TeamFeatureStore r, Member TeamCollaboratorsSubsystem r, - Member Random r + Member Random r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> Maybe ConnId -> @@ -295,17 +307,19 @@ createGroupConversationGeneric :: Member (ErrorS ChannelsNotEnabled) r, Member (ErrorS NotAnMlsConversation) r, Member (Error UnreachableBackends) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member (Input Env) r, Member (Input Opts) r, + Member (Input ConversationSubsystemConfig) r, Member Now r, Member LegalHoldStore r, Member TeamStore r, Member P.TinyLog r, Member TeamFeatureStore r, Member TeamCollaboratorsSubsystem r, - Member Random r + Member Random r, + Member TeamSubsystem r ) => Local UserId -> Maybe ConnId -> @@ -347,9 +361,10 @@ createGroupConversationGeneric lusr conn newConv joinType = do ensureNoLegalholdConflicts :: ( Member (ErrorS 'MissingLegalholdConsent) r, - Member (Input Opts) r, + Member (Input ConversationSubsystemConfig) r, Member LegalHoldStore r, - Member TeamStore r + Member TeamStore r, + Member TeamSubsystem r ) => UserList UserId -> Sem r () @@ -370,7 +385,8 @@ checkCreateConvPermissions :: Member TeamStore r, Member (Input Opts) r, Member TeamFeatureStore r, - Member TeamCollaboratorsSubsystem r + Member TeamCollaboratorsSubsystem r, + Member TeamSubsystem r ) => Local UserId -> NewConv -> @@ -414,7 +430,7 @@ checkCreateConvPermissions lusr newConv (Just tinfo) allUsers = do when (length allUsers > 1 || newConv.newConvProtocol == BaseProtocolMLSTag) $ do void $ permissionCheck AddRemoveConvMember teamAssociation - convLocalMemberships <- mapM (E.getTeamMember convTeam) (ulLocals allUsers) + convLocalMemberships <- mapM (flip TeamSubsystem.internalGetTeamMember convTeam) (ulLocals allUsers) ensureAccessRole (accessRoles newConv) (zip (ulLocals allUsers) convLocalMemberships) -- Team members are always considered to be connected, so we only check -- 'ensureConnected' for non-team-members. @@ -444,9 +460,9 @@ checkCreateConvPermissions lusr newConv (Just tinfo) allUsers = do ensureCreateChannelPermissions _ Nothing = do throwS @NotATeamMember -getTeamMember :: (Member TeamStore r) => UserId -> Maybe TeamId -> Sem r (Maybe TeamMember) -getTeamMember uid (Just tid) = E.getTeamMember tid uid -getTeamMember uid Nothing = E.getUserTeams uid >>= maybe (pure Nothing) (flip E.getTeamMember uid) . headMay +getTeamMember :: (Member TeamStore r, Member TeamSubsystem r) => UserId -> Maybe TeamId -> Sem r (Maybe TeamMember) +getTeamMember uid (Just tid) = TeamSubsystem.internalGetTeamMember uid tid +getTeamMember uid Nothing = TeamStore.getUserTeams uid >>= maybe (pure Nothing) (TeamSubsystem.internalGetTeamMember uid) . headMay ---------------------------------------------------------------------------- -- Other kinds of conversations @@ -491,12 +507,13 @@ createOne2OneConversation :: Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'NotConnected) r, Member (Error UnreachableBackendsLegacy) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member Now r, Member TeamStore r, Member P.TinyLog r, - Member TeamCollaboratorsSubsystem r + Member TeamCollaboratorsSubsystem r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -524,13 +541,13 @@ createOne2OneConversation lusr zcon j = where verifyMembership :: ( Member (ErrorS 'NoBindingTeamMembers) r, - Member TeamStore r + Member TeamSubsystem r ) => TeamId -> UserId -> Sem r () verifyMembership tid u = do - membership <- E.getTeamMember tid u + membership <- TeamSubsystem.internalGetTeamMember u tid when (isNothing membership) $ throwS @'NoBindingTeamMembers checkBindingTeamPermissions :: @@ -540,21 +557,22 @@ createOne2OneConversation lusr zcon j = Member (ErrorS OperationDenied) r, Member (ErrorS 'TeamNotFound) r, Member TeamCollaboratorsSubsystem r, - Member TeamStore r + Member TeamStore r, + Member TeamSubsystem r ) => Local UserId -> TeamId -> Sem r (Maybe TeamId) checkBindingTeamPermissions lother tid = do mTeamCollaborator <- internalGetTeamCollaborator tid (tUnqualified lusr) - zusrMembership <- E.getTeamMember tid (tUnqualified lusr) + zusrMembership <- TeamSubsystem.internalGetTeamMember (tUnqualified lusr) tid case (mTeamCollaborator, zusrMembership) of (Just collaborator, Nothing) -> guardPerm CollaboratorPermission.ImplicitConnection collaborator (Nothing, mbMember) -> void $ permissionCheck CreateConversation mbMember (Just collaborator, Just member) -> unless (hasPermission collaborator CollaboratorPermission.ImplicitConnection || hasPermission member CreateConversation) $ throwS @OperationDenied - E.getTeamBinding tid >>= \case + TeamStore.getTeamBinding tid >>= \case Just Binding -> do when (isJust zusrMembership) $ verifyMembership tid (tUnqualified lusr) @@ -576,7 +594,7 @@ createLegacyOne2OneConversationUnchecked :: Member (Error FederationError) r, Member (Error InternalError) r, Member (Error InvalidInput) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member Now r, Member P.TinyLog r @@ -620,7 +638,7 @@ createOne2OneConversationUnchecked :: Member (Error FederationError) r, Member (Error InternalError) r, Member (Error UnreachableBackends) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member Now r, Member P.TinyLog r @@ -645,7 +663,7 @@ createOne2OneConversationLocally :: Member (Error FederationError) r, Member (Error InternalError) r, Member (Error UnreachableBackends) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member Now r, Member P.TinyLog r @@ -700,7 +718,7 @@ createConnectConversation :: Member (Error InvalidInput) r, Member (ErrorS 'InvalidOperation) r, Member (Error UnreachableBackends) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member Now r, Member P.TinyLog r @@ -872,7 +890,7 @@ notifyCreatedConversation :: Member (Error FederationError) r, Member (Error InternalError) r, Member (Error UnreachableBackends) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member BackendNotificationQueueAccess r, Member Now r, diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index b80a3335035..5a5cd06db6b 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -57,6 +57,7 @@ import Galley.Effects import Galley.Options import Galley.Types.Conversations.One2One import Imports +import Network.Wai.Utilities.Exception import Polysemy import Polysemy.Error import Polysemy.Input @@ -77,6 +78,7 @@ import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Common (EmptyResponse (..)) import Wire.API.Federation.API.Galley hiding (id) +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Endpoint import Wire.API.Federation.Error import Wire.API.Federation.Version @@ -93,6 +95,7 @@ import Wire.API.ServantProto import Wire.API.User (BaseProtocolTag (..)) import Wire.ConversationStore qualified as E import Wire.ConversationSubsystem +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) import Wire.FireAndForget qualified as E import Wire.NotificationSubsystem import Wire.Sem.Now (Now) @@ -100,6 +103,7 @@ import Wire.Sem.Now qualified as Now import Wire.StoredConversation import Wire.StoredConversation qualified as Data import Wire.TeamCollaboratorsSubsystem +import Wire.TeamSubsystem (TeamSubsystem) import Wire.UserList (UserList (UserList)) type FederationAPI = "federation" :> FedApi 'Galley @@ -143,7 +147,8 @@ onClientRemoved :: Member Now r, Member ProposalStore r, Member Random r, - Member TinyLog r + Member TinyLog r, + Member (Input ConversationSubsystemConfig) r ) => Domain -> ClientRemovedRequest -> @@ -266,7 +271,7 @@ leaveConversation :: Member ConversationStore r, Member (Error InternalError) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member ConversationSubsystem r, Member NotificationSubsystem r, Member (Input Env) r, @@ -275,9 +280,10 @@ leaveConversation :: Member ProposalStore r, Member Random r, Member TinyLog r, - Member TeamStore r, + Member TeamSubsystem r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member (Input ConversationSubsystemConfig) r ) => Domain -> LeaveConversationRequest -> @@ -334,7 +340,7 @@ leaveConversation requestingDomain lc = do pure $ LeaveConversationResponse (Right ()) where - internalErr = InternalErrorWithDescription . LT.pack . displayException + internalErr = InternalErrorWithDescription . LT.pack . displayExceptionNoBacktrace -- FUTUREWORK: report errors to the originating backend -- FUTUREWORK: error handling for missing / mismatched clients @@ -392,14 +398,14 @@ sendMessage :: Member ClientStore r, Member ConversationStore r, Member (Error InvalidInput) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member BackendNotificationQueueAccess r, Member NotificationSubsystem r, Member (Input (Local ())) r, Member (Input Opts) r, Member Now r, Member ExternalAccess r, - Member TeamStore r, + Member TeamSubsystem r, Member P.TinyLog r ) => Domain -> @@ -423,10 +429,10 @@ onUserDeleted :: Member NotificationSubsystem r, Member (Input (Local ())) r, Member Now r, - Member (Input Env) r, Member ProposalStore r, Member Random r, - Member TinyLog r + Member TinyLog r, + Member (Input ConversationSubsystemConfig) r ) => Domain -> UserDeletedConversationsNotification -> @@ -480,7 +486,7 @@ updateConversation :: Member (Error FederationError) r, Member (Error InvalidInput) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member (Error InternalError) r, Member ConversationSubsystem r, Member NotificationSubsystem r, @@ -489,7 +495,7 @@ updateConversation :: Member Now r, Member LegalHoldStore r, Member ProposalStore r, - Member TeamStore r, + Member TeamSubsystem r, Member TinyLog r, Member Resource r, Member ConversationStore r, @@ -497,7 +503,9 @@ updateConversation :: Member TeamFeatureStore r, Member (Input (Local ())) r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamStore r, + Member (Input ConversationSubsystemConfig) r ) => Domain -> ConversationUpdateRequest -> @@ -619,7 +627,7 @@ sendMLSCommitBundle :: Member ExternalAccess r, Member (Error FederationError) r, Member (Error InternalError) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member ConversationSubsystem r, Member NotificationSubsystem r, Member (Input (Local ())) r, @@ -630,11 +638,13 @@ sendMLSCommitBundle :: Member TeamFeatureStore r, Member Resource r, Member TeamStore r, + Member TeamSubsystem r, Member P.TinyLog r, Member Random r, Member ProposalStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member (Input ConversationSubsystemConfig) r ) => Domain -> MLSMessageSendRequest -> @@ -679,17 +689,17 @@ sendMLSMessage :: Member ExternalAccess r, Member (Error FederationError) r, Member (Error InternalError) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member (Input (Local ())) r, Member (Input Env) r, Member (Input Opts) r, Member Now r, Member LegalHoldStore r, - Member TeamStore r, Member P.TinyLog r, Member ProposalStore r, - Member TeamCollaboratorsSubsystem r + Member TeamCollaboratorsSubsystem r, + Member TeamStore r ) => Domain -> MLSMessageSendRequest -> @@ -716,7 +726,7 @@ sendMLSMessage remoteDomain msr = handleMLSMessageErrors $ do getSubConversationForRemoteUser :: ( Member ConversationStore r, Member (Input (Local ())) r, - Member TeamStore r + Member TeamSubsystem r ) => Domain -> GetSubConversationsRequest -> @@ -735,8 +745,9 @@ leaveSubConversation :: Member (Error FederationError) r, Member (Input (Local ())) r, Member Resource r, - Member TeamStore r, - Member E.MLSCommitLockStore r + Member TeamSubsystem r, + Member E.MLSCommitLockStore r, + Member (Input ConversationSubsystemConfig) r ) => Domain -> LeaveSubConversationRequest -> @@ -757,7 +768,7 @@ deleteSubConversationForRemoteUser :: ( Member ConversationStore r, Member (Input (Local ())) r, Member Resource r, - Member TeamStore r, + Member TeamSubsystem r, Member E.MLSCommitLockStore r ) => Domain -> @@ -968,11 +979,11 @@ queryGroupInfo origDomain req = updateTypingIndicator :: ( Member NotificationSubsystem r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member ConversationStore r, Member Now r, Member (Input (Local ())) r, - Member TeamStore r + Member TeamSubsystem r ) => Domain -> TypingDataUpdateRequest -> @@ -990,8 +1001,7 @@ updateTypingIndicator origDomain TypingDataUpdateRequest {..} = do pure (either TypingDataUpdateError TypingDataUpdateSuccess ret) onTypingIndicatorUpdated :: - ( Member NotificationSubsystem r - ) => + (Member NotificationSubsystem r) => Domain -> TypingDataUpdated -> Sem r EmptyResponse diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 65f7f12e9de..e8140aa492b 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -56,9 +56,7 @@ import Galley.App import Galley.Effects import Galley.Effects.ClientStore import Galley.Effects.CustomBackendStore -import Galley.Effects.LegalHoldStore as LegalHoldStore -import Galley.Effects.TeamStore -import Galley.Effects.TeamStore qualified as E +import Galley.Env (FanoutLimit) import Galley.Monad import Galley.Options hiding (brig) import Galley.Queue qualified as Q @@ -94,6 +92,8 @@ import Wire.BackendNotificationQueueAccess import Wire.ConversationStore import Wire.ConversationStore qualified as E import Wire.ConversationSubsystem +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) +import Wire.LegalHoldStore as LegalHoldStore import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now @@ -102,6 +102,9 @@ import Wire.Sem.Paging.Cassandra import Wire.ServiceStore import Wire.StoredConversation import Wire.StoredConversation qualified as Data +import Wire.TeamStore +import Wire.TeamStore qualified as E +import Wire.TeamSubsystem (TeamSubsystem) import Wire.TeamSubsystem qualified as TeamSubsystem import Wire.UserList @@ -212,14 +215,16 @@ iTeamsAPI = mkAPI $ \tid -> hoistAPIHandler Imports.id (base tid) <@> mkNamedAPI @"update-team-status" (Teams.updateTeamStatus tid) <@> hoistAPISegment ( mkNamedAPI @"unchecked-add-team-member" (Teams.uncheckedAddTeamMember tid) - <@> mkNamedAPI @"unchecked-get-team-members" (TeamSubsystem.internalGetTeamMembers tid) + <@> mkNamedAPI @"unchecked-get-team-members" (TeamSubsystem.internalGetTeamMembersWithLimit tid) <@> mkNamedAPI @"unchecked-select-team-member-infos" (\userIds -> TeamSubsystem.internalSelectTeamMemberInfos tid (cUsers userIds)) + <@> mkNamedAPI @"unchecked-select-team-members" (\userIds -> TeamSubsystem.internalSelectTeamMembers tid (cUsers userIds)) <@> mkNamedAPI @"unchecked-get-team-member" (Teams.uncheckedGetTeamMember tid) <@> mkNamedAPI @"can-user-join-team" (Teams.canUserJoinTeam tid) <@> mkNamedAPI @"unchecked-update-team-member" (Teams.uncheckedUpdateTeamMember Nothing Nothing tid) <@> mkNamedAPI @"unchecked-get-team-admins" (TeamSubsystem.internalGetTeamAdmins tid) ) <@> mkNamedAPI @"user-is-team-owner" (Teams.userIsTeamOwner tid) + <@> mkNamedAPI @"finalize-delete-team" (\lusr mconn -> TeamSubsystem.internalFinalizeDeleteTeam lusr mconn tid $> NoContent) <@> hoistAPISegment ( mkNamedAPI @"get-search-visibility-internal" (Teams.getSearchVisibilityInternal tid) <@> mkNamedAPI @"set-search-visibility-internal" (Teams.setSearchVisibilityInternal (featureEnabledForTeam @SearchVisibilityAvailableConfig) tid) @@ -286,6 +291,9 @@ allFeaturesAPI = <@> featureAPI1Full <@> featureAPI1Get <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full featureAPI :: API IFeatureAPI GalleyEffects featureAPI = @@ -309,6 +317,8 @@ featureAPI = <@> mkNamedAPI @'("ilock", AppsConfig) (updateLockStatus @AppsConfig) <@> mkNamedAPI @'("ilock", SimplifiedUserConnectionRequestQRCodeConfig) (updateLockStatus @SimplifiedUserConnectionRequestQRCodeConfig) <@> mkNamedAPI @'("ilock", StealthUsersConfig) (updateLockStatus @StealthUsersConfig) + <@> mkNamedAPI @'("ilock", MeetingsConfig) (updateLockStatus @MeetingsConfig) + <@> mkNamedAPI @'("ilock", MeetingsPremiumConfig) (updateLockStatus @MeetingsPremiumConfig) -- all features <@> mkNamedAPI @"feature-configs-internal" (maybe getAllTeamFeaturesForServer getAllTeamFeaturesForUser) @@ -335,7 +345,10 @@ rmUser :: Member P.TinyLog r, Member Random r, Member TeamFeatureStore r, - Member TeamStore r + Member TeamStore r, + Member (Input FanoutLimit) r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> Maybe ConnId -> @@ -472,7 +485,7 @@ deleteLoop = do doDelete usr con tid = do lusr <- qualifyLocal usr - Teams.uncheckedDeleteTeam lusr con tid + TeamSubsystem.internalFinalizeDeleteTeam lusr con tid safeForever :: String -> App () -> App () safeForever funName action = @@ -484,10 +497,10 @@ safeForever funName action = guardLegalholdPolicyConflictsH :: ( Member BrigAPIAccess r, Member (Input Opts) r, - Member TeamStore r, Member P.TinyLog r, Member (ErrorS 'MissingLegalholdConsent) r, - Member (ErrorS 'MissingLegalholdConsentOldClients) r + Member (ErrorS 'MissingLegalholdConsentOldClients) r, + Member TeamSubsystem r ) => GuardLegalholdPolicyConflicts -> Sem r () @@ -499,8 +512,7 @@ guardLegalholdPolicyConflictsH glh = do -- | Get an MLS conversation client list iGetMLSClientListForConv :: forall r. - ( Member ConversationStore r - ) => + (Member ConversationStore r) => GroupId -> Sem r ClientList iGetMLSClientListForConv gid = do diff --git a/services/galley/src/Galley/API/LegalHold.hs b/services/galley/src/Galley/API/LegalHold.hs index 600784f8027..db530859c11 100644 --- a/services/galley/src/Galley/API/LegalHold.hs +++ b/services/galley/src/Galley/API/LegalHold.hs @@ -31,7 +31,6 @@ module Galley.API.LegalHold where import Brig.Types.Connection (UpdateConnectionsInternal (..)) -import Brig.Types.Team.LegalHold (legalHoldService, viewLegalHoldService) import Control.Exception (assert) import Control.Lens (view, (^.)) import Data.ByteString.Conversion (toByteString) @@ -50,9 +49,7 @@ import Galley.API.Update (removeMemberFromLocalConv) import Galley.API.Util import Galley.App import Galley.Effects -import Galley.Effects.LegalHoldStore qualified as LegalHoldData import Galley.Effects.TeamMemberStore -import Galley.Effects.TeamStore import Galley.External.LegalHoldService qualified as LHService import Galley.Types.Teams as Team import Imports @@ -67,19 +64,24 @@ import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.Provider.Service import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Public.Galley.LegalHold +import Wire.API.Team.Feature (LegalholdConfig) import Wire.API.Team.LegalHold import Wire.API.Team.LegalHold qualified as Public import Wire.API.Team.LegalHold.External hiding (userId) +import Wire.API.Team.LegalHold.Internal import Wire.API.Team.Member import Wire.API.User.Client.Prekey import Wire.BrigAPIAccess import Wire.ConversationStore import Wire.ConversationSubsystem +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) import Wire.FireAndForget +import Wire.LegalHoldStore qualified as LegalHoldData import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.Sem.Paging @@ -87,6 +89,9 @@ import Wire.Sem.Paging.Cassandra import Wire.StoredConversation import Wire.StoredConversation qualified as Data import Wire.TeamCollaboratorsSubsystem +import Wire.TeamStore +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem createSettings :: forall r. @@ -97,8 +102,9 @@ createSettings :: Member (ErrorS 'LegalHoldServiceBadResponse) r, Member LegalHoldStore r, Member TeamFeatureStore r, - Member TeamStore r, - Member P.TinyLog r + Member P.TinyLog r, + Member (Input (FeatureDefaults LegalholdConfig)) r, + Member TeamSubsystem r ) => Local UserId -> TeamId -> @@ -107,7 +113,7 @@ createSettings :: createSettings lzusr tid newService = do let zusr = tUnqualified lzusr assertLegalHoldEnabledForTeam tid - zusrMembership <- getTeamMember tid zusr + zusrMembership <- TeamSubsystem.internalGetTeamMember zusr tid -- let zothers = map (view userId) membs -- Log.debug $ -- Log.field "targets" (toByteString . show $ toByteString <$> zothers) @@ -126,14 +132,15 @@ getSettings :: ( Member (ErrorS 'NotATeamMember) r, Member LegalHoldStore r, Member TeamFeatureStore r, - Member TeamStore r + Member (Input (FeatureDefaults LegalholdConfig)) r, + Member TeamSubsystem r ) => Local UserId -> TeamId -> Sem r Public.ViewLegalHoldService getSettings lzusr tid = do let zusr = tUnqualified lzusr - zusrMembership <- getTeamMember tid zusr + zusrMembership <- TeamSubsystem.internalGetTeamMember zusr tid void $ maybe (throwS @'NotATeamMember) pure zusrMembership isenabled <- isLegalHoldEnabledForTeam tid mresult <- LegalHoldData.getSettings tid @@ -159,7 +166,7 @@ removeSettingsInternalPaging :: Member (ErrorS OperationDenied) r, Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member FireAndForget r, Member NotificationSubsystem r, Member ConversationSubsystem r, @@ -175,7 +182,10 @@ removeSettingsInternalPaging :: Member TeamStore r, Member (Embed IO) r, Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member (Input (FeatureDefaults LegalholdConfig)) r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> TeamId -> @@ -205,7 +215,7 @@ removeSettings :: Member (ErrorS OperationDenied) r, Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member FireAndForget r, Member NotificationSubsystem r, Member ConversationSubsystem r, @@ -218,7 +228,10 @@ removeSettings :: Member Random r, Member (Embed IO) r, Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member (Input (FeatureDefaults LegalholdConfig)) r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => UserId -> TeamId -> @@ -227,7 +240,7 @@ removeSettings :: removeSettings zusr tid (Public.RemoveLegalHoldSettingsRequest mPassword) = do assertNotWhitelisting assertLegalHoldEnabledForTeam tid - zusrMembership <- getTeamMember tid zusr + zusrMembership <- TeamSubsystem.internalGetTeamMember zusr tid -- let zothers = map (view userId) membs -- Log.debug $ -- Log.field "targets" (toByteString . show $ toByteString <$> zothers) @@ -238,7 +251,8 @@ removeSettings zusr tid (Public.RemoveLegalHoldSettingsRequest mPassword) = do where assertNotWhitelisting :: Sem r () assertNotWhitelisting = do - getLegalHoldFlag >>= \case + featureLegalHold <- input @(FeatureDefaults LegalholdConfig) + case featureLegalHold of FeatureLegalHoldDisabledPermanently -> pure () FeatureLegalHoldDisabledByDefault -> pure () FeatureLegalHoldWhitelistTeamsAndImplicitConsent -> @@ -259,7 +273,7 @@ removeSettings' :: Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member (ErrorS 'LegalHoldCouldNotBlockConnections) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member FireAndForget r, Member NotificationSubsystem r, Member ConversationSubsystem r, @@ -274,7 +288,9 @@ removeSettings' :: Member P.TinyLog r, Member (Embed IO) r, Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => TeamId -> Sem r () @@ -312,7 +328,7 @@ grantConsent :: Member (ErrorS 'TeamMemberNotFound) r, Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input Env) r, @@ -323,7 +339,9 @@ grantConsent :: Member Random r, Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> TeamId -> @@ -331,7 +349,7 @@ grantConsent :: grantConsent lusr tid = do userLHStatus <- noteS @'TeamMemberNotFound - =<< fmap (view legalHoldStatus) <$> getTeamMember tid (tUnqualified lusr) + =<< fmap (view legalHoldStatus) <$> TeamSubsystem.internalGetTeamMember (tUnqualified lusr) tid case userLHStatus of lhs@UserLegalHoldNoConsent -> changeLegalholdStatusAndHandlePolicyConflicts tid lusr lhs UserLegalHoldDisabled $> GrantConsentSuccess @@ -360,7 +378,7 @@ requestDevice :: Member (ErrorS 'UserLegalHoldAlreadyEnabled) r, Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input (Local ())) r, @@ -374,7 +392,10 @@ requestDevice :: Member TeamStore r, Member (Embed IO) r, Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member (Input (FeatureDefaults LegalholdConfig)) r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> TeamId -> @@ -387,9 +408,9 @@ requestDevice lzusr tid uid = do P.debug $ Log.field "targets" (toByteString (tUnqualified luid)) . Log.field "action" (Log.val "LegalHold.requestDevice") - zusrMembership <- getTeamMember tid zusr + zusrMembership <- TeamSubsystem.internalGetTeamMember zusr tid void $ permissionCheck ChangeLegalHoldUserSettings zusrMembership - member <- noteS @'TeamMemberNotFound =<< getTeamMember tid uid + member <- noteS @'TeamMemberNotFound =<< TeamSubsystem.internalGetTeamMember uid tid case member ^. legalHoldStatus of UserLegalHoldEnabled -> throwS @'UserLegalHoldAlreadyEnabled lhs@UserLegalHoldPending -> @@ -454,7 +475,7 @@ approveDevice :: Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member (ErrorS 'UserLegalHoldNotPending) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input (Local ())) r, @@ -468,7 +489,10 @@ approveDevice :: Member TeamStore r, Member (Embed IO) r, Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member (Input (FeatureDefaults LegalholdConfig)) r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -487,7 +511,7 @@ approveDevice lzusr connId tid uid (Public.ApproveLegalHoldForUserRequest mPassw assertOnTeam (tUnqualified luid) tid ensureReAuthorised zusr mPassword Nothing Nothing userLHStatus <- - maybe defUserLegalHoldStatus (view legalHoldStatus) <$> getTeamMember tid (tUnqualified luid) + maybe defUserLegalHoldStatus (view legalHoldStatus) <$> TeamSubsystem.internalGetTeamMember (tUnqualified luid) tid assertUserLHPending userLHStatus mPreKeys <- LegalHoldData.selectPendingPrekeys (tUnqualified luid) (prekeys, lastPrekey') <- case mPreKeys of @@ -532,7 +556,7 @@ disableForUser :: Member (ErrorS OperationDenied) r, Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input Env) r, @@ -545,7 +569,9 @@ disableForUser :: Member TeamStore r, Member (Embed IO) r, Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> TeamId -> @@ -557,11 +583,11 @@ disableForUser lzusr tid uid (Public.DisableLegalHoldForUserRequest mPassword) = P.debug $ Log.field "targets" (toByteString (tUnqualified luid)) . Log.field "action" (Log.val "LegalHold.disableForUser") - zusrMembership <- getTeamMember tid (tUnqualified lzusr) + zusrMembership <- TeamSubsystem.internalGetTeamMember (tUnqualified lzusr) tid void $ permissionCheck ChangeLegalHoldUserSettings zusrMembership userLHStatus <- - maybe defUserLegalHoldStatus (view legalHoldStatus) <$> getTeamMember tid (tUnqualified luid) + maybe defUserLegalHoldStatus (view legalHoldStatus) <$> TeamSubsystem.internalGetTeamMember (tUnqualified luid) tid let doDisable = disableLH (tUnqualified lzusr) luid userLHStatus $> DisableLegalHoldSuccess case userLHStatus of @@ -598,7 +624,7 @@ changeLegalholdStatusAndHandlePolicyConflicts :: Member (ErrorS 'LegalHoldCouldNotBlockConnections) r, Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input Env) r, @@ -609,7 +635,9 @@ changeLegalholdStatusAndHandlePolicyConflicts :: Member Random r, Member P.TinyLog r, Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => TeamId -> Local UserId -> @@ -656,7 +684,8 @@ blockNonConsentingConnections :: ( Member BrigAPIAccess r, Member TeamStore r, Member P.TinyLog r, - Member (ErrorS 'LegalHoldCouldNotBlockConnections) r + Member (ErrorS 'LegalHoldCouldNotBlockConnections) r, + Member TeamSubsystem r ) => UserId -> Sem r () @@ -715,7 +744,7 @@ handleGroupConvPolicyConflicts :: Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input Env) r, @@ -725,7 +754,9 @@ handleGroupConvPolicyConflicts :: Member Random r, Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) 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 ca3f63920b8..705d3b2f28a 100644 --- a/services/galley/src/Galley/API/LegalHold/Conflicts.hs +++ b/services/galley/src/Galley/API/LegalHold/Conflicts.hs @@ -1,19 +1,3 @@ --- 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 . {-# OPTIONS_GHC -Wno-overlapping-patterns #-} -- This file is part of the Wire Server implementation. @@ -51,7 +35,6 @@ import Data.Qualified import Data.Set qualified as Set import Galley.API.Util import Galley.Effects -import Galley.Effects.TeamStore import Galley.Options import Galley.Types.Teams import Imports @@ -66,6 +49,8 @@ import Wire.API.Team.Member import Wire.API.User import Wire.API.User.Client as Client import Wire.BrigAPIAccess +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem data LegalholdConflicts = LegalholdConflicts @@ -76,8 +61,8 @@ guardQualifiedLegalholdPolicyConflicts :: Member (Error LegalholdConflicts) r, Member (Input (Local ())) r, Member (Input Opts) r, - Member TeamStore r, - Member P.TinyLog r + Member P.TinyLog r, + Member TeamSubsystem r ) => LegalholdProtectee -> QualifiedUserClients -> @@ -100,8 +85,8 @@ guardLegalholdPolicyConflicts :: ( Member BrigAPIAccess r, Member (Error LegalholdConflicts) r, Member (Input Opts) r, - Member TeamStore r, - Member P.TinyLog r + Member P.TinyLog r, + Member TeamSubsystem r ) => LegalholdProtectee -> UserClients -> @@ -120,12 +105,16 @@ guardLegalholdPolicyConflicts (ProtectedUser self) otherClients = do FeatureLegalHoldDisabledByDefault -> guardLegalholdPolicyConflictsUid self otherClients FeatureLegalHoldWhitelistTeamsAndImplicitConsent -> guardLegalholdPolicyConflictsUid self otherClients +-- | Guard notification handling against legal-hold policy conflicts. +-- Ensures that if any user has a LH client then no user can be missing consent. +-- See also: "Brig.API.Connection.checkLegalholdPolicyConflict" +-- and "Galley.API.Action.checkLHPolicyConflictsLocal". guardLegalholdPolicyConflictsUid :: forall r. ( Member BrigAPIAccess r, Member (Error LegalholdConflicts) r, - Member TeamStore r, - Member P.TinyLog r + Member P.TinyLog r, + Member TeamSubsystem r ) => UserId -> UserClients -> @@ -152,7 +141,7 @@ guardLegalholdPolicyConflictsUid self (Map.keys . userClients -> otherUids) = do checkUserConsentMissing user = case userTeam user of Just tid -> do - mbMem <- getTeamMember tid (Wire.API.User.userId user) + mbMem <- TeamSubsystem.internalGetTeamMember (Wire.API.User.userId user) tid case mbMem of Nothing -> pure True -- it's weird that there is a member id but no member, we better bail Just mem -> pure $ case mem ^. legalHoldStatus of diff --git a/services/galley/src/Galley/API/LegalHold/Get.hs b/services/galley/src/Galley/API/LegalHold/Get.hs index 3607c040060..e6ac3379fac 100644 --- a/services/galley/src/Galley/API/LegalHold/Get.hs +++ b/services/galley/src/Galley/API/LegalHold/Get.hs @@ -24,8 +24,6 @@ import Data.LegalHold (UserLegalHoldStatus (..)) import Data.Qualified import Galley.API.Error import Galley.Effects -import Galley.Effects.LegalHoldStore qualified as LegalHoldData -import Galley.Effects.TeamStore import Imports import Polysemy import Polysemy.Error @@ -37,6 +35,9 @@ import Wire.API.Team.LegalHold import Wire.API.Team.LegalHold qualified as Public import Wire.API.Team.Member import Wire.API.User.Client.Prekey +import Wire.LegalHoldStore qualified as LegalHoldData +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem -- | Learn whether a user has LH enabled and fetch pre-keys. -- Note that this is accessible to ANY authenticated user, even ones outside the team @@ -45,15 +46,15 @@ getUserStatus :: ( Member (Error InternalError) r, Member (ErrorS 'TeamMemberNotFound) r, Member LegalHoldStore r, - Member TeamStore r, - Member P.TinyLog r + Member P.TinyLog r, + Member TeamSubsystem r ) => Local UserId -> TeamId -> UserId -> Sem r Public.UserLegalHoldStatusResponse getUserStatus _lzusr tid uid = do - teamMember <- noteS @'TeamMemberNotFound =<< getTeamMember tid uid + teamMember <- noteS @'TeamMemberNotFound =<< TeamSubsystem.internalGetTeamMember uid tid let status = view legalHoldStatus teamMember (mlk, lcid) <- case status of UserLegalHoldNoConsent -> pure (Nothing, Nothing) diff --git a/services/galley/src/Galley/API/LegalHold/Team.hs b/services/galley/src/Galley/API/LegalHold/Team.hs index ae5460aeb13..557b19fee5b 100644 --- a/services/galley/src/Galley/API/LegalHold/Team.hs +++ b/services/galley/src/Galley/API/LegalHold/Team.hs @@ -28,23 +28,24 @@ import Data.Default import Data.Id import Data.Range import Galley.Effects -import Galley.Effects.LegalHoldStore qualified as LegalHoldData import Galley.Effects.TeamFeatureStore -import Galley.Effects.TeamStore +import Galley.Env import Galley.Types.Teams as Team import Imports import Polysemy +import Polysemy.Input (Input, input) import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Team.Feature import Wire.API.Team.Size import Wire.BrigAPIAccess +import Wire.LegalHoldStore qualified as LegalHoldData assertLegalHoldEnabledForTeam :: forall r. ( Member LegalHoldStore r, - Member TeamStore r, Member TeamFeatureStore r, + Member (Input (FeatureDefaults LegalholdConfig)) r, Member (ErrorS 'LegalHoldNotEnabled) r ) => TeamId -> @@ -54,14 +55,15 @@ assertLegalHoldEnabledForTeam tid = throwS @'LegalHoldNotEnabled computeLegalHoldFeatureStatus :: - ( Member TeamStore r, - Member LegalHoldStore r + ( Member LegalHoldStore r, + Member (Input (FeatureDefaults LegalholdConfig)) r ) => TeamId -> DbFeature LegalholdConfig -> Sem r FeatureStatus -computeLegalHoldFeatureStatus tid dbFeature = - getLegalHoldFlag >>= \case +computeLegalHoldFeatureStatus tid dbFeature = do + featureLegalHold <- input @(FeatureDefaults LegalholdConfig) + case featureLegalHold of FeatureLegalHoldDisabledPermanently -> pure FeatureStatusDisabled FeatureLegalHoldDisabledByDefault -> pure (applyDbFeature dbFeature def).status @@ -72,8 +74,8 @@ computeLegalHoldFeatureStatus tid dbFeature = isLegalHoldEnabledForTeam :: forall r. ( Member LegalHoldStore r, - Member TeamStore r, - Member TeamFeatureStore r + Member TeamFeatureStore r, + Member (Input (FeatureDefaults LegalholdConfig)) r ) => TeamId -> Sem r Bool @@ -85,7 +87,8 @@ isLegalHoldEnabledForTeam tid = do ensureNotTooLargeToActivateLegalHold :: ( Member BrigAPIAccess r, Member (ErrorS 'CannotEnableLegalHoldServiceLargeTeam) r, - Member TeamStore r + Member (Input FanoutLimit) r, + Member (Input (FeatureDefaults LegalholdConfig)) r ) => TeamId -> Sem r () @@ -94,11 +97,17 @@ ensureNotTooLargeToActivateLegalHold tid = do unlessM (teamSizeBelowLimit (fromIntegral teamSize)) $ throwS @'CannotEnableLegalHoldServiceLargeTeam -teamSizeBelowLimit :: (Member TeamStore r) => Int -> Sem r Bool +teamSizeBelowLimit :: + ( Member (Input FanoutLimit) r, + Member (Input (FeatureDefaults LegalholdConfig)) r + ) => + Int -> + Sem r Bool teamSizeBelowLimit teamSize = do - limit <- fromIntegral . fromRange <$> fanoutLimit + limit <- fromIntegral . fromRange <$> input @FanoutLimit let withinLimit = teamSize <= limit - getLegalHoldFlag >>= \case + featureLegalHold <- input @(FeatureDefaults LegalholdConfig) + case featureLegalHold of FeatureLegalHoldDisabledPermanently -> pure withinLimit FeatureLegalHoldDisabledByDefault -> pure withinLimit FeatureLegalHoldWhitelistTeamsAndImplicitConsent -> diff --git a/services/galley/src/Galley/API/MLS/CheckClients.hs b/services/galley/src/Galley/API/MLS/CheckClients.hs index 6a788060d76..31ff97cf2ce 100644 --- a/services/galley/src/Galley/API/MLS/CheckClients.hs +++ b/services/galley/src/Galley/API/MLS/CheckClients.hs @@ -36,6 +36,7 @@ import Polysemy import Polysemy.Error import Wire.API.Error import Wire.API.Error.Galley +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.MLS.CipherSuite import Wire.API.MLS.KeyPackage @@ -45,7 +46,7 @@ import Wire.ConversationStore.MLS.Types checkClients :: ( Member BrigAPIAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member (ErrorS MLSClientMismatch) r, Member (ErrorS MLSIdentityMismatch) r, Member (Error MLSProtocolError) r @@ -124,15 +125,15 @@ mkClientData clientInfo = infoMap = Map.fromList [ (info.clientId, key) - | info <- toList clientInfo, - key <- toList info.mlsSignatureKey + | info <- toList clientInfo, + key <- toList info.mlsSignatureKey ] } -- | Get list of mls clients from Brig (local or remote). getClientData :: ( Member BrigAPIAccess r, - Member FederatorAccess r + Member (FederationAPIAccess FederatorClient) r ) => Local x -> CipherSuiteTag -> diff --git a/services/galley/src/Galley/API/MLS/Commit/Core.hs b/services/galley/src/Galley/API/MLS/Commit/Core.hs index ddfd05776bb..966693c5940 100644 --- a/services/galley/src/Galley/API/MLS/Commit/Core.hs +++ b/services/galley/src/Galley/API/MLS/Commit/Core.hs @@ -36,7 +36,6 @@ import Galley.API.MLS.Conversation import Galley.API.MLS.IncomingMessage import Galley.API.MLS.Proposal import Galley.Effects -import Galley.Effects.FederatorAccess import Galley.Env import Galley.Options import Imports @@ -52,6 +51,7 @@ import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Federation.API import Wire.API.Federation.API.Brig +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Endpoint import Wire.API.Federation.Error import Wire.API.Federation.Version @@ -67,6 +67,8 @@ import Wire.API.User.Client import Wire.BrigAPIAccess import Wire.ConversationStore import Wire.ConversationStore.MLS.Types +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) +import Wire.FederationAPIAccess import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.TeamCollaboratorsSubsystem @@ -87,7 +89,8 @@ type HasProposalActionEffects r = Member (ErrorS 'MLSSelfRemovalNotAllowed) r, Member (ErrorS 'GroupIdVersionNotSupported) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, + Member (Input ConversationSubsystemConfig) r, Member (Input Env) r, Member (Input Opts) r, Member Now r, @@ -147,7 +150,7 @@ incrementEpoch (SubConv c s) = do getClientInfo :: ( Member BrigAPIAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member (Error FederationError) r ) => Local x -> @@ -157,7 +160,7 @@ getClientInfo :: getClientInfo loc = foldQualified loc getLocalMLSClients getRemoteMLSClients getRemoteMLSClients :: - ( Member FederatorAccess r, + ( Member (FederationAPIAccess FederatorClient) r, Member (Error FederationError) r ) => Remote UserId -> @@ -175,7 +178,7 @@ getRemoteMLSClients rusr suite = do getSingleClientInfo :: ( Member BrigAPIAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member (Error FederationError) r ) => Local x -> @@ -186,7 +189,7 @@ getSingleClientInfo :: getSingleClientInfo loc = foldQualified loc getLocalMLSClient getRemoteMLSClient getRemoteMLSClient :: - ( Member FederatorAccess r, + ( Member (FederationAPIAccess FederatorClient) r, Member (Error FederationError) r ) => Remote UserId -> @@ -245,7 +248,7 @@ checkUpdatePath :: Member (Error MLSProtocolError) r, Member (Error FederationError) r, Member BrigAPIAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member (ErrorS MLSInvalidLeafNodeSignature) r ) => Local ConvOrSubConv -> diff --git a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs index a89eacd3df6..4fe175b8507 100644 --- a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs +++ b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs @@ -40,10 +40,10 @@ import Galley.API.MLS.Proposal import Galley.API.MLS.Util import Galley.API.Util import Galley.Effects -import Galley.Effects.ProposalStore import Imports import Polysemy import Polysemy.Error +import Polysemy.Input (Input) import Polysemy.Resource (Resource) import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Action @@ -61,7 +61,10 @@ import Wire.API.Unreachable import Wire.ConversationStore import Wire.ConversationStore.MLS.Types import Wire.ConversationSubsystem +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) +import Wire.ProposalStore import Wire.StoredConversation +import Wire.TeamSubsystem (TeamSubsystem) processInternalCommit :: forall r. @@ -77,7 +80,9 @@ processInternalCommit :: Member Resource r, Member Random r, Member (ErrorS MLSInvalidLeafNodeSignature) r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => SenderIdentity -> Maybe ConnId -> @@ -256,7 +261,11 @@ processInternalCommit senderIdentity con lConvOrSub ciphersuite ciphersuiteUpdat pure events addMembers :: - (HasProposalActionEffects r, Member ConversationSubsystem r, Member MLSCommitLockStore r) => + ( HasProposalActionEffects r, + Member ConversationSubsystem r, + Member MLSCommitLockStore r, + Member TeamSubsystem r + ) => Qualified UserId -> Maybe ConnId -> Local ConvOrSubConv -> @@ -280,7 +289,11 @@ addMembers qusr con lConvOrSub users = case tUnqualified lConvOrSub of SubConv _ _ -> pure [] removeMembers :: - (HasProposalActionEffects r, Member ConversationSubsystem r, Member MLSCommitLockStore r) => + ( HasProposalActionEffects r, + Member ConversationSubsystem r, + Member MLSCommitLockStore r, + Member TeamSubsystem r + ) => Qualified UserId -> Maybe ConnId -> Local ConvOrSubConv -> diff --git a/services/galley/src/Galley/API/MLS/GroupInfo.hs b/services/galley/src/Galley/API/MLS/GroupInfo.hs index 1ca5c0d729a..a10870a306e 100644 --- a/services/galley/src/Galley/API/MLS/GroupInfo.hs +++ b/services/galley/src/Galley/API/MLS/GroupInfo.hs @@ -24,7 +24,6 @@ import Galley.API.MLS.Enabled import Galley.API.MLS.Util import Galley.API.Util import Galley.Effects -import Galley.Effects.FederatorAccess qualified as E import Galley.Env import Imports import Polysemy @@ -34,10 +33,12 @@ 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 Wire.API.MLS.GroupInfo import Wire.API.MLS.SubConversation import Wire.ConversationStore qualified as E +import Wire.FederationAPIAccess qualified as E type MLSGroupInfoStaticErrors = '[ ErrorS 'ConvNotFound, @@ -48,7 +49,7 @@ type MLSGroupInfoStaticErrors = getGroupInfo :: ( Member ConversationStore r, Member (Error FederationError) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member (Input Env) r ) => (Members MLSGroupInfoStaticErrors r) => @@ -64,8 +65,7 @@ getGroupInfo lusr qcnvId = do qcnvId getGroupInfoFromLocalConv :: - ( Member ConversationStore r - ) => + (Member ConversationStore r) => (Members MLSGroupInfoStaticErrors r) => Qualified UserId -> Local ConvId -> @@ -77,7 +77,7 @@ getGroupInfoFromLocalConv qusr lcnvId = do getGroupInfoFromRemoteConv :: ( Member (Error FederationError) r, - Member FederatorAccess r + Member (FederationAPIAccess FederatorClient) r ) => (Members MLSGroupInfoStaticErrors r) => Local UserId -> diff --git a/services/galley/src/Galley/API/MLS/GroupInfoCheck.hs b/services/galley/src/Galley/API/MLS/GroupInfoCheck.hs index cb2f7f03f08..e4dcfc4e5bc 100644 --- a/services/galley/src/Galley/API/MLS/GroupInfoCheck.hs +++ b/services/galley/src/Galley/API/MLS/GroupInfoCheck.hs @@ -22,6 +22,7 @@ module Galley.API.MLS.GroupInfoCheck where import Control.Lens (view) +import Data.Bifunctor import Data.Id import Galley.API.Teams.Features.Get import Galley.Effects @@ -31,6 +32,8 @@ import Polysemy import Polysemy.Error import Polysemy.Input import Polysemy.NonDet +import Wire.API.Conversation hiding (Member) +import Wire.API.Error import Wire.API.Error.Galley import Wire.API.MLS.Credential import Wire.API.MLS.Extension @@ -38,41 +41,65 @@ import Wire.API.MLS.GroupInfo import Wire.API.MLS.KeyPackage import Wire.API.MLS.LeafNode import Wire.API.MLS.RatchetTree +import Wire.API.MLS.Serialisation import Wire.API.Team.Feature +import Wire.ConversationStore import Wire.ConversationStore.MLS.Types data GroupInfoMismatch = GroupInfoMismatch {clients :: [(Int, ClientIdentity)]} + deriving (Show) checkGroupState :: forall r. ( Member (Error GroupInfoMismatch) r, Member (Input Opts) r, Member (Error MLSProtocolError) r, - Member TeamFeatureStore r + Member TeamFeatureStore r, + Member ConversationStore r ) => - Maybe TeamId -> + ConvOrSubConv -> IndexMap -> GroupInfo -> Sem r () -checkGroupState mTid leaves groupInfo = do - check <- isGroupInfoCheckEnabled mTid - when check $ do - trees <- - either - (\_ -> throw (mlsProtocolError "Could not parse ratchet tree extension in GroupInfo")) - pure - $ findExtension groupInfo.tbs.extensions - tree :: RatchetTree <- case trees of - (tree : _) -> pure tree - _ -> throw $ mlsProtocolError "No ratchet tree extension found in GroupInfo" - giLeaves <- imFromList <$> traverse (traverse getIdentity) (ratchetTreeLeaves tree) - when (leaves /= giLeaves) $ throw (GroupInfoMismatch (imAssocs leaves)) +checkGroupState convOrSub newLeaves groupInfo = do + check <- isGroupInfoCheckEnabled convOrSub.conv.mcMetadata.cnvmTeam + case (check, groupStateMismatch newLeaves groupInfo) of + (True, Left e) -> throw (mlsProtocolError e) + (True, Right (Just mismatch)) -> do + existingMismatch <- existingGroupStateMismatch convOrSub + when (isNothing existingMismatch) $ throw mismatch + _ -> pure () + +groupStateMismatch :: IndexMap -> GroupInfo -> Either Text (Maybe GroupInfoMismatch) +groupStateMismatch leaves groupInfo = do + trees <- + first + (const "Could not parse ratchet tree extension in GroupInfo") + $ findExtension groupInfo.tbs.extensions + tree :: RatchetTree <- case trees of + (tree : _) -> pure tree + _ -> Left "No ratchet tree extension found in GroupInfo" + giLeaves <- imFromList <$> traverse (traverse getIdentity) (ratchetTreeLeaves tree) + pure $ guard (leaves /= giLeaves) $> GroupInfoMismatch (imAssocs leaves) where - getIdentity :: LeafNode -> Sem r ClientIdentity - getIdentity leaf = case credentialIdentityAndKey leaf.credential of - Left e -> throw (mlsProtocolError e) - Right (cid, _) -> pure cid + getIdentity :: LeafNode -> Either Text ClientIdentity + getIdentity leaf = fst <$> credentialIdentityAndKey leaf.credential + +existingGroupStateMismatch :: + (Member ConversationStore r) => + ConvOrSubConv -> + Sem r (Maybe GroupInfoMismatch) +existingGroupStateMismatch convOrSub = + fmap join . runErrorS @MLSMissingGroupInfo $ + do + groupInfoData <- getConvOrSubGroupInfo convOrSub.id >>= noteS @MLSMissingGroupInfo + groupInfo <- + either (\_ -> throwS @MLSMissingGroupInfo) pure $ + decodeMLS' (unGroupInfoData groupInfoData) + case groupStateMismatch convOrSub.indexMap groupInfo of + Left _ -> throwS @MLSMissingGroupInfo + Right m -> pure m isGroupInfoCheckEnabled :: ( Member TeamFeatureStore r, diff --git a/services/galley/src/Galley/API/MLS/Keys.hs b/services/galley/src/Galley/API/MLS/Keys.hs index 71895166859..89c610b71e4 100644 --- a/services/galley/src/Galley/API/MLS/Keys.hs +++ b/services/galley/src/Galley/API/MLS/Keys.hs @@ -18,25 +18,24 @@ module Galley.API.MLS.Keys (getMLSRemovalKey, SomeKeyPair (..)) where import Control.Error.Util (hush) -import Control.Lens (view) import Data.Proxy -import Galley.Env import Imports hiding (getFirst) import Polysemy import Polysemy.Error import Polysemy.Input import Wire.API.MLS.CipherSuite import Wire.API.MLS.Keys +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig (..)) data SomeKeyPair where SomeKeyPair :: forall ss. (IsSignatureScheme ss) => Proxy ss -> KeyPair ss -> SomeKeyPair getMLSRemovalKey :: - (Member (Input Env) r) => + (Member (Input ConversationSubsystemConfig) r) => SignatureSchemeTag -> Sem r (Maybe SomeKeyPair) getMLSRemovalKey ss = fmap hush . runError @() $ do - keysByPurpose <- note () =<< inputs (view mlsKeys) + keysByPurpose <- note () =<< inputs (.mlsKeys) let keys = keysByPurpose.removal case ss of Ed25519 -> pure $ SomeKeyPair (Proxy @Ed25519) (mlsKeyPair_ed25519 keys) diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 154b651b473..052b8132b0b 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -58,8 +58,6 @@ import Galley.API.MLS.Util import Galley.API.MLS.Welcome (sendWelcomes) import Galley.API.Util import Galley.Effects -import Galley.Effects.FederatorAccess -import Galley.Effects.TeamStore qualified as TeamStore import Imports import Polysemy import Polysemy.Error @@ -74,6 +72,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 Wire.API.MLS.CipherSuite import Wire.API.MLS.Commit hiding (output) @@ -89,9 +88,13 @@ import Wire.API.Team.LegalHold import Wire.ConversationStore import Wire.ConversationStore.MLS.Types import Wire.ConversationSubsystem +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) +import Wire.FederationAPIAccess import Wire.NotificationSubsystem import Wire.Sem.Now qualified as Now import Wire.StoredConversation +import Wire.TeamStore qualified as TeamStore +import Wire.TeamSubsystem (TeamSubsystem) -- FUTUREWORK -- - Check that the capabilities of a leaf node in an add proposal contains all @@ -181,7 +184,9 @@ postMLSCommitBundle :: Members MLSBundleStaticErrors r, HasProposalEffects r, Member ConversationSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local x -> Qualified UserId -> @@ -210,7 +215,9 @@ postMLSCommitBundleFromLocalUser :: Members MLSBundleStaticErrors r, HasProposalEffects r, Member ConversationSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Version -> Local UserId -> @@ -243,7 +250,9 @@ postMLSCommitBundleToLocalConv :: Members MLSBundleStaticErrors r, HasProposalEffects r, Member ConversationSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Qualified UserId -> ClientId -> @@ -309,7 +318,7 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do checkConversationOutOfSync newUsers lConvOrSub ciphersuite lift $ - checkGroupState convOrSub.conv.mcMetadata.cnvmTeam newIndexMap bundle.groupInfo.value + checkGroupState convOrSub newIndexMap bundle.groupInfo.value -- process additions and removals events <- @@ -328,7 +337,7 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do pure (events, newClients) Nothing -> do (newIndexMap, action) <- lift $ getExternalCommitData senderIdentity.client lConvOrSub bundle.epoch bundle.commit.value - lift $ checkGroupState convOrSub.conv.mcMetadata.cnvmTeam newIndexMap bundle.groupInfo.value + lift $ checkGroupState convOrSub newIndexMap bundle.groupInfo.value let senderIdentity' = senderIdentity {index = Just action.add} processExternalCommit senderIdentity' @@ -380,7 +389,7 @@ postMLSCommitBundleToRemoteConv :: Member (Error MLSOutOfSyncError) r, Member (Input EnableOutOfSyncCheck) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationStore r, Member TinyLog r @@ -525,10 +534,13 @@ postMLSMessageToLocalConv qusr c con msg ctype convOrSubId = do for_ convOrSub.ciphersuite $ \ciphersuite -> do checkConversationOutOfSync mempty lConvOrSub ciphersuite - -- reject application messages older than 2 epochs - -- FUTUREWORK: consider rejecting this message if the conversation epoch is 0 + -- reject application messages for epoch 0 let epochInt :: Epoch -> Integer epochInt = fromIntegral . epochNumber + when (epochInt msg.epoch == 0) . throw $ + mlsProtocolError "Application messages at epoch 0 are not supported" + + -- reject application messages older than 2 epochs case convOrSub.mlsMeta.cnvmlsActiveData of Nothing -> throw $ mlsProtocolError "Application messages at epoch 0 are not supported" Just activeData -> @@ -593,8 +605,7 @@ postMLSMessageToRemoteConv loc qusr senderClient con msg rConvOrSubId = do MLSMessageResponseOutOfSyncError e -> throw e storeGroupInfo :: - ( Member ConversationStore r - ) => + (Member ConversationStore r) => ConvOrSubConvId -> GroupInfoData -> Sem r () diff --git a/services/galley/src/Galley/API/MLS/Migration.hs b/services/galley/src/Galley/API/MLS/Migration.hs index bb3da3678b4..64bc3741ae6 100644 --- a/services/galley/src/Galley/API/MLS/Migration.hs +++ b/services/galley/src/Galley/API/MLS/Migration.hs @@ -21,14 +21,17 @@ import Brig.Types.Intra import Data.Qualified import Data.Set qualified as Set import Data.Time -import Galley.Effects.FederatorAccess import Imports import Polysemy +import Polysemy.Error (Error) import Wire.API.Federation.API +import Wire.API.Federation.Client (FederatorClient) +import Wire.API.Federation.Error import Wire.API.Team.Feature import Wire.API.User import Wire.BrigAPIAccess import Wire.ConversationStore.MLS.Types +import Wire.FederationAPIAccess import Wire.StoredConversation -- | Similar to @Ap f All@, but short-circuiting. @@ -48,7 +51,8 @@ instance (Monad f) => Monoid (ApAll f) where checkMigrationCriteria :: ( Member BrigAPIAccess r, - Member FederatorAccess r + Member (FederationAPIAccess FederatorClient) r, + Member (Error FederationError) r ) => UTCTime -> MLSConversation -> diff --git a/services/galley/src/Galley/API/MLS/OutOfSync.hs b/services/galley/src/Galley/API/MLS/OutOfSync.hs index e1d133f1693..955efdd84ff 100644 --- a/services/galley/src/Galley/API/MLS/OutOfSync.hs +++ b/services/galley/src/Galley/API/MLS/OutOfSync.hs @@ -32,6 +32,7 @@ import Polysemy import Polysemy.Error import Polysemy.Input import Wire.API.Error.Galley +import Wire.API.Federation.Client (FederatorClient) import Wire.API.MLS.CipherSuite import Wire.API.MLS.Credential import Wire.API.MLS.OutOfSync @@ -43,7 +44,7 @@ import Wire.StoredConversation checkConversationOutOfSync :: ( Member ConversationStore r, Member BrigAPIAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member (Error MLSOutOfSyncError) r, Member (Input EnableOutOfSyncCheck) r ) => @@ -66,7 +67,7 @@ checkConversationOutOfSync newMembers lConvOrSub ciphersuite = case tUnqualified checkOutOfSyncUser :: ( Member BrigAPIAccess r, - Member FederatorAccess r + Member (FederationAPIAccess FederatorClient) r ) => Local x -> CipherSuiteTag -> diff --git a/services/galley/src/Galley/API/MLS/Proposal.hs b/services/galley/src/Galley/API/MLS/Proposal.hs index edd9beb581e..72d2db09d3e 100644 --- a/services/galley/src/Galley/API/MLS/Proposal.hs +++ b/services/galley/src/Galley/API/MLS/Proposal.hs @@ -42,7 +42,6 @@ import Galley.API.Error import Galley.API.MLS.IncomingMessage import Galley.API.Util import Galley.Effects -import Galley.Effects.ProposalStore import Galley.Env import Galley.Options import Imports @@ -55,6 +54,7 @@ import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Protocol import Wire.API.Error import Wire.API.Error.Galley +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.MLS.AuthenticatedContent import Wire.API.MLS.CipherSuite @@ -70,6 +70,7 @@ import Wire.API.Message import Wire.BrigAPIAccess import Wire.ConversationStore.MLS.Types import Wire.NotificationSubsystem +import Wire.ProposalStore import Wire.Sem.Now (Now) import Wire.TeamCollaboratorsSubsystem @@ -129,7 +130,7 @@ type HasProposalEffects r = Member (Error NonFederatingBackends) r, Member (Error UnreachableBackends) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member (Input Env) r, Member (Input (Local ())) r, Member (Input Opts) r, @@ -137,7 +138,6 @@ type HasProposalEffects r = Member LegalHoldStore r, Member ProposalStore r, Member TeamStore r, - Member TeamStore r, Member TinyLog r, Member TeamCollaboratorsSubsystem r ) diff --git a/services/galley/src/Galley/API/MLS/Removal.hs b/services/galley/src/Galley/API/MLS/Removal.hs index de90336757d..a65e4a0c80b 100644 --- a/services/galley/src/Galley/API/MLS/Removal.hs +++ b/services/galley/src/Galley/API/MLS/Removal.hs @@ -35,8 +35,6 @@ import Galley.API.MLS.Conversation import Galley.API.MLS.Keys import Galley.API.MLS.Propagate import Galley.Effects -import Galley.Effects.ProposalStore -import Galley.Env import Imports import Polysemy import Polysemy.Error @@ -55,7 +53,9 @@ import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation import Wire.ConversationStore import Wire.ConversationStore.MLS.Types +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) import Wire.NotificationSubsystem +import Wire.ProposalStore import Wire.Sem.Now (Now) import Wire.Sem.Random import Wire.StoredConversation @@ -70,7 +70,7 @@ createAndSendRemoveProposals :: Member ExternalAccess r, Member NotificationSubsystem r, Member ProposalStore r, - Member (Input Env) r, + Member (Input ConversationSubsystemConfig) r, Member Random r, Traversable t ) => @@ -128,7 +128,7 @@ removeClientsWithClientMapRecursively :: Member NotificationSubsystem r, Member ConversationStore r, Member ProposalStore r, - Member (Input Env) r, + Member (Input ConversationSubsystemConfig) r, Member Random r, Traversable f ) => @@ -159,7 +159,7 @@ removeClientsFromSubConvs :: Member ExternalAccess r, Member NotificationSubsystem r, Member ProposalStore r, - Member (Input Env) r, + Member (Input ConversationSubsystemConfig) r, Member Random r, Traversable f, Member ConversationStore r @@ -195,7 +195,7 @@ removeClient :: Member (Error FederationError) r, Member ExternalAccess r, Member NotificationSubsystem r, - Member (Input Env) r, + Member (Input ConversationSubsystemConfig) r, Member Now r, Member ConversationStore r, Member ProposalStore r, @@ -231,7 +231,7 @@ removeUser :: Member (Error FederationError) r, Member ExternalAccess r, Member NotificationSubsystem r, - Member (Input Env) r, + Member (Input ConversationSubsystemConfig) r, Member Now r, Member ConversationStore r, Member ProposalStore r, @@ -277,7 +277,7 @@ removeExtraneousClients :: Member (Error FederationError) r, Member ExternalAccess r, Member NotificationSubsystem r, - Member (Input Env) r, + Member (Input ConversationSubsystemConfig) r, Member Now r, Member ConversationStore r, Member ProposalStore r, diff --git a/services/galley/src/Galley/API/MLS/Reset.hs b/services/galley/src/Galley/API/MLS/Reset.hs index a1b2addba73..5d9515c9722 100644 --- a/services/galley/src/Galley/API/MLS/Reset.hs +++ b/services/galley/src/Galley/API/MLS/Reset.hs @@ -35,14 +35,17 @@ import Polysemy.TinyLog qualified as P import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.MLS.SubConversation import Wire.API.Routes.Public.Galley.MLS import Wire.ConversationStore import Wire.ConversationSubsystem +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.TeamCollaboratorsSubsystem +import Wire.TeamSubsystem (TeamSubsystem) resetMLSConversation :: ( Member (Input Env) r, @@ -59,7 +62,7 @@ resetMLSConversation :: Member (ErrorS GroupIdVersionNotSupported) r, Member BackendNotificationQueueAccess r, Member ConversationStore r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member ExternalAccess r, Member (Error FederationError) r, Member BrigAPIAccess r, @@ -68,10 +71,11 @@ resetMLSConversation :: Member ProposalStore r, Member Random r, Member Resource r, - Member TeamStore r, Member P.TinyLog r, Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> MLSReset -> @@ -112,7 +116,7 @@ resetRemoteMLSConversation :: Member (Error InternalError) r, Member BrigAPIAccess r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationStore r ) => diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index b7a452c2c40..3616c18e773 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -44,7 +44,6 @@ import Galley.API.MLS.Util import Galley.API.Util import Galley.App (Env) import Galley.Effects -import Galley.Effects.FederatorAccess import Imports import Polysemy import Polysemy.Error @@ -57,6 +56,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 Wire.API.MLS.Credential import Wire.API.MLS.Group.Serialisation @@ -66,10 +66,13 @@ import Wire.API.MLS.SubConversation import Wire.API.Routes.Public.Galley.MLS import Wire.ConversationStore qualified as Eff import Wire.ConversationStore.MLS.Types as Eff +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) +import Wire.FederationAPIAccess import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.StoredConversation import Wire.StoredConversation qualified as Data +import Wire.TeamSubsystem (TeamSubsystem) type MLSGetSubConvStaticErrors = '[ ErrorS 'ConvNotFound, @@ -83,8 +86,8 @@ getSubConversation :: Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'MLSSubConvUnsupportedConvType) r, Member (Error FederationError) r, - Member FederatorAccess r, - Member TeamStore r + Member (FederationAPIAccess FederatorClient) r, + Member TeamSubsystem r ) => Local UserId -> Qualified ConvId -> @@ -102,7 +105,7 @@ getLocalSubConversation :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'MLSSubConvUnsupportedConvType) r, - Member TeamStore r + Member TeamSubsystem r ) => Qualified UserId -> Local ConvId -> @@ -135,7 +138,8 @@ getRemoteSubConversation :: '[ ErrorS 'ConvNotFound, ErrorS 'ConvAccessDenied, ErrorS 'MLSSubConvUnsupportedConvType, - FederatorAccess + Error FederationError, + FederationAPIAccess FederatorClient ] r, RethrowErrors MLSGetSubConvStaticErrors r @@ -162,7 +166,7 @@ getSubConversationGroupInfo :: ( Members '[ ConversationStore, Error FederationError, - FederatorAccess, + FederationAPIAccess FederatorClient, Input Env ] r, @@ -206,11 +210,11 @@ deleteSubConversation :: Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'MLSStaleMessage) r, Member (Error FederationError) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member (Input Env) r, Member Resource r, - Member TeamStore r, - Member Eff.MLSCommitLockStore r + Member Eff.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> Qualified ConvId -> @@ -231,7 +235,7 @@ resetRemoteSubConversation :: Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'MLSStaleMessage) r, Member (Error FederationError) r, - Member FederatorAccess r + Member (FederationAPIAccess FederatorClient) r ) => Local UserId -> Remote ConvId -> @@ -259,7 +263,7 @@ type HasLeaveSubConversationEffects r = ( Member BackendNotificationQueueAccess r, Member ConversationStore r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member (Input Env) r, Member Now r, @@ -283,8 +287,9 @@ leaveSubConversation :: Member (ErrorS 'MLSNotEnabled) r, Member Resource r, Members LeaveSubConversationStaticErrors r, - Member TeamStore r, - Member Eff.MLSCommitLockStore r + Member Eff.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ClientId -> @@ -309,8 +314,9 @@ leaveLocalSubConversation :: Member (Error FederationError) r, Member Resource r, Members LeaveSubConversationStaticErrors r, - Member TeamStore r, - Member Eff.MLSCommitLockStore r + Member Eff.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => ClientIdentity -> Local ConvId -> @@ -351,7 +357,7 @@ leaveRemoteSubConversation :: ErrorS 'ConvAccessDenied, Error FederationError, Error MLSProtocolError, - FederatorAccess + FederationAPIAccess FederatorClient ] r ) => @@ -382,8 +388,8 @@ resetLocalSubConversation :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'MLSStaleMessage) r, Member Resource r, - Member TeamStore r, - Member Eff.MLSCommitLockStore r + Member Eff.MLSCommitLockStore r, + Member TeamSubsystem r ) => Qualified UserId -> Local ConvId -> diff --git a/services/galley/src/Galley/API/MLS/Util.hs b/services/galley/src/Galley/API/MLS/Util.hs index 97b6151abca..1127873fc5d 100644 --- a/services/galley/src/Galley/API/MLS/Util.hs +++ b/services/galley/src/Galley/API/MLS/Util.hs @@ -26,7 +26,6 @@ import Data.Set qualified as Set import Data.Text qualified as T import Galley.Data.Types import Galley.Effects -import Galley.Effects.ProposalStore import Imports import Polysemy import Polysemy.Error @@ -44,6 +43,7 @@ import Wire.API.MLS.Proposal import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation import Wire.ConversationStore +import Wire.ProposalStore getLocalConvForUser :: ( Member (ErrorS 'ConvNotFound) r, diff --git a/services/galley/src/Galley/API/MLS/Welcome.hs b/services/galley/src/Galley/API/MLS/Welcome.hs index e36aadac178..115ad8faab1 100644 --- a/services/galley/src/Galley/API/MLS/Welcome.hs +++ b/services/galley/src/Galley/API/MLS/Welcome.hs @@ -30,7 +30,6 @@ import Data.Map qualified as Map import Data.Qualified import Data.Time import Galley.API.Push -import Galley.Effects.FederatorAccess import Imports import Network.Wai.Utilities.JSONResponse import Polysemy @@ -41,6 +40,7 @@ import Wire.API.Error.Galley import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Galley +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.MLS.Credential import Wire.API.MLS.Message @@ -50,12 +50,13 @@ import Wire.API.MLS.Welcome import Wire.API.Message import Wire.API.Push.V2 (RecipientClients (..)) import Wire.ExternalAccess +import Wire.FederationAPIAccess import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now sendWelcomes :: - ( Member FederatorAccess r, + ( Member (FederationAPIAccess FederatorClient) r, Member ExternalAccess r, Member P.TinyLog r, Member Now r, @@ -104,7 +105,7 @@ sendLocalWelcomes qcnv qusr con now welcome lclients = do newMessagePush mempty con defMessageMetadata rcpts e sendRemoteWelcomes :: - ( Member FederatorAccess r, + ( Member (FederationAPIAccess FederatorClient) r, Member P.TinyLog r ) => Qualified ConvId -> diff --git a/services/galley/src/Galley/API/Message.hs b/services/galley/src/Galley/API/Message.hs index 2ecd0c65513..6ca08e514d0 100644 --- a/services/galley/src/Galley/API/Message.hs +++ b/services/galley/src/Galley/API/Message.hs @@ -54,8 +54,7 @@ import Galley.API.Push import Galley.API.Util import Galley.Effects import Galley.Effects.ClientStore -import Galley.Effects.FederatorAccess -import Galley.Effects.TeamStore +import Galley.Env import Galley.Options import Galley.Types.Clients qualified as Clients import Imports hiding (forkIO) @@ -83,10 +82,14 @@ import Wire.API.UserMap (UserMap (..)) import Wire.BackendNotificationQueueAccess import Wire.BrigAPIAccess import Wire.ConversationStore +import Wire.FederationAPIAccess import Wire.NotificationSubsystem (NotificationSubsystem) import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now import Wire.StoredConversation +import Wire.TeamStore +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem data UserType = User | Bot @@ -218,7 +221,8 @@ checkMessageClients sender participantMap recipientMap mismatchStrat = ) getRemoteClients :: - (Member FederatorAccess r) => + forall r. + (Member (FederationAPIAccess FederatorClient) r) => [RemoteMember] -> Sem r [Either (Remote [UserId], FederationError) (Map (Domain, UserId) (Set ClientId))] getRemoteClients remoteMembers = @@ -233,7 +237,9 @@ getRemoteClients remoteMembers = <$> fedClient @'Brig @"get-user-clients" (GetUserClients uids) postRemoteOtrMessage :: - (Member FederatorAccess r) => + ( Member (FederationAPIAccess FederatorClient) r, + Member (Error FederationError) r + ) => Local UserId -> Remote ConvId -> ByteString -> @@ -259,7 +265,9 @@ postBroadcast :: Member Now r, Member TeamStore r, Member P.TinyLog r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => Local UserId -> Maybe ConnId -> @@ -278,7 +286,7 @@ postBroadcast lusr con msg = runError $ do now <- Now.get tid <- lookupBindingTeam senderUser - limit <- fromIntegral . fromRange <$> fanoutLimit + limit <- fromIntegral . fromRange <$> input @FanoutLimit -- If we are going to fan this out to more than limit, we want to fail early unless (Map.size rcps <= limit) $ throwS @'BroadcastLimitExceeded @@ -331,7 +339,7 @@ postBroadcast lusr con msg = runError $ do where maybeFetchLimitedTeamMemberList :: ( Member (ErrorS 'BroadcastLimitExceeded) r, - Member TeamStore r + Member TeamSubsystem r ) => Int -> TeamId -> @@ -343,11 +351,12 @@ postBroadcast lusr con msg = runError $ do let localUserIdsToLookup = Set.toList $ Set.union (Set.fromList localUserIdsInFilter) (Set.fromList localUserIdsInRcps) unless (length localUserIdsToLookup <= limit) $ throwS @'BroadcastLimitExceeded - selectTeamMembers tid localUserIdsToLookup + TeamSubsystem.internalSelectTeamMembers tid localUserIdsToLookup maybeFetchAllMembersInTeam :: ( Member (ErrorS 'BroadcastLimitExceeded) r, - Member TeamStore r + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => TeamId -> Sem r [TeamMember] @@ -361,14 +370,14 @@ postQualifiedOtrMessage :: ( Member BrigAPIAccess r, Member ClientStore r, Member ConversationStore r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member BackendNotificationQueueAccess r, Member ExternalAccess r, Member (Input Opts) r, Member Now r, - Member TeamStore r, Member P.TinyLog r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member TeamSubsystem r ) => UserType -> Qualified UserId -> @@ -526,8 +535,8 @@ guardQualifiedLegalholdPolicyConflictsWrapper :: ( Member BrigAPIAccess r, Member (Error (MessageNotSent MessageSendingStatus)) r, Member (Input Opts) r, - Member TeamStore r, - Member P.TinyLog r + Member P.TinyLog r, + Member TeamSubsystem r ) => UserType -> Qualified UserId -> diff --git a/services/galley/src/Galley/API/Public/Bot.hs b/services/galley/src/Galley/API/Public/Bot.hs index ad839a6f24f..4d151325496 100644 --- a/services/galley/src/Galley/API/Public/Bot.hs +++ b/services/galley/src/Galley/API/Public/Bot.hs @@ -34,6 +34,7 @@ import Wire.API.Event.Team qualified as Public () import Wire.API.Provider.Bot import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.Bot +import Wire.TeamSubsystem (TeamSubsystem) botAPI :: API BotAPI GalleyEffects botAPI = @@ -48,7 +49,8 @@ getBotConversation :: Member TeamFeatureStore r, Member (ErrorS 'AccessDenied) r, Member (ErrorS 'ConvNotFound) r, - Member TeamStore r + Member TeamStore r, + Member TeamSubsystem r ) => BotId -> ConvId -> diff --git a/services/galley/src/Galley/API/Public/Feature.hs b/services/galley/src/Galley/API/Public/Feature.hs index 1313ac8ed80..09e315fc0ba 100644 --- a/services/galley/src/Galley/API/Public/Feature.hs +++ b/services/galley/src/Galley/API/Public/Feature.hs @@ -67,7 +67,9 @@ featureAPI = <@> deprecatedFeatureAPI <@> mkNamedAPI @'("get", DomainRegistrationConfig) getFeature <@> featureAPIGetPut - <@> featureAPIGetPut + <@> mkNamedAPI @'("get", CellsConfig) getFeature + <@> mkNamedAPI @"put-CellsConfig@v13" setFeature + <@> mkNamedAPI @'("put", CellsConfig) setFeature <@> mkNamedAPI @'("get", AllowedGlobalOperationsConfig) getFeature <@> mkNamedAPI @'("get", AssetAuditLogConfig) getFeature <@> mkNamedAPI @'("get", ConsumableNotificationsConfig) getFeature @@ -75,6 +77,9 @@ featureAPI = <@> mkNamedAPI @'("get", AppsConfig) getFeature <@> mkNamedAPI @'("get", SimplifiedUserConnectionRequestQRCodeConfig) getFeature <@> mkNamedAPI @'("get", StealthUsersConfig) getFeature + <@> mkNamedAPI @'("get", CellsInternalConfig) getFeature + <@> featureAPIGetPut @MeetingsConfig + <@> featureAPIGetPut @MeetingsPremiumConfig deprecatedFeatureConfigAPI :: API DeprecatedFeatureAPI GalleyEffects deprecatedFeatureConfigAPI = diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index d70c47254b6..7d3e758030c 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -78,8 +78,6 @@ import Galley.API.Util import Galley.Data.Types (Code (codeConversation)) import Galley.Data.Types qualified as Data import Galley.Effects -import Galley.Effects.FederatorAccess qualified as E -import Galley.Effects.TeamStore qualified as E import Galley.Env import Galley.Options import Imports @@ -112,19 +110,22 @@ import Wire.API.Team.Member (HiddenPerm (..), TeamMember) import Wire.API.User import Wire.ConversationStore qualified as E import Wire.ConversationStore.MLS.Types +import Wire.FederationAPIAccess qualified as E import Wire.HashPassword (HashPassword) import Wire.RateLimit import Wire.Sem.Paging.Cassandra import Wire.StoredConversation import Wire.StoredConversation qualified as Data import Wire.TeamCollaboratorsSubsystem +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem import Wire.UserList getBotConversation :: ( Member ConversationStore r, Member (ErrorS 'ConvNotFound) r, Member (Input (Local ())) r, - Member TeamStore r + Member TeamSubsystem r ) => BotId -> ConvId -> @@ -151,7 +152,7 @@ getUnqualifiedOwnConversation :: Member (ErrorS 'ConvAccessDenied) r, Member (Error InternalError) r, Member P.TinyLog r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> ConvId -> @@ -165,7 +166,7 @@ getUnqualifiedConversation :: ( Member ConversationStore r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvAccessDenied) r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> ConvId -> @@ -180,9 +181,9 @@ getConversation :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvAccessDenied) r, Member (Error FederationError) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member P.TinyLog r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> Qualified ConvId -> @@ -201,9 +202,9 @@ getOwnConversation :: Member (ErrorS 'ConvAccessDenied) r, Member (Error FederationError) r, Member (Error InternalError) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member P.TinyLog r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> Qualified ConvId -> @@ -220,7 +221,7 @@ getRemoteConversation :: Member (ErrorS ConvNotFound) r, Member (Error FederationError) r, Member TinyLog r, - Member FederatorAccess r + Member (FederationAPIAccess FederatorClient) r ) => Local UserId -> Remote ConvId -> @@ -236,7 +237,7 @@ getRemoteConversations :: ( Member ConversationStore r, Member (Error FederationError) r, Member (ErrorS 'ConvNotFound) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member P.TinyLog r ) => Local UserId -> @@ -305,7 +306,7 @@ partitionGetConversationFailures = bimap concat concat . partitionEithers . map getRemoteConversationsWithFailures :: ( Member ConversationStore r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member P.TinyLog r ) => Local UserId -> @@ -353,7 +354,7 @@ getRemoteConversationsWithFailures lusr convs = do handleFailure (Left (rcids, e)) = do P.warn $ Logger.msg ("Error occurred while fetching remote conversations" :: ByteString) - . Logger.field "error" (show e) + . Logger.field "error" (displayException e) pure . Left $ failedGetConversationRemotely (sequenceA rcids) e handleFailure (Right c) = pure . Right . traverse (.convs) $ c @@ -361,7 +362,7 @@ getConversationRoles :: ( Member ConversationStore r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvAccessDenied) r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> ConvId -> @@ -501,7 +502,7 @@ getConversationsInternal luser mids mstart msize = do listConversations :: ( Member ConversationStore r, Member (Error InternalError) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member P.TinyLog r ) => Local UserId -> @@ -585,7 +586,7 @@ getSelfMember :: Member (ErrorS ConvNotFound) r, Member (Error FederationError) r, Member TinyLog r, - Member FederatorAccess r + Member (FederationAPIAccess FederatorClient) r ) => Local UserId -> Qualified ConvId -> @@ -605,8 +606,7 @@ getSelfMember lusr cnv = do pure $ Just $ conv.cnvMembers.cmSelf getLocalSelf :: - ( Member ConversationStore r - ) => + (Member ConversationStore r) => Local UserId -> ConvId -> Sem r (Maybe Public.Member) @@ -640,18 +640,18 @@ getConversationByReusableCode :: Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'GuestLinksDisabled) r, Member (ErrorS 'NotATeamMember) r, - Member TeamStore r, Member TeamFeatureStore r, Member (Input Opts) r, Member HashPassword r, - Member RateLimit r + Member RateLimit r, + Member TeamSubsystem r ) => Local UserId -> Key -> Value -> Sem r ConversationCoverView getConversationByReusableCode lusr key value = do - c <- verifyReusableCode (RateLimitUser (tUnqualified lusr)) False Nothing (ConversationCode key value Nothing) + c <- verifyReusableCode (RateLimitUser (tUnqualified lusr)) False Nothing (ConversationCode key value) conv <- E.getConversation (codeConversation c) >>= noteS @'ConvNotFound ensureConversationAccess (tUnqualified lusr) conv CodeAccess ensureGuestLinksEnabled (Data.convTeam conv) @@ -685,14 +685,14 @@ getConversationGuestLinksStatus :: Member (ErrorS 'ConvAccessDenied) r, Member (Input Opts) r, Member TeamFeatureStore r, - Member TeamStore r + Member TeamSubsystem r ) => UserId -> ConvId -> Sem r (LockableFeature GuestLinksConfig) getConversationGuestLinksStatus uid convId = do conv <- E.getConversation convId >>= noteS @'ConvNotFound - mTeamMember <- maybe (pure Nothing) (flip E.getTeamMember uid) conv.metadata.cnvmTeam + mTeamMember <- maybe (pure Nothing) (TeamSubsystem.internalGetTeamMember uid) conv.metadata.cnvmTeam ensureConvAdmin conv uid mTeamMember getConversationGuestLinksFeatureStatus (Data.convTeam conv) @@ -777,10 +777,11 @@ getMLSOne2OneConversationV5 :: Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'NotConnected) r, Member (ErrorS 'MLSFederatedOne2OneNotSupported) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member TeamStore r, Member P.TinyLog r, - Member TeamCollaboratorsSubsystem r + Member TeamCollaboratorsSubsystem r, + Member TeamSubsystem r ) => Local UserId -> Qualified UserId -> @@ -798,10 +799,11 @@ getMLSOne2OneConversationInternal :: Member (Error InternalError) r, Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'NotConnected) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member TeamStore r, Member P.TinyLog r, - Member TeamCollaboratorsSubsystem r + Member TeamCollaboratorsSubsystem r, + Member TeamSubsystem r ) => Local UserId -> Qualified UserId -> @@ -817,10 +819,11 @@ getMLSOne2OneConversationV6 :: Member (Error InternalError) r, Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'NotConnected) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member TeamStore r, Member P.TinyLog r, - Member TeamCollaboratorsSubsystem r + Member TeamCollaboratorsSubsystem r, + Member TeamSubsystem r ) => Local UserId -> Qualified UserId -> @@ -843,10 +846,11 @@ getMLSOne2OneConversation :: Member (Error InternalError) r, Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'NotConnected) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member TeamStore r, Member P.TinyLog r, - Member TeamCollaboratorsSubsystem r + Member TeamCollaboratorsSubsystem r, + Member TeamSubsystem r ) => Local UserId -> Qualified UserId -> @@ -883,7 +887,7 @@ getRemoteMLSOne2OneConversation :: ( Member (Error InternalError) r, Member (Error FederationError) r, Member (ErrorS 'NotConnected) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member (ErrorS MLSNotEnabled) r, Member TinyLog r ) => @@ -941,7 +945,7 @@ isMLSOne2OneEstablished :: Member (Error InternalError) r, Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'NotConnected) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member TinyLog r ) => Local UserId -> @@ -972,7 +976,7 @@ isRemoteMLSOne2OneEstablished :: ( Member (ErrorS 'NotConnected) r, Member (Error FederationError) r, Member (Error InternalError) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member (ErrorS MLSNotEnabled) r, Member TinyLog r ) => @@ -994,7 +998,7 @@ searchChannels :: ( Member ConversationStore r, Member (ErrorS NotATeamMember) r, Member (ErrorS OperationDenied) r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> TeamId -> @@ -1007,7 +1011,7 @@ searchChannels :: Sem r ConversationPage searchChannels lusr tid searchString sortOrder pageSize lastName lastId discoverable = do r <- runError @(Tagged OperationDenied ()) $ do - mem <- E.getTeamMember tid (tUnqualified lusr) + mem <- TeamSubsystem.internalGetTeamMember (tUnqualified lusr) tid void $ permissionCheck SearchChannels mem case r of Left e | not discoverable -> throw e diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index 314ae7b43c5..df79cc4504b 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -1,19 +1,3 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . {-# LANGUAGE LambdaCase #-} -- This file is part of the Wire Server implementation. @@ -44,7 +28,6 @@ module Galley.API.Teams getBindingTeamMembers, getManyTeams, deleteTeam, - uncheckedDeleteTeam, addTeamMember, getTeamConversationRoles, getTeamMembers, @@ -78,16 +61,14 @@ import Brig.Types.Team (TeamSize (..)) import Cassandra (PageWithState (pwsResults), pwsHasMore) import Cassandra qualified as C import Control.Lens -import Data.ByteString.Conversion (List, toByteString) -import Data.ByteString.Conversion qualified +import Data.ByteString.Conversion (toByteString) +import Data.ByteString.Conversion qualified as BSC import Data.ByteString.Lazy qualified as LBS import Data.Default import Data.HashMap.Strict qualified as HM import Data.Id import Data.Json.Util import Data.LegalHold qualified as LH -import Data.List.Extra qualified as List -import Data.List.NonEmpty (NonEmpty ((:|))) import Data.Map qualified as Map import Data.Proxy import Data.Qualified @@ -104,13 +85,10 @@ import Galley.API.Update qualified as API import Galley.API.Util import Galley.App import Galley.Effects -import Galley.Effects.LegalHoldStore qualified as Data import Galley.Effects.Queue qualified as E import Galley.Effects.SearchVisibilityStore qualified as SearchVisibilityData -import Galley.Effects.SparAccess qualified as Spar import Galley.Effects.TeamMemberStore qualified as E -import Galley.Effects.TeamStore qualified as E -import Galley.Intra.Journal qualified as Journal +import Galley.Env import Galley.Options import Galley.Types.Teams import Imports hiding (forkIO) @@ -125,13 +103,13 @@ import Wire.API.Conversation.Role (wireConvRoles) import Wire.API.Conversation.Role qualified as Public import Wire.API.Error import Wire.API.Error.Galley -import Wire.API.Event.Conversation qualified as Conv import Wire.API.Event.LeaveReason import Wire.API.Event.Team +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.Push.V2 (RecipientClients (RecipientClientsAll)) import Wire.API.Routes.Internal.Galley.TeamsIntra -import Wire.API.Routes.MultiTablePaging (MultiTablePage (..), MultiTablePagingState (mtpsState)) +import Wire.API.Routes.MultiTablePaging (MultiTablePage (..), MultiTablePagingState (..)) import Wire.API.Routes.Public.Galley.TeamMember import Wire.API.Team import Wire.API.Team qualified as Public @@ -152,7 +130,7 @@ import Wire.BrigAPIAccess qualified as Brig import Wire.BrigAPIAccess qualified as E import Wire.ConversationStore qualified as E import Wire.ConversationSubsystem -import Wire.ExternalAccess qualified as E +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) import Wire.ListItems qualified as E import Wire.NotificationSubsystem import Wire.Sem.Now @@ -160,13 +138,16 @@ import Wire.Sem.Now qualified as Now import Wire.Sem.Paging.Cassandra import Wire.StoredConversation import Wire.TeamCollaboratorsSubsystem +import Wire.TeamJournal (TeamJournal) +import Wire.TeamJournal qualified as Journal +import Wire.TeamStore qualified as E import Wire.TeamSubsystem (TeamSubsystem) import Wire.TeamSubsystem qualified as TeamSubsystem import Wire.UserList getTeamH :: forall r. - (Member (ErrorS 'TeamNotFound) r, Member (Queue DeleteItem) r, Member TeamStore r) => + (Member (ErrorS 'TeamNotFound) r, Member (Queue DeleteItem) r, Member TeamStore r, Member TeamSubsystem r) => UserId -> TeamId -> Sem r Public.Team @@ -210,7 +191,8 @@ getTeamNameInternal = fmap (fmap TeamName) . E.getTeamName getManyTeams :: ( Member TeamStore r, Member (Queue DeleteItem) r, - Member (ListItems LegacyPaging TeamId) r + Member (ListItems LegacyPaging TeamId) r, + Member TeamSubsystem r ) => UserId -> Sem r Public.TeamList @@ -221,13 +203,14 @@ getManyTeams zusr = lookupTeam :: ( Member TeamStore r, - Member (Queue DeleteItem) r + Member (Queue DeleteItem) r, + Member TeamSubsystem r ) => UserId -> TeamId -> Sem r (Maybe Public.Team) lookupTeam zusr tid = do - tm <- E.getTeamMember tid zusr + tm <- TeamSubsystem.internalGetTeamMember zusr tid if isJust tm then do t <- E.getTeam tid @@ -277,7 +260,8 @@ updateTeamStatus :: Member (ErrorS 'InvalidTeamStatusUpdate) r, Member (ErrorS 'TeamNotFound) r, Member Now r, - Member TeamStore r + Member TeamStore r, + Member TeamJournal r ) => TeamId -> TeamStatusUpdate -> @@ -316,7 +300,8 @@ updateTeamH :: Member (ErrorS ('MissingPermission ('Just 'SetTeamData))) r, Member NotificationSubsystem r, Member Now r, - Member TeamStore r + Member TeamStore r, + Member TeamSubsystem r ) => UserId -> ConnId -> @@ -324,7 +309,7 @@ updateTeamH :: Public.TeamUpdateData -> Sem r () updateTeamH zusr zcon tid updateData = do - zusrMembership <- E.getTeamMember tid zusr + zusrMembership <- TeamSubsystem.internalGetTeamMember zusr tid void $ permissionCheckS SSetTeamData zusrMembership E.setTeamData tid updateData now <- Now.get @@ -350,7 +335,8 @@ deleteTeam :: Member (ErrorS OperationDenied) r, Member (ErrorS 'TeamNotFound) r, Member (Queue DeleteItem) r, - Member TeamStore r + Member TeamStore r, + Member TeamSubsystem r ) => UserId -> ConnId -> @@ -368,7 +354,7 @@ deleteTeam zusr zcon tid body = do queueTeamDeletion tid zusr (Just zcon) where checkPermissions team = do - void $ permissionCheck DeleteTeam =<< E.getTeamMember tid zusr + void $ permissionCheck DeleteTeam =<< TeamSubsystem.internalGetTeamMember zusr tid when (tdTeam team ^. teamBinding == Binding) $ do ensureReAuthorised zusr (body ^. tdAuthPassword) (body ^. tdVerificationCode) (Just U.DeleteTeam) @@ -379,7 +365,8 @@ internalDeleteBindingTeam :: Member (ErrorS 'NotAOneMemberTeam) r, Member (ErrorS 'DeleteQueueFull) r, Member (Queue DeleteItem) r, - Member TeamStore r + Member TeamStore r, + Member TeamSubsystem r ) => TeamId -> Bool -> @@ -390,112 +377,32 @@ internalDeleteBindingTeam tid force = do Nothing -> throwS @'TeamNotFound Just team | team ^. teamBinding /= Binding -> throwS @'NoBindingTeam Just team -> do - mems <- E.getTeamMembersWithLimit tid (unsafeRange 2) + mems <- TeamSubsystem.internalGetTeamMembersWithLimit tid (Just (unsafeRange 2)) case mems ^. teamMembers of [mem] -> queueTeamDeletion tid (mem ^. userId) Nothing -- if the team has more than one member (and deletion is forced) or no members we use the team creator's userId for deletion events xs | null xs || force -> queueTeamDeletion tid (team ^. teamCreator) Nothing _ -> throwS @'NotAOneMemberTeam --- This function is "unchecked" because it does not validate that the user has the `DeleteTeam` permission. -uncheckedDeleteTeam :: - forall r. - ( Member BrigAPIAccess r, - Member ExternalAccess r, - Member NotificationSubsystem r, - Member (Input Opts) r, - Member Now r, - Member LegalHoldStore r, - Member SparAccess r, - Member TeamStore r, - Member ConversationStore r - ) => - Local UserId -> - Maybe ConnId -> - TeamId -> - Sem r () -uncheckedDeleteTeam lusr zcon tid = do - team <- E.getTeam tid - when (isJust team) $ do - Spar.deleteTeam tid - now <- Now.get - convs <- E.getTeamConversations tid - -- Even for LARGE TEAMS, we _DO_ want to fetch all team members here because we - -- want to generate conversation deletion events for non-team users. This should - -- be fine as it is done once during the life team of a team and we still do not - -- fanout this particular event to all team members anyway. And this is anyway - -- done asynchronously - membs <- E.getTeamMembers tid - (ue, be) <- foldrM (createConvDeleteEvents now membs) ([], []) convs - let e = newEvent tid now EdTeamDelete - pushDeleteEvents membs e ue - E.deliverAsync be - -- TODO: we don't delete bots here, but we should do that, since - -- every bot user can only be in a single conversation. Just - -- deleting conversations from the database is not enough. - when ((view teamBinding . tdTeam <$> team) == Just Binding) $ do - mapM_ (E.deleteUser . view userId) membs - Journal.teamDelete tid - Data.unsetTeamLegalholdWhitelisted tid - E.deleteTeam tid - where - pushDeleteEvents :: [TeamMember] -> Event -> [Push] -> Sem r () - pushDeleteEvents membs e ue = do - o <- inputs (view settings) - let r = userRecipient (tUnqualified lusr) :| membersToRecipients (Just (tUnqualified lusr)) membs - -- To avoid DoS on gundeck, send team deletion events in chunks - let chunkSize = fromMaybe defConcurrentDeletionEvents (o ^. concurrentDeletionEvents) - let chunks = List.chunksOf chunkSize (toList r) - forM_ chunks $ \chunk -> - -- push TeamDelete events. Note that despite having a complete list, we are guaranteed in the - -- push module to never fan this out to more than the limit - pushNotifications [def {origin = Just (tUnqualified lusr), json = toJSONObject e, recipients = chunk, conn = zcon}] - -- To avoid DoS on gundeck, send conversation deletion events slowly - pushNotificationsSlowly ue - createConvDeleteEvents :: - UTCTime -> - [TeamMember] -> - ConvId -> - ([Push], [(BotMember, Conv.Event)]) -> - Sem r ([Push], [(BotMember, Conv.Event)]) - createConvDeleteEvents now teamMembs cid (pp, ee) = do - let qconvId = tUntagged $ qualifyAs lusr cid - (bots, convMembs) <- localBotsAndUsers <$> E.getLocalMembers cid - -- Only nonTeamMembers need to get any events, since on team deletion, - -- all team users are deleted immediately after these events are sent - -- and will thus never be able to see these events in practice. - let mm = nonTeamMembers convMembs teamMembs - let e = Conv.Event qconvId Nothing (Conv.EventFromUser (tUntagged lusr)) now (Just tid) Conv.EdConvDelete - -- This event always contains all the required recipients - let p = - def - { origin = Just (tUnqualified lusr), - json = toJSONObject e, - recipients = map localMemberToRecipient mm - } - let ee' = map (,e) bots - let pp' = (p {conn = zcon}) : pp - pure (pp', ee' ++ ee) - getTeamConversationRoles :: ( Member (ErrorS 'NotATeamMember) r, - Member TeamStore r + Member TeamSubsystem r ) => UserId -> TeamId -> Sem r Public.ConversationRolesList getTeamConversationRoles zusr tid = do - void $ E.getTeamMember tid zusr >>= noteS @'NotATeamMember + void $ TeamSubsystem.internalGetTeamMember zusr tid >>= noteS @'NotATeamMember -- NOTE: If/when custom roles are added, these roles should -- be merged with the team roles (if they exist) pure $ Public.ConversationRolesList wireConvRoles getTeamMembers :: ( Member (ErrorS 'NotATeamMember) r, - Member TeamStore r, Member BrigAPIAccess r, Member (TeamMemberStore CassandraPaging) r, - Member P.TinyLog r + Member P.TinyLog r, + Member TeamSubsystem r ) => Local UserId -> TeamId -> @@ -504,7 +411,7 @@ getTeamMembers :: Sem r TeamMembersPage getTeamMembers lzusr tid mbMaxResults mbPagingState = do let uid = tUnqualified lzusr - member <- E.getTeamMember tid uid >>= noteS @'NotATeamMember + member <- TeamSubsystem.internalGetTeamMember uid tid >>= noteS @'NotATeamMember let mState = C.PagingState . LBS.fromStrict <$> (mbPagingState >>= mtpsState) let mLimit = fromMaybe (unsafeRange Public.hardTruncationLimit) mbMaxResults if member `hasPermission` SearchContacts @@ -537,22 +444,27 @@ getTeamMembers lzusr tid mbMaxResults mbPagingState = do -- we only return the person who invited them and the self user. let invitee = member ^. invitation <&> fst let uids = uid : maybeToList invitee - E.selectTeamMembersPaginated tid uids mState mLimit <&> toTeamMembersPage member + TeamSubsystem.internalSelectTeamMembers tid uids <&> toTeamSingleMembersPage member where toTeamMembersPage :: TeamMember -> C.PageWithState TeamMember -> TeamMembersPage toTeamMembersPage member p = let withPerms = (member `canSeePermsOf`) in TeamMembersPage $ MultiTablePage - (map (setOptionalPerms withPerms) $ pwsResults p) - (pwsHasMore p) - (teamMemberPagingState p) + { mtpResults = map (setOptionalPerms withPerms) $ pwsResults p, + mtpHasMore = pwsHasMore p, + mtpPagingState = teamMemberPagingState p + } + + toTeamSingleMembersPage :: TeamMember -> [TeamMember] -> TeamMembersPage + toTeamSingleMembersPage member = + mkSingleTeamMembersPage . map (setOptionalPerms (member `canSeePermsOf`)) -- | like 'getTeamMembers', but with an explicit list of users we are to return. bulkGetTeamMembers :: ( Member (ErrorS 'BulkGetMemberLimitExceeded) r, Member (ErrorS 'NotATeamMember) r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> TeamId -> @@ -562,8 +474,8 @@ bulkGetTeamMembers :: bulkGetTeamMembers lzusr tid mbMaxResults uids = do unless (length (U.mUsers uids) <= fromIntegral (fromRange (fromMaybe (unsafeRange Public.hardTruncationLimit) mbMaxResults))) $ throwS @'BulkGetMemberLimitExceeded - m <- E.getTeamMember tid (tUnqualified lzusr) >>= noteS @'NotATeamMember - mems <- E.selectTeamMembers tid (U.mUsers uids) + m <- TeamSubsystem.internalGetTeamMember (tUnqualified lzusr) tid >>= noteS @'NotATeamMember + mems <- TeamSubsystem.internalSelectTeamMembers tid (U.mUsers uids) let withPerms = (m `canSeePermsOf`) hasMore = ListComplete pure $ setOptionalPermsMany withPerms (newTeamMemberList mems hasMore) @@ -571,7 +483,7 @@ bulkGetTeamMembers lzusr tid mbMaxResults uids = do getTeamMember :: ( Member (ErrorS 'TeamMemberNotFound) r, Member (ErrorS 'NotATeamMember) r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> TeamId -> @@ -579,10 +491,10 @@ getTeamMember :: Sem r TeamMemberOptPerms getTeamMember lzusr tid uid = do m <- - E.getTeamMember tid (tUnqualified lzusr) + TeamSubsystem.internalGetTeamMember (tUnqualified lzusr) tid >>= noteS @'NotATeamMember let withPerms = (m `canSeePermsOf`) - member <- E.getTeamMember tid uid >>= noteS @'TeamMemberNotFound + member <- TeamSubsystem.internalGetTeamMember uid tid >>= noteS @'TeamMemberNotFound pure $ setOptionalPerms withPerms member uncheckedGetTeamMember :: @@ -615,7 +527,10 @@ addTeamMember :: Member TeamFeatureStore r, Member TeamNotificationStore r, Member TeamStore r, - Member P.TinyLog r + Member P.TinyLog r, + Member (Input FanoutLimit) r, + Member (Input (FeatureDefaults LegalholdConfig)) r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -630,7 +545,7 @@ addTeamMember lzusr zcon tid nmem = do . Log.field "action" (Log.val "Teams.addTeamMember") -- verify permissions zusrMembership <- - E.getTeamMember tid zusr + TeamSubsystem.internalGetTeamMember zusr tid >>= permissionCheck AddTeamMember let targetPermissions = nmem ^. nPermissions targetPermissions `ensureNotElevated` zusrMembership @@ -655,7 +570,10 @@ uncheckedAddTeamMember :: Member P.TinyLog r, Member TeamFeatureStore r, Member TeamNotificationStore r, - Member TeamStore r + Member TeamStore r, + Member (Input FanoutLimit) r, + Member (Input (FeatureDefaults LegalholdConfig)) r, + Member TeamJournal r ) => TeamId -> NewTeamMember -> @@ -676,7 +594,9 @@ uncheckedUpdateTeamMember :: Member NotificationSubsystem r, Member Now r, Member P.TinyLog r, - Member TeamStore r + Member TeamStore r, + Member TeamJournal r, + Member TeamSubsystem r ) => Maybe (Local UserId) -> Maybe ConnId -> @@ -695,7 +615,7 @@ uncheckedUpdateTeamMember mlzusr mZcon tid newMem = do team <- fmap tdTeam $ E.getTeam tid >>= noteS @'TeamNotFound previousMember <- - E.getTeamMember tid targetId >>= noteS @'TeamMemberNotFound + TeamSubsystem.internalGetTeamMember targetId tid >>= noteS @'TeamMemberNotFound admins <- E.getTeamAdmins tid let admins' = [targetId | isAdminOrOwner targetPermissions] <> filter (/= targetId) admins @@ -735,7 +655,9 @@ updateTeamMember :: Member NotificationSubsystem r, Member Now r, Member P.TinyLog r, - Member TeamStore r + Member TeamStore r, + Member TeamJournal r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -753,13 +675,13 @@ updateTeamMember lzusr zcon tid newMem = do -- get the team and verify permissions user <- - E.getTeamMember tid zusr + TeamSubsystem.internalGetTeamMember zusr tid >>= permissionCheck SetMemberPermissions -- user may not elevate permissions targetPermissions `ensureNotElevated` user previousMember <- - E.getTeamMember tid targetId >>= noteS @'TeamMemberNotFound + TeamSubsystem.internalGetTeamMember targetId tid >>= noteS @'TeamMemberNotFound when ( downgradesOwner previousMember targetPermissions && not (canDowngradeOwner user previousMember) @@ -791,7 +713,10 @@ deleteTeamMember :: Member ConversationSubsystem r, Member TeamFeatureStore r, Member TeamStore r, - Member P.TinyLog r + Member P.TinyLog r, + Member (Input FanoutLimit) r, + Member TeamJournal r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -817,7 +742,10 @@ deleteNonBindingTeamMember :: Member ConversationSubsystem r, Member TeamFeatureStore r, Member TeamStore r, - Member P.TinyLog r + Member P.TinyLog r, + Member (Input FanoutLimit) r, + Member TeamJournal r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -843,7 +771,10 @@ deleteTeamMember' :: Member ConversationSubsystem r, Member TeamFeatureStore r, Member TeamStore r, - Member P.TinyLog r + Member P.TinyLog r, + Member (Input FanoutLimit) r, + Member TeamJournal r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -855,8 +786,8 @@ deleteTeamMember' lusr zcon tid remove mBody = do P.debug $ Log.field "targets" (toByteString remove) . Log.field "action" (Log.val "Teams.deleteTeamMember") - zusrMember <- E.getTeamMember tid (tUnqualified lusr) - targetMember <- E.getTeamMember tid remove + zusrMember <- TeamSubsystem.internalGetTeamMember (tUnqualified lusr) tid + targetMember <- TeamSubsystem.internalGetTeamMember remove tid void $ permissionCheck RemoveTeamMember zusrMember do dm <- noteS @'NotATeamMember zusrMember @@ -998,15 +929,15 @@ removeFromConvsAndPushConvLeaveEvent lusr zcon tid remove = do getTeamConversations :: ( Member (ErrorS 'NotATeamMember) r, Member (ErrorS OperationDenied) r, - Member TeamStore r, - Member ConversationStore r + Member ConversationStore r, + Member TeamSubsystem r ) => UserId -> TeamId -> Sem r Public.TeamConversationList getTeamConversations zusr tid = do tm <- - E.getTeamMember tid zusr + TeamSubsystem.internalGetTeamMember zusr tid >>= noteS @'NotATeamMember unless (tm `hasPermission` GetTeamConversations) $ throwS @OperationDenied @@ -1016,8 +947,8 @@ getTeamConversation :: ( Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'NotATeamMember) r, Member (ErrorS OperationDenied) r, - Member TeamStore r, - Member ConversationStore r + Member ConversationStore r, + Member TeamSubsystem r ) => UserId -> TeamId -> @@ -1025,7 +956,7 @@ getTeamConversation :: Sem r Public.TeamConversation getTeamConversation zusr tid cid = do tm <- - E.getTeamMember tid zusr + TeamSubsystem.internalGetTeamMember zusr tid >>= noteS @'NotATeamMember unless (tm `hasPermission` GetTeamConversations) $ throwS @OperationDenied @@ -1042,12 +973,14 @@ deleteTeamConversation :: Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'NotATeamMember) r, Member (ErrorS ('ActionDenied 'Public.DeleteConversation)) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member ProposalStore r, Member ConversationSubsystem r, Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -1062,13 +995,13 @@ getSearchVisibility :: ( Member (ErrorS 'NotATeamMember) r, Member (ErrorS OperationDenied) r, Member SearchVisibilityStore r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> TeamId -> Sem r TeamSearchVisibilityView getSearchVisibility luid tid = do - zusrMembership <- E.getTeamMember tid (tUnqualified luid) + zusrMembership <- TeamSubsystem.internalGetTeamMember (tUnqualified luid) tid void $ permissionCheck ViewTeamSearchVisibility zusrMembership getSearchVisibilityInternal tid @@ -1078,7 +1011,7 @@ setSearchVisibility :: Member (ErrorS OperationDenied) r, Member (ErrorS 'TeamSearchVisibilityNotEnabled) r, Member SearchVisibilityStore r, - Member TeamStore r + Member TeamSubsystem r ) => (TeamId -> Sem r Bool) -> Local UserId -> @@ -1086,7 +1019,7 @@ setSearchVisibility :: Public.TeamSearchVisibilityView -> Sem r () setSearchVisibility availableForTeam luid tid req = do - zusrMembership <- E.getTeamMember tid (tUnqualified luid) + zusrMembership <- TeamSubsystem.internalGetTeamMember (tUnqualified luid) tid void $ permissionCheck ChangeTeamSearchVisibility zusrMembership setSearchVisibilityInternal availableForTeam tid req @@ -1107,7 +1040,7 @@ setSearchVisibility availableForTeam luid tid req = do withTeamIds :: (Member TeamStore r, Member (ListItems LegacyPaging TeamId) r) => UserId -> - Maybe (Either (Range 1 32 (List TeamId)) TeamId) -> + Maybe (Either (Range 1 32 (BSC.List TeamId)) TeamId) -> Range 1 100 Int32 -> (Bool -> [TeamId] -> Sem r a) -> Sem r a @@ -1119,7 +1052,7 @@ withTeamIds usr range size k = case range of r <- E.listItems usr (Just c) (rcast size) k (resultSetType r == ResultSetTruncated) (resultSetResult r) Just (Left (fromRange -> cc)) -> do - ids <- E.selectTeams usr (Data.ByteString.Conversion.fromList cc) + ids <- E.selectTeams usr (BSC.fromList cc) k False ids {-# INLINE withTeamIds #-} @@ -1185,9 +1118,10 @@ ensureNotTooLarge tid = do ensureNotTooLargeForLegalHold :: forall r. ( Member LegalHoldStore r, - Member TeamStore r, Member TeamFeatureStore r, - Member (ErrorS 'TooManyTeamMembersOnTeamWithLegalhold) r + Member (ErrorS 'TooManyTeamMembersOnTeamWithLegalhold) r, + Member (Input FanoutLimit) r, + Member (Input (FeatureDefaults LegalholdConfig)) r ) => TeamId -> Int -> @@ -1246,7 +1180,9 @@ addTeamMemberInternal tid origin originConn (ntmNewTeamMember -> new) = do getBindingTeamMembers :: ( Member (ErrorS 'TeamNotFound) r, Member (ErrorS 'NonBindingTeam) r, - Member TeamStore r + Member TeamStore r, + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => UserId -> Sem r TeamMemberList @@ -1271,9 +1207,10 @@ canUserJoinTeam :: forall r. ( Member BrigAPIAccess r, Member LegalHoldStore r, - Member TeamStore r, Member TeamFeatureStore r, - Member (ErrorS 'TooManyTeamMembersOnTeamWithLegalhold) r + Member (ErrorS 'TooManyTeamMembersOnTeamWithLegalhold) r, + Member (Input FanoutLimit) r, + Member (Input (FeatureDefaults LegalholdConfig)) r ) => TeamId -> Sem r () @@ -1311,7 +1248,7 @@ userIsTeamOwner :: Member (ErrorS 'AccessDenied) r, Member (ErrorS 'NotATeamMember) r, Member (Input (Local ())) r, - Member TeamStore r + Member TeamSubsystem r ) => TeamId -> UserId -> @@ -1346,9 +1283,9 @@ updateTeamCollaborator :: Member (ErrorS OperationDenied) r, Member (ErrorS NotATeamMember) r, Member P.TinyLog r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member ConversationSubsystem r + Member ConversationSubsystem r, + Member TeamSubsystem r ) => Local UserId -> TeamId -> @@ -1359,7 +1296,7 @@ updateTeamCollaborator lusr tid rusr perms = do P.debug $ Log.field "targets" (toByteString rusr) . Log.field "action" (Log.val "Teams.updateTeamCollaborator") - zusrMember <- E.getTeamMember tid (tUnqualified lusr) + zusrMember <- TeamSubsystem.internalGetTeamMember (tUnqualified lusr) tid void $ permissionCheck UpdateTeamCollaborator zusrMember when (Set.null $ Set.intersection (Set.fromList [Collaborator.CreateTeamConversation, Collaborator.ImplicitConnection]) perms) $ removeFromConvsAndPushConvLeaveEvent lusr Nothing tid rusr @@ -1378,7 +1315,9 @@ removeTeamCollaborator :: Member P.TinyLog r, Member TeamFeatureStore r, Member TeamStore r, - Member TeamCollaboratorsSubsystem r + Member TeamCollaboratorsSubsystem r, + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => Local UserId -> TeamId -> @@ -1388,7 +1327,7 @@ removeTeamCollaborator lusr tid rusr = do P.debug $ Log.field "targets" (toByteString rusr) . Log.field "action" (Log.val "Teams.removeTeamCollaborator") - zusrMember <- E.getTeamMember tid (tUnqualified lusr) + zusrMember <- TeamSubsystem.internalGetTeamMember (tUnqualified lusr) tid void $ permissionCheck RemoveTeamCollaborator zusrMember toNotify <- getFeatureForTeam @LimitedEventFanoutConfig tid diff --git a/services/galley/src/Galley/API/Teams/Export.hs b/services/galley/src/Galley/API/Teams/Export.hs index 229253d2eb7..51c99e5fbdb 100644 --- a/services/galley/src/Galley/API/Teams/Export.hs +++ b/services/galley/src/Galley/API/Teams/Export.hs @@ -30,9 +30,7 @@ import Data.Id import Data.Map qualified as Map import Data.Qualified (Local, tUnqualified) import Galley.Effects -import Galley.Effects.SparAccess qualified as Spar import Galley.Effects.TeamMemberStore (listTeamMembers) -import Galley.Effects.TeamStore import Imports hiding (atomicModifyIORef, newEmptyMVar, newIORef, putMVar, readMVar, takeMVar, threadDelay, tryPutMVar) import Polysemy import Polysemy.Async @@ -48,6 +46,9 @@ import Wire.Sem.Concurrency import Wire.Sem.Concurrency.IO import Wire.Sem.Paging qualified as E import Wire.Sem.Paging.Cassandra (InternalPaging) +import Wire.SparAPIAccess qualified as Spar +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem -- | Cache of inviter handles. -- @@ -84,7 +85,7 @@ lookupInviter cache uid = flip onException ensureCache $ do getUserRecord :: ( Member BrigAPIAccess r, - Member Spar.SparAccess r, + Member SparAPIAccess r, Member (ErrorS TeamMemberNotFound) r, Member (Final IO) r, Member Resource r @@ -121,15 +122,15 @@ getTeamMembersCSV :: ( Member BrigAPIAccess r, Member (ErrorS 'AccessDenied) r, Member (TeamMemberStore InternalPaging) r, - Member TeamStore r, Member (Final IO) r, - Member SparAccess r + Member SparAPIAccess r, + Member TeamSubsystem r ) => Local UserId -> TeamId -> Sem r LowLevelStreamingBody getTeamMembersCSV lusr tid = do - getTeamMember tid (tUnqualified lusr) >>= \case + TeamSubsystem.internalGetTeamMember (tUnqualified lusr) tid >>= \case Nothing -> throwS @'AccessDenied Just member -> unless (member `hasPermission` DownloadTeamMembersCsv) $ throwS @'AccessDenied diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index d5f4ff10fd1..3804928ee57 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -44,12 +44,12 @@ import Galley.API.Error (InternalError) import Galley.API.LegalHold qualified as LegalHold import Galley.API.LegalHold.Team qualified as LegalHold import Galley.API.Teams.Features.Get -import Galley.API.Util (assertTeamExists, getTeamMembersForFanout, membersToRecipients, permissionCheck) +import Galley.API.Util (assertTeamExists, getTeamMembersForFanout, permissionCheck) import Galley.App import Galley.Effects import Galley.Effects.SearchVisibilityStore qualified as SearchVisibilityData import Galley.Effects.TeamFeatureStore -import Galley.Effects.TeamStore (getLegalHoldFlag, getTeamMember) +import Galley.Env (FanoutLimit) import Galley.Options import Galley.Types.Teams import Imports @@ -62,17 +62,21 @@ import Wire.API.Conversation.Role (Action (RemoveConversationMember)) import Wire.API.Error (ErrorS) import Wire.API.Error.Galley import Wire.API.Event.FeatureConfig +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.Team.Feature import Wire.API.Team.Member import Wire.BrigAPIAccess (updateSearchVisibilityInbound) import Wire.ConversationStore (MLSCommitLockStore) import Wire.ConversationSubsystem +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) import Wire.NotificationSubsystem import Wire.Sem.Now (Now) import Wire.Sem.Paging import Wire.Sem.Paging.Cassandra import Wire.TeamCollaboratorsSubsystem +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem patchFeatureInternal :: forall cfg r. @@ -84,7 +88,9 @@ patchFeatureInternal :: Member TeamStore r, Member TeamFeatureStore r, Member P.TinyLog r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => TeamId -> LockableFeaturePatch cfg -> @@ -118,17 +124,18 @@ setFeature :: Member (ErrorS OperationDenied) r, Member (Error TeamFeatureError) r, Member (Input Opts) r, - Member TeamStore r, Member TeamFeatureStore r, Member P.TinyLog r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => UserId -> TeamId -> Feature cfg -> Sem r (LockableFeature cfg) setFeature uid tid feat = do - zusrMembership <- getTeamMember tid uid + zusrMembership <- TeamSubsystem.internalGetTeamMember uid tid void $ permissionCheck ChangeTeamFeature zusrMembership setFeatureUnchecked tid feat @@ -143,7 +150,9 @@ setFeatureInternal :: Member TeamStore r, Member TeamFeatureStore r, Member P.TinyLog r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => TeamId -> Feature cfg -> @@ -159,10 +168,11 @@ setFeatureUnchecked :: SetFeatureForTeamConstraints cfg r, Member (Error TeamFeatureError) r, Member (Input Opts) r, - Member TeamStore r, Member TeamFeatureStore r, Member (P.Logger (Log.Msg -> Log.Msg)) r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => TeamId -> Feature cfg -> @@ -205,8 +215,9 @@ pushFeatureEvent :: forall cfg r. ( IsFeatureConfig cfg, Member NotificationSubsystem r, - Member TeamStore r, - Member P.TinyLog r + Member P.TinyLog r, + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => TeamId -> Event -> @@ -243,7 +254,8 @@ setFeatureForTeam :: Member P.TinyLog r, Member NotificationSubsystem r, Member TeamFeatureStore r, - Member TeamStore r + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => TeamId -> LockableFeature cfg -> @@ -324,11 +336,12 @@ instance SetFeatureConfig LegalholdConfig where Member (ErrorS 'UserLegalHoldIllegalOperation) r, Member (ErrorS 'LegalHoldCouldNotBlockConnections) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member FireAndForget r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input (Local ())) r, + Member (Input (FeatureDefaults LegalholdConfig)) r, Member (Input Env) r, Member Now r, Member LegalHoldStore r, @@ -340,14 +353,17 @@ instance SetFeatureConfig LegalholdConfig where Member Random r, Member (Embed IO) r, Member TeamCollaboratorsSubsystem r, - Member MLSCommitLockStore r + Member MLSCommitLockStore r, + Member (Input FanoutLimit) r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) prepareFeature tid feat = do -- this extra do is to encapsulate the assertions running before the actual operation. -- enabling LH for teams is only allowed in normal operation; disabled-permanently and -- whitelist-teams have no or their own way to do that, resp. - featureLegalHold <- getLegalHoldFlag + featureLegalHold <- input @(FeatureDefaults LegalholdConfig) case featureLegalHold of FeatureLegalHoldDisabledByDefault -> do pure () @@ -451,6 +467,15 @@ instance SetFeatureConfig DomainRegistrationConfig instance SetFeatureConfig CellsConfig +instance SetFeatureConfig CellsInternalConfig where + type + SetFeatureForTeamConstraints CellsInternalConfig r = + (Member (Error TeamFeatureError) r) + + prepareFeature _ feat = do + unless (feat.status == FeatureStatusEnabled && feat.lockStatus == LockStatusUnlocked) $ do + throw InvalidStatusUpdate + instance SetFeatureConfig ConsumableNotificationsConfig instance SetFeatureConfig ChatBubblesConfig @@ -460,3 +485,7 @@ instance SetFeatureConfig AppsConfig instance SetFeatureConfig SimplifiedUserConnectionRequestQRCodeConfig instance SetFeatureConfig StealthUsersConfig + +instance SetFeatureConfig MeetingsConfig + +instance SetFeatureConfig MeetingsPremiumConfig diff --git a/services/galley/src/Galley/API/Teams/Features/Get.hs b/services/galley/src/Galley/API/Teams/Features/Get.hs index fd19148afa6..e99b035ccc9 100644 --- a/services/galley/src/Galley/API/Teams/Features/Get.hs +++ b/services/galley/src/Galley/API/Teams/Features/Get.hs @@ -47,7 +47,6 @@ import Galley.API.LegalHold.Team import Galley.API.Util import Galley.Effects import Galley.Effects.TeamFeatureStore -import Galley.Effects.TeamStore (getOneUserTeam, getTeamMember) import Galley.Options import Galley.Types.Teams import Imports @@ -61,6 +60,9 @@ import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti qualified as Mul import Wire.API.Team.Feature import Wire.BrigAPIAccess (getAccountConferenceCallingConfigClient) import Wire.ConversationStore as ConversationStore +import Wire.TeamStore qualified as TeamStore +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem data DoAuth = DoAuth UserId | DontDoAuth @@ -118,13 +120,13 @@ getFeature :: Member (Input Opts) r, Member TeamFeatureStore r, Member (ErrorS 'NotATeamMember) r, - Member TeamStore r + Member TeamSubsystem r ) => UserId -> TeamId -> Sem r (LockableFeature cfg) getFeature uid tid = do - void $ getTeamMember tid uid >>= noteS @'NotATeamMember + void $ TeamSubsystem.internalGetTeamMember uid tid >>= noteS @'NotATeamMember getFeatureForTeam @cfg tid getFeatureInternal :: @@ -147,14 +149,15 @@ toTeamStatus tid feat = Multi.TeamStatus tid feat.status getTeamAndCheckMembership :: ( Member TeamStore r, Member (ErrorS 'NotATeamMember) r, - Member (ErrorS 'TeamNotFound) r + Member (ErrorS 'TeamNotFound) r, + Member TeamSubsystem r ) => UserId -> Sem r (Maybe TeamId) getTeamAndCheckMembership uid = do - mTid <- getOneUserTeam uid + mTid <- TeamStore.getOneUserTeam uid for_ mTid $ \tid -> do - zusrMembership <- getTeamMember tid uid + zusrMembership <- TeamSubsystem.internalGetTeamMember uid tid void $ maybe (throwS @'NotATeamMember) pure zusrMembership assertTeamExists tid pure mTid @@ -165,13 +168,15 @@ getAllTeamFeaturesForTeam :: Member (ErrorS 'NotATeamMember) r, Member LegalHoldStore r, Member TeamFeatureStore r, - Member TeamStore r + Member TeamStore r, + Member (Input (FeatureDefaults LegalholdConfig)) r, + Member TeamSubsystem r ) => Local UserId -> TeamId -> Sem r AllTeamFeatures getAllTeamFeaturesForTeam luid tid = do - void $ getTeamMember tid (tUnqualified luid) >>= noteS @'NotATeamMember + void $ TeamSubsystem.internalGetTeamMember (tUnqualified luid) tid >>= noteS @'NotATeamMember getAllTeamFeatures tid class @@ -196,7 +201,8 @@ getAllTeamFeatures :: ( Member (Input Opts) r, Member LegalHoldStore r, Member TeamFeatureStore r, - Member TeamStore r + Member TeamStore r, + Member (Input (FeatureDefaults LegalholdConfig)) r ) => TeamId -> Sem r AllTeamFeatures @@ -225,7 +231,9 @@ getAllTeamFeaturesForUser :: Member (Input Opts) r, Member LegalHoldStore r, Member TeamFeatureStore r, - Member TeamStore r + Member TeamStore r, + Member (Input (FeatureDefaults LegalholdConfig)) r, + Member TeamSubsystem r ) => UserId -> Sem r AllTeamFeatures @@ -244,7 +252,8 @@ getSingleFeatureForUser :: Member TeamStore r, Member TeamFeatureStore r, GetFeatureForUserConstraints cfg r, - ComputeFeatureConstraints cfg r + ComputeFeatureConstraints cfg r, + Member TeamSubsystem r ) => UserId -> Sem r (LockableFeature cfg) @@ -310,13 +319,17 @@ instance GetFeatureConfig LegalholdConfig where Member TeamFeatureStore r, Member LegalHoldStore r, Member TeamStore r, + Member (Input (FeatureDefaults LegalholdConfig)) r, Member (ErrorS OperationDenied) r, Member (ErrorS 'NotATeamMember) r, Member (ErrorS 'TeamNotFound) r ) type ComputeFeatureConstraints LegalholdConfig r = - (Member TeamStore r, Member LegalHoldStore r) + ( Member TeamStore r, + Member LegalHoldStore r, + Member (Input (FeatureDefaults LegalholdConfig)) r + ) computeFeature tid defFeature dbFeature = do status <- computeLegalHoldFeatureStatus tid dbFeature @@ -399,6 +412,8 @@ instance GetFeatureConfig DomainRegistrationConfig instance GetFeatureConfig CellsConfig +instance GetFeatureConfig CellsInternalConfig + instance GetFeatureConfig AllowedGlobalOperationsConfig instance GetFeatureConfig AssetAuditLogConfig @@ -413,6 +428,10 @@ instance GetFeatureConfig SimplifiedUserConnectionRequestQRCodeConfig instance GetFeatureConfig StealthUsersConfig +instance GetFeatureConfig MeetingsConfig + +instance GetFeatureConfig MeetingsPremiumConfig + -- | 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.) diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index 8888ac375c1..ee4220a500b 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -1,19 +1,3 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE RecordWildCards #-} @@ -118,8 +102,7 @@ import Galley.Data.Types import Galley.Effects import Galley.Effects.ClientStore qualified as E import Galley.Effects.CodeStore qualified as E -import Galley.Effects.FederatorAccess qualified as E -import Galley.Effects.TeamStore qualified as E +import Galley.Env import Galley.Options import Imports hiding (forkIO) import Polysemy @@ -140,6 +123,7 @@ import Wire.API.Event.Conversation import Wire.API.Event.LeaveReason import Wire.API.Federation.API 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 (ZHostValue) @@ -152,7 +136,9 @@ import Wire.API.User.Client import Wire.API.UserGroup import Wire.ConversationStore qualified as E import Wire.ConversationSubsystem +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig) import Wire.ExternalAccess qualified as E +import Wire.FederationAPIAccess qualified as E import Wire.HashPassword as HashPassword import Wire.NotificationSubsystem import Wire.RateLimit @@ -160,6 +146,8 @@ import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now import Wire.StoredConversation import Wire.TeamCollaboratorsSubsystem +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem import Wire.UserGroupStore (UserGroupStore, getUserGroupsForConv) import Wire.UserList @@ -288,11 +276,12 @@ type UpdateConversationAccessEffects = ErrorS 'InvalidOperation, ErrorS 'InvalidTargetAccess, ExternalAccess, - FederatorAccess, + FederationAPIAccess FederatorClient, FireAndForget, NotificationSubsystem, ConversationSubsystem, Input Env, + Input ConversationSubsystemConfig, ProposalStore, Random, TeamStore, @@ -303,7 +292,8 @@ updateConversationAccess :: ( Members UpdateConversationAccessEffects r, Member Now r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -319,7 +309,8 @@ updateConversationAccessUnqualified :: ( Members UpdateConversationAccessEffects r, Member Now r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -345,14 +336,15 @@ updateConversationReceiptMode :: Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'MLSReadReceiptsNotAllowed) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input (Local ())) r, Member TinyLog r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -380,7 +372,7 @@ updateRemoteConversation :: forall tag r. ( Member BrigAPIAccess r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member (Input (Local ())) r, Member ConversationStore r, @@ -426,14 +418,15 @@ updateConversationReceiptModeUnqualified :: Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'MLSReadReceiptsNotAllowed) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input (Local ())) r, Member TinyLog r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -450,9 +443,10 @@ updateConversationMessageTimer :: Member (ErrorS 'InvalidOperation) r, Member (Error FederationError) r, Member ConversationSubsystem r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -483,9 +477,10 @@ updateConversationMessageTimerUnqualified :: Member (ErrorS 'InvalidOperation) r, Member (Error FederationError) r, Member ConversationSubsystem r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -504,12 +499,14 @@ deleteLocalConversation :: Member (ErrorS ('ActionDenied 'DeleteConversation)) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member ConversationSubsystem r, Member ProposalStore r, Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -535,7 +532,7 @@ addCodeUnqualifiedWithReqBody :: Member (Input Opts) r, Member TeamFeatureStore r, Member RateLimit r, - Member TeamStore r + Member TeamSubsystem r ) => UserId -> Maybe Text -> @@ -561,7 +558,7 @@ addCodeUnqualified :: Member HashPassword r, Member TeamFeatureStore r, Member RateLimit r, - Member TeamStore r + Member TeamSubsystem r ) => Maybe CreateConversationCodeRequest -> UserId -> @@ -589,7 +586,7 @@ addCode :: Member (Input Opts) r, Member TeamFeatureStore r, Member RateLimit r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> Maybe ZHostValue -> @@ -600,7 +597,7 @@ addCode :: addCode lusr mbZHost mZcon lcnv mReq = do conv <- E.getConversation (tUnqualified lcnv) >>= noteS @'ConvNotFound Query.ensureGuestLinksEnabled (convTeam conv) - mTeamMember <- maybe (pure Nothing) (flip E.getTeamMember (tUnqualified lusr)) conv.metadata.cnvmTeam + mTeamMember <- maybe (pure Nothing) (TeamSubsystem.internalGetTeamMember (tUnqualified lusr)) conv.metadata.cnvmTeam Query.ensureConvAdmin conv (tUnqualified lusr) mTeamMember ensureAccess conv CodeAccess ensureGuestsOrNonTeamMembersAllowed conv @@ -639,7 +636,7 @@ rmCodeUnqualified :: Member NotificationSubsystem r, Member (Input (Local ())) r, Member Now r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -657,7 +654,7 @@ rmCode :: Member ExternalAccess r, Member NotificationSubsystem r, Member Now r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -666,7 +663,7 @@ rmCode :: rmCode lusr zcon lcnv = do conv <- E.getConversation (tUnqualified lcnv) >>= noteS @'ConvNotFound - mTeamMember <- maybe (pure Nothing) (flip E.getTeamMember (tUnqualified lusr)) conv.metadata.cnvmTeam + mTeamMember <- maybe (pure Nothing) (TeamSubsystem.internalGetTeamMember (tUnqualified lusr)) conv.metadata.cnvmTeam Query.ensureConvAdmin conv (tUnqualified lusr) mTeamMember ensureAccess conv CodeAccess let (bots, users) = localBotsAndUsers $ conv.localMembers @@ -747,13 +744,14 @@ updateConversationProtocolWithLocalUser :: Member NotificationSubsystem r, Member ConversationSubsystem r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member Random r, Member ProposalStore r, Member TeamFeatureStore r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -787,7 +785,6 @@ updateChannelAddPermission :: Member ExternalAccess r, Member NotificationSubsystem r, Member ConversationSubsystem r, - Member TeamStore r, Member (Input (Local ())) r, Member TinyLog r, Member (ErrorS (MissingPermission Nothing)) r, @@ -796,10 +793,12 @@ updateChannelAddPermission :: Member (Error NonFederatingBackends) r, Member (Error UnreachableBackends) r, Member BrigAPIAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member (ErrorS 'InvalidTargetAccess) r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -837,10 +836,11 @@ joinConversationByReusableCode :: Member (ErrorS 'TooManyMembers) r, Member ConversationSubsystem r, Member (Input Opts) r, - Member TeamStore r, Member TeamFeatureStore r, Member HashPassword r, - Member RateLimit r + Member RateLimit r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -862,8 +862,8 @@ joinConversationById :: Member (ErrorS 'NotATeamMember) r, Member (ErrorS 'TooManyMembers) r, Member ConversationSubsystem r, - Member (Input Opts) r, - Member TeamStore r + Member (Input ConversationSubsystemConfig) r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -881,9 +881,9 @@ joinConversation :: Member (ErrorS 'NotATeamMember) r, Member (ErrorS 'TooManyMembers) r, Member ConversationSubsystem r, - Member (Input Opts) r, + Member (Input ConversationSubsystemConfig) r, Member ConversationStore r, - Member TeamStore r + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -940,11 +940,9 @@ addMembers :: Member (Error NonFederatingBackends) r, Member (Error UnreachableBackends) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member ConversationSubsystem r, Member NotificationSubsystem r, - Member (Input Env) r, - Member (Input Opts) r, Member Now r, Member LegalHoldStore r, Member ProposalStore r, @@ -952,7 +950,9 @@ addMembers :: Member TeamStore r, Member TinyLog r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -967,7 +967,7 @@ addMembers lusr zcon qcnv (InviteQualified users role) = do mapErrorS @OperationDenied @('ActionDenied 'AddConversationMember) $ forM_ conv.metadata.cnvmTeam $ \tid -> do forM_ users $ \u -> do - mTeamMembership <- E.getTeamMember tid $ qUnqualified u + mTeamMembership <- TeamSubsystem.internalGetTeamMember (qUnqualified u) tid forM_ (mTeamMembership >>= permissionsRole . Wire.API.Team.Member.getPermissions) $ permissionCheck JoinRegularConversations . Just @@ -995,11 +995,9 @@ addMembersUnqualifiedV2 :: Member (Error NonFederatingBackends) r, Member (Error UnreachableBackends) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member ConversationSubsystem r, Member NotificationSubsystem r, - Member (Input Env) r, - Member (Input Opts) r, Member Now r, Member LegalHoldStore r, Member ProposalStore r, @@ -1007,7 +1005,9 @@ addMembersUnqualifiedV2 :: Member TeamStore r, Member TinyLog r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -1039,11 +1039,9 @@ addMembersUnqualified :: Member (Error NonFederatingBackends) r, Member (Error UnreachableBackends) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member ConversationSubsystem r, Member NotificationSubsystem r, - Member (Input Env) r, - Member (Input Opts) r, Member Now r, Member LegalHoldStore r, Member ProposalStore r, @@ -1051,7 +1049,9 @@ addMembersUnqualified :: Member TeamStore r, Member TinyLog r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -1086,10 +1086,9 @@ replaceMembers :: Member (Error NonFederatingBackends) r, Member (Error UnreachableBackends) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member (Input Env) r, - Member (Input Opts) r, Member Now r, Member LegalHoldStore r, Member ProposalStore r, @@ -1099,7 +1098,9 @@ replaceMembers :: Member TeamCollaboratorsSubsystem r, Member E.MLSCommitLockStore r, Member UserGroupStore r, - Member ConversationSubsystem r + Member ConversationSubsystem r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -1115,7 +1116,7 @@ replaceMembers lusr zcon qcnv (InviteQualified invitedUsers role) = do mapErrorS @OperationDenied @('ActionDenied 'AddConversationMember) $ forM_ conv.metadata.cnvmTeam $ \tid -> do forM_ invitedUsers $ \u -> do - mTeamMembership <- E.getTeamMember tid $ qUnqualified u + mTeamMembership <- TeamSubsystem.internalGetTeamMember (qUnqualified u) tid forM_ (mTeamMembership >>= permissionsRole . Wire.API.Team.Member.getPermissions) $ permissionCheck JoinRegularConversations . Just @@ -1226,9 +1227,10 @@ updateOtherMemberLocalConv :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvMemberNotFound) r, Member ConversationSubsystem r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local ConvId -> Local UserId -> @@ -1252,9 +1254,10 @@ updateOtherMemberUnqualified :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvMemberNotFound) r, Member ConversationSubsystem r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -1277,9 +1280,10 @@ updateOtherMember :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvMemberNotFound) r, Member ConversationSubsystem r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -1310,7 +1314,7 @@ removeMemberUnqualified :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input Env) r, @@ -1318,9 +1322,10 @@ removeMemberUnqualified :: Member ProposalStore r, Member Random r, Member TinyLog r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -1341,7 +1346,7 @@ removeMemberQualified :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input Env) r, @@ -1349,9 +1354,10 @@ removeMemberQualified :: Member ProposalStore r, Member Random r, Member TinyLog r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -1373,7 +1379,8 @@ pattern EdMembersLeaveRemoved :: QualifiedUserIdList -> EventData pattern EdMembersLeaveRemoved l = EdMembersLeave EdReasonRemoved l removeMemberFromRemoteConv :: - ( Member FederatorAccess r, + ( Member (FederationAPIAccess FederatorClient) r, + Member (Error FederationError) r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, Member (ErrorS 'ConvNotFound) r, Member Now r @@ -1419,7 +1426,7 @@ removeMemberFromLocalConv :: Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Input Env) r, @@ -1427,9 +1434,10 @@ removeMemberFromLocalConv :: Member ProposalStore r, Member Random r, Member TinyLog r, - Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local ConvId -> Local UserId -> @@ -1463,13 +1471,12 @@ removeMemberFromLocalConv lcnv lusr con victim removeMemberFromChannel :: forall r. ( Member (ErrorS 'ConvNotFound) r, - Member TeamStore r, Member (Input Env) r, Member (Error NoChanges) r, Member ProposalStore r, Member Now r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member NotificationSubsystem r, Member ConversationSubsystem r, Member (Error InternalError) r, @@ -1477,7 +1484,9 @@ removeMemberFromChannel :: Member TinyLog r, Member (Error FederationError) r, Member BackendNotificationQueueAccess r, - Member ConversationStore r + Member ConversationStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Qualified UserId -> Local StoredConversation -> @@ -1493,7 +1502,7 @@ removeMemberFromChannel qusr lconv victim = do kickMember qusr lconv notificationTargets victim where getTeamMembership :: StoredConversation -> Local UserId -> Sem r (Maybe TeamMember) - getTeamMembership conv luid = maybe (pure Nothing) (`E.getTeamMember` tUnqualified luid) conv.metadata.cnvmTeam + getTeamMembership conv luid = maybe (pure Nothing) (TeamSubsystem.internalGetTeamMember (tUnqualified luid)) conv.metadata.cnvmTeam -- OTR @@ -1501,14 +1510,15 @@ postProteusMessage :: ( Member BrigAPIAccess r, Member ClientStore r, Member ConversationStore r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, + Member (Error FederationError) r, Member BackendNotificationQueueAccess r, Member NotificationSubsystem r, Member ExternalAccess r, Member (Input Opts) r, Member Now r, - Member TeamStore r, - Member TinyLog r + Member TinyLog r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1533,7 +1543,9 @@ postProteusBroadcast :: Member (Input Opts) r, Member Now r, Member TeamStore r, - Member TinyLog r + Member TinyLog r, + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1577,14 +1589,14 @@ postBotMessageUnqualified :: Member ClientStore r, Member ConversationStore r, Member ExternalAccess r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member BackendNotificationQueueAccess r, Member NotificationSubsystem r, Member (Input (Local ())) r, Member (Input Opts) r, - Member TeamStore r, Member TinyLog r, - Member Now r + Member Now r, + Member TeamSubsystem r ) => BotId -> ConvId -> @@ -1613,7 +1625,9 @@ postOtrBroadcastUnqualified :: Member (Input Opts) r, Member Now r, Member TeamStore r, - Member TinyLog r + Member TinyLog r, + Member (Input FanoutLimit) r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1630,14 +1644,14 @@ postOtrMessageUnqualified :: ( Member BrigAPIAccess r, Member ClientStore r, Member ConversationStore r, - Member FederatorAccess r, + Member (FederationAPIAccess FederatorClient) r, Member BackendNotificationQueueAccess r, Member ExternalAccess r, Member NotificationSubsystem r, Member (Input Opts) r, Member Now r, - Member TeamStore r, - Member TinyLog r + Member TinyLog r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1663,7 +1677,9 @@ updateConversationName :: Member ConversationSubsystem r, Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -1689,7 +1705,9 @@ updateUnqualifiedConversationName :: Member ConversationSubsystem r, Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -1711,7 +1729,9 @@ updateLocalConversationName :: Member ConversationSubsystem r, Member TeamStore r, Member TeamCollaboratorsSubsystem r, - Member E.MLSCommitLockStore r + Member E.MLSCommitLockStore r, + Member TeamSubsystem r, + Member (Input ConversationSubsystemConfig) r ) => Local UserId -> ConnId -> @@ -1728,8 +1748,9 @@ memberTyping :: Member (Input (Local ())) r, Member Now r, Member ConversationStore r, - Member FederatorAccess r, - Member TeamStore r + Member (FederationAPIAccess FederatorClient) r, + Member (Error FederationError) r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1766,8 +1787,9 @@ memberTypingUnqualified :: Member (Input (Local ())) r, Member Now r, Member ConversationStore r, - Member FederatorAccess r, - Member TeamStore r + Member (FederationAPIAccess FederatorClient) r, + Member (Error FederationError) r, + Member TeamSubsystem r ) => Local UserId -> ConnId -> @@ -1788,7 +1810,7 @@ addBot :: Member (ErrorS 'TooManyMembers) r, Member ExternalAccess r, Member NotificationSubsystem r, - Member (Input Opts) r, + Member (Input ConversationSubsystemConfig) r, Member Now r ) => Local UserId -> diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index 52d37be558f..aa6a22995e4 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -20,7 +20,7 @@ module Galley.API.Util where -import Control.Lens (to, view, (^.)) +import Control.Lens (view, (^.)) import Control.Monad.Extra (allM, anyM) import Control.Monad.Trans.Maybe import Data.Bifunctor @@ -46,11 +46,7 @@ import Galley.Data.Types qualified as DataTypes import Galley.Effects import Galley.Effects.ClientStore import Galley.Effects.CodeStore -import Galley.Effects.FederatorAccess -import Galley.Effects.LegalHoldStore -import Galley.Effects.TeamStore -import Galley.Effects.TeamStore qualified as E -import Galley.Options +import Galley.Env import Galley.Types.Clients (Clients, fromUserClients) import Galley.Types.Conversations.Roles import Galley.Types.Teams @@ -72,6 +68,7 @@ import Wire.API.Error.Galley import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Galley +import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.Federation.Version import Wire.API.MLS.Group.Serialisation @@ -80,7 +77,6 @@ import Wire.API.Routes.Public.Galley.Conversation import Wire.API.Routes.Public.Util import Wire.API.Team.Collaborator import Wire.API.Team.Collaborator qualified as CollaboratorPermission (CollaboratorPermission (..)) -import Wire.API.Team.Feature import Wire.API.Team.Member import Wire.API.Team.Member qualified as Mem import Wire.API.Team.Member.Error @@ -91,15 +87,22 @@ import Wire.API.VersionInfo import Wire.BackendNotificationQueueAccess import Wire.BrigAPIAccess import Wire.ConversationStore +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig (..)) import Wire.ExternalAccess +import Wire.FederationAPIAccess import Wire.HashPassword (HashPassword) import Wire.HashPassword qualified as HashPassword +import Wire.LegalHoldStore import Wire.NotificationSubsystem import Wire.RateLimit import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now import Wire.StoredConversation as Data import Wire.TeamCollaboratorsSubsystem +import Wire.TeamStore +import Wire.TeamSubsystem (TeamSubsystem) +import Wire.TeamSubsystem qualified as TeamSubsystem +import Wire.TeamSubsystem qualified as TeamSubsytem import Wire.UserList data NoChanges = NoChanges @@ -130,7 +133,8 @@ ensureConnectedOrSameTeam :: ( Member BrigAPIAccess r, Member (ErrorS 'NotConnected) r, Member TeamStore r, - Member TeamCollaboratorsSubsystem r + Member TeamCollaboratorsSubsystem r, + Member TeamSubsystem r ) => Local UserId -> [Qualified UserId] -> @@ -151,7 +155,8 @@ ensureConnectedToLocalsOrSameTeam :: ( Member BrigAPIAccess r, Member (ErrorS 'NotConnected) r, Member TeamStore r, - Member TeamCollaboratorsSubsystem r + Member TeamCollaboratorsSubsystem r, + Member TeamSubsystem r ) => Local UserId -> [UserId] -> @@ -163,7 +168,7 @@ ensureConnectedToLocalsOrSameTeam (tUnqualified -> u) uids = do icUsers <- getTeamCollaborators uTeams -- We collect all the relevant uids from same teams as the origin user sameTeamUids <- forM (uTeams `union` icTeams) $ \team -> - fmap (view Mem.userId) <$> selectTeamMembers team uids + fmap (view Mem.userId) <$> TeamSubsytem.internalSelectTeamMembers team uids -- Do not check connections for users that are on the same team ensureConnectedToLocals u ((uids \\ join sameTeamUids) \\ icUsers) where @@ -298,7 +303,7 @@ ensureConvRoleNotElevated origMember targetRole = do checkGroupIdSupport :: ( Member (ErrorS GroupIdVersionNotSupported) r, - Member FederatorAccess r + Member (FederationAPIAccess FederatorClient) r ) => Local x -> StoredConversation -> @@ -318,7 +323,7 @@ checkGroupIdSupport loc conv joinAction = void $ runMaybeT $ do -- check that each remote backend is compatible with group ID version >= 2 let (_, remoteUsers) = partitionQualified loc joinAction.users lift - . (failOnFirstError <=< runFederatedConcurrentlyEither @_ @Brig remoteUsers) + . (failOnFirstError <=< runFederatedConcurrentlyEither @_ @_ @Brig remoteUsers) $ \_ -> do guardVersion $ \fedV -> fedV >= groupIdFedVersion GroupIdVersion2 where @@ -381,13 +386,13 @@ assertTeamExists tid = do assertOnTeam :: ( Member (ErrorS 'NotATeamMember) r, - Member TeamStore r + Member TeamSubsystem r ) => UserId -> TeamId -> Sem r () assertOnTeam uid tid = - getTeamMember tid uid >>= \case + TeamSubsystem.internalGetTeamMember uid tid >>= \case Nothing -> throwS @'NotATeamMember Just _ -> pure () @@ -441,12 +446,6 @@ acceptOne2One lusr conv conn = do acceptConnectConversation cid pure $ Data.convSetType One2OneConv conv -localMemberToRecipient :: LocalMember -> Recipient -localMemberToRecipient = userRecipient . (.id_) - -userRecipient :: UserId -> Recipient -userRecipient u = Recipient u PushV2.RecipientClientsAll - memberJoinEvent :: Local UserId -> Qualified ConvId -> @@ -601,25 +600,6 @@ bmFromMembers lmems rusers = case localBotsAndUsers lmems of convBotsAndMembers :: StoredConversation -> BotsAndMembers convBotsAndMembers conv = bmFromMembers (conv.localMembers) (conv.remoteMembers) -localBotsAndUsers :: (Foldable f) => f LocalMember -> ([BotMember], [LocalMember]) -localBotsAndUsers = foldMap botOrUser - where - botOrUser m = case m.service of - -- we drop invalid bots here, which shouldn't happen - Just _ -> (toList (newBotMember m), []) - Nothing -> ([], [m]) - -nonTeamMembers :: [LocalMember] -> [TeamMember] -> [LocalMember] -nonTeamMembers cm tm = filter (not . isMemberOfTeam . (.id_)) cm - where - -- FUTUREWORK: remote members: teams and their members are always on the same backend - isMemberOfTeam = \case - uid -> isTeamMember uid tm - -membersToRecipients :: Maybe UserId -> [TeamMember] -> [Recipient] -membersToRecipients Nothing = map (userRecipient . view Mem.userId) -membersToRecipients (Just u) = map userRecipient . filter (/= u) . map (view Mem.userId) - getSelfMemberFromLocals :: (Foldable t, Member (ErrorS 'ConvNotFound) r) => UserId -> @@ -660,7 +640,7 @@ getConversationAsMember :: ( Member ConversationStore r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvAccessDenied) r, - Member TeamStore r + Member TeamSubsystem r ) => Qualified UserId -> Local ConvId -> @@ -676,7 +656,7 @@ getConversationAsViewer :: ( Member ConversationStore r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'ConvAccessDenied) r, - Member TeamStore r + Member TeamSubsystem r ) => Qualified UserId -> Local ConvId -> @@ -692,7 +672,7 @@ getConversationAsViewer qusr lcnv = do =<< runMaybeT ( do uid <- hoistMaybe $ foldQualified lcnv (Just . tUnqualified) (const Nothing) qusr - tm <- MaybeT $ E.getTeamMember tid uid + tm <- MaybeT $ TeamSubsystem.internalGetTeamMember uid tid guard $ hasManageChannelsPermission c tm ) (Nothing, Nothing) -> throwAccessDenied @@ -786,7 +766,7 @@ ensureConversationAccess :: ( Member BrigAPIAccess r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'NotATeamMember) r, - Member TeamStore r + Member TeamSubsystem r ) => UserId -> StoredConversation -> @@ -794,7 +774,7 @@ ensureConversationAccess :: Sem r () ensureConversationAccess zusr conv access = do ensureAccess conv access - zusrMembership <- maybe (pure Nothing) (`getTeamMember` zusr) (Data.convTeam conv) + zusrMembership <- maybe (pure Nothing) (TeamSubsystem.internalGetTeamMember zusr) (Data.convTeam conv) ensureAccessRole (Data.convAccessRoles conv) [(zusr, zusrMembership)] ensureAccess :: @@ -938,7 +918,7 @@ registerRemoteConversationMemberships :: Member (Error UnreachableBackends) r, Member (Error FederationError) r, Member BackendNotificationQueueAccess r, - Member FederatorAccess r + Member (FederationAPIAccess FederatorClient) r ) => -- | The time stamp when the conversation was created UTCTime -> @@ -1049,7 +1029,7 @@ consentGiven = \case UserLegalHoldNoConsent -> ConsentNotGiven checkConsent :: - (Member TeamStore r) => + (Member TeamSubsystem r) => Map UserId TeamId -> UserId -> Sem r ConsentGiven @@ -1059,7 +1039,7 @@ checkConsent teamsOfUsers other = do -- Get legalhold status of user. Defaults to 'defUserLegalHoldStatus' if user -- doesn't belong to a team. getLHStatus :: - (Member TeamStore r) => + (Member TeamSubsystem r) => Maybe TeamId -> UserId -> Sem r UserLegalHoldStatus @@ -1067,18 +1047,19 @@ getLHStatus teamOfUser other = do case teamOfUser of Nothing -> pure defUserLegalHoldStatus Just team -> do - mMember <- getTeamMember team other + mMember <- TeamSubsystem.internalGetTeamMember other team pure $ maybe defUserLegalHoldStatus (view legalHoldStatus) mMember anyLegalholdActivated :: - ( Member (Input Opts) r, - Member TeamStore r + ( Member (Input ConversationSubsystemConfig) r, + Member TeamStore r, + Member TeamSubsystem r ) => [UserId] -> Sem r Bool anyLegalholdActivated uids = do - opts <- input - case view (settings . featureFlags . to npProject) opts of + cfg <- input + case legalholdDefaults cfg of FeatureLegalHoldDisabledPermanently -> pure False FeatureLegalHoldDisabledByDefault -> check FeatureLegalHoldWhitelistTeamsAndImplicitConsent -> check @@ -1089,15 +1070,16 @@ anyLegalholdActivated uids = do anyM (\uid -> userLHEnabled <$> getLHStatus (Map.lookup uid teamsOfUsers) uid) uidsPage allLegalholdConsentGiven :: - ( Member (Input Opts) r, + ( Member (Input ConversationSubsystemConfig) r, Member LegalHoldStore r, - Member TeamStore r + Member TeamStore r, + Member TeamSubsystem r ) => [UserId] -> Sem r Bool allLegalholdConsentGiven uids = do - opts <- input - case view (settings . featureFlags . to npProject) opts of + cfg <- input + case legalholdDefaults cfg of FeatureLegalHoldDisabledPermanently -> pure False FeatureLegalHoldDisabledByDefault -> do flip allM (chunksOf 32 uids) $ \uidsPage -> do @@ -1116,7 +1098,7 @@ allLegalholdConsentGiven uids = do -- | Add to every uid the legalhold status getLHStatusForUsers :: - (Member TeamStore r) => + (Member TeamStore r, Member TeamSubsystem r) => [UserId] -> Sem r [(UserId, UserLegalHoldStatus)] getLHStatusForUsers uids = @@ -1129,15 +1111,20 @@ getLHStatusForUsers uids = (uid,) <$> getLHStatus (Map.lookup uid teamsOfUsers) uid ) -getTeamMembersForFanout :: (Member TeamStore r) => TeamId -> Sem r TeamMemberList +getTeamMembersForFanout :: + ( Member (Input FanoutLimit) r, + Member TeamSubsystem r + ) => + TeamId -> + Sem r TeamMemberList getTeamMembersForFanout tid = do - lim <- fanoutLimit - getTeamMembersWithLimit tid lim + lim <- input + TeamSubsystem.internalGetTeamMembersWithLimit tid (Just lim) ensureMemberLimit :: ( Foldable f, ( Member (ErrorS 'TooManyMembers) r, - Member (Input Opts) r + Member (Input ConversationSubsystemConfig) r ) ) => ProtocolTag -> @@ -1146,8 +1133,8 @@ ensureMemberLimit :: Sem r () ensureMemberLimit ProtocolMLSTag _ _ = pure () ensureMemberLimit _ old new = do - o <- input - let maxSize = fromIntegral (o ^. settings . maxConvSize) + cfg <- input + let maxSize = fromIntegral (cfg.maxConvSize) when (length old + length new > maxSize) $ throwS @'TooManyMembers diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index 471b92b44b2..488d692aa7a 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -53,26 +53,27 @@ import Data.Qualified import Data.Range import Data.Text qualified as Text import Galley.API.Error -import Galley.Aws qualified as Aws import Galley.Cassandra.Client import Galley.Cassandra.Code import Galley.Cassandra.CustomBackend -import Galley.Cassandra.LegalHold -import Galley.Cassandra.Proposal import Galley.Cassandra.SearchVisibility import Galley.Cassandra.Team + ( interpretInternalTeamListToCassandra, + interpretTeamListToCassandra, + interpretTeamMemberStoreToCassandra, + interpretTeamMemberStoreToCassandraWithPaging, + ) import Galley.Cassandra.TeamFeatures import Galley.Cassandra.TeamNotifications import Galley.Effects import Galley.Env -import Galley.Intra.Effects -import Galley.Intra.Federator +import Galley.External.LegalHoldService.Internal qualified as LHInternal import Galley.Keys +import Galley.Monad (runApp) import Galley.Options hiding (brig, endpoint, federator) import Galley.Options qualified as O import Galley.Queue import Galley.Queue qualified as Q -import Galley.TeamSubsystem (interpretTeamSubsystem) import Galley.Types.Teams import HTTP2.Client.Manager (Http2Manager, http2ManagerWithSSLCtx) import Hasql.Pool qualified as Hasql @@ -104,27 +105,38 @@ import Wire.API.Error import Wire.API.Federation.Error import Wire.API.Team.Collaborator import Wire.API.Team.Feature +import Wire.AWS qualified as Aws import Wire.BackendNotificationQueueAccess.RabbitMq qualified as BackendNotificationQueueAccess import Wire.BrigAPIAccess.Rpc import Wire.ConversationStore.Cassandra import Wire.ConversationStore.Postgres -import Wire.ConversationSubsystem.Interpreter (interpretConversationSubsystem) +import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig (..), interpretConversationSubsystem) import Wire.Error import Wire.ExternalAccess.External +import Wire.FederationAPIAccess.Interpreter import Wire.FireAndForget import Wire.GundeckAPIAccess (runGundeckAPIAccess) import Wire.HashPassword.Interpreter +import Wire.LegalHoldStore.Cassandra (interpretLegalHoldStoreToCassandra) +import Wire.LegalHoldStore.Env (LegalHoldEnv (..)) import Wire.NotificationSubsystem.Interpreter (runNotificationSubsystemGundeck) import Wire.ParseException +import Wire.ProposalStore.Cassandra import Wire.RateLimit import Wire.RateLimit.Interpreter import Wire.Rpc +import Wire.Sem.Concurrency +import Wire.Sem.Concurrency.IO import Wire.Sem.Delay import Wire.Sem.Now.IO (nowToIO) import Wire.Sem.Random.IO import Wire.ServiceStore.Cassandra (interpretServiceStoreToCassandra) +import Wire.SparAPIAccess.Rpc import Wire.TeamCollaboratorsStore.Postgres (interpretTeamCollaboratorsStoreToPostgres) import Wire.TeamCollaboratorsSubsystem.Interpreter +import Wire.TeamJournal.Aws +import Wire.TeamStore.Cassandra (interpretTeamStoreToCassandra) +import Wire.TeamSubsystem.Interpreter import Wire.UserGroupStore.Postgres (interpretUserGroupStoreToPostgres) -- Effects needed by the interpretation of other effects @@ -132,6 +144,7 @@ type GalleyEffects0 = '[ Input ClientState, Input Hasql.Pool, Input Env, + Input ConversationSubsystemConfig, Error MigrationError, Error InvalidInput, Error ParseException, @@ -150,6 +163,7 @@ type GalleyEffects0 = Embed IO, Error JSONResponse, Resource, + Concurrency 'Unsafe, Final IO ] @@ -160,7 +174,7 @@ validateOptions :: Opts -> IO (Either HttpsUrl (Map Text HttpsUrl)) validateOptions o = do let settings' = view settings o optFanoutLimit = fromIntegral . fromRange $ currentFanoutLimit o - when (settings' ^. maxConvSize > fromIntegral optFanoutLimit) $ + when (settings'._maxConvSize > fromIntegral optFanoutLimit) $ error "setMaxConvSize cannot be > setTruncationLimit" when (settings' ^. maxTeamSize < optFanoutLimit) $ error "setMaxTeamSize cannot be < setTruncationLimit" @@ -193,7 +207,7 @@ createEnv o l = do Env (RequestId defRequestId) o l mgr h2mgr (o ^. O.federator) (o ^. O.brig) cass postgres <$> Q.new 16000 <*> initExtEnv disableTlsV1 - <*> maybe (pure Nothing) (fmap Just . Aws.mkEnv l mgr) (o ^. journal) + <*> maybe (pure Nothing) (\jo -> fmap Just (Aws.mkEnv l mgr (jo ^. O.endpoint) (jo ^. O.queueName))) (o ^. journal) <*> traverse loadAllMLSKeys (o ^. settings . mlsPrivateKeyPaths) <*> traverse (mkRabbitMqChannelMVar l (Just "galley")) (o ^. rabbitmq) <*> pure codeURIcfg @@ -274,6 +288,10 @@ evalGalley e = MigrationToPostgresql -> interpretConversationStoreToCassandraAndPostgres (e ^. cstate) PostgresqlStorage -> interpretConversationStoreToPostgres localUnit = toLocalUnsafe (e ^. options . settings . federationDomain) () + teamSubsystemConfig = + TeamSubsystemConfig + { concurrentDeletionEvents = fromMaybe defConcurrentDeletionEvents e._options._settings._concurrentDeletionEvents + } backendNotificationQueueAccessEnv = case e._rabbitmqChannel of Nothing -> Nothing @@ -285,8 +303,23 @@ evalGalley e = BackendNotificationQueueAccess.local = localUnit, BackendNotificationQueueAccess.requestId = e ^. reqId } + federationAPIAccessConfig = + FederationAPIAccessConfig + { ownDomain = e._options._settings._federationDomain, + federatorEndpoint = e._options._federator, + http2Manager = e._http2Manager, + requestId = e._reqId + } + conversationSubsystemConfig = + ConversationSubsystemConfig + { mlsKeys = e._mlsKeys, + federationProtocols = e._options._settings._federationProtocols, + legalholdDefaults = lh, + maxConvSize = e._options._settings._maxConvSize + } in ExceptT . runFinal @IO + . unsafelyPerformConcurrency . resourceToIOFinal . runError . embedToFinal @IO @@ -303,6 +336,7 @@ evalGalley e = . mapError toResponse . mapError toResponse . logAndMapError toResponse (Text.pack . show) "migration error" + . runInputConst conversationSubsystemConfig . runInputConst e . runInputConst (e ^. hasqlPool) . runInputConst (e ^. cstate) @@ -316,6 +350,7 @@ evalGalley e = . runInputConst localUnit . interpretTeamFeatureSpecialContext e . runInputSem getAllTeamFeaturesForServer + . runInputConst (currentFanoutLimit (e ^. options)) . interpretInternalTeamListToCassandra . interpretTeamListToCassandra . interpretTeamMemberStoreToCassandraWithPaging lh @@ -323,12 +358,14 @@ evalGalley e = . interpretTeamFeatureStoreToCassandra . interpretMLSCommitLockStoreToCassandra (e ^. cstate) . convStoreInterpreter - . interpretTeamStoreToCassandra lh . interpretTeamNotificationStoreToCassandra . interpretServiceStoreToCassandra (e ^. cstate) . interpretUserGroupStoreToPostgres - . interpretSearchVisibilityStoreToCassandra + . runInputConst legalHoldEnv . interpretLegalHoldStoreToCassandra lh + . interpretTeamJournal (e ^. aEnv) + . interpretTeamStoreToCassandra + . interpretSearchVisibilityStoreToCassandra . interpretCustomBackendStoreToCassandra . randomToIO . runHashPassword e._options._settings._passwordHashingOptions @@ -339,22 +376,24 @@ evalGalley e = . interpretTeamCollaboratorsStoreToPostgres . interpretFireAndForget . BackendNotificationQueueAccess.interpretBackendNotificationQueueAccess backendNotificationQueueAccessEnv - . interpretFederatorAccess + . interpretFederationAPIAccess federationAPIAccessConfig . runRpcWithHttp (e ^. manager) (e ^. reqId) . runGundeckAPIAccess (e ^. options . gundeck) - . interpretTeamSubsystem . interpretBrigAccess (e ^. brig) . interpretExternalAccess (e ^. extEnv) . runNotificationSubsystemGundeck (notificationSubsystemConfig e) + . interpretSparAPIAccessToRpc (e ^. options . spar) + . interpretTeamSubsystem teamSubsystemConfig . interpretConversationSubsystem . interpretTeamCollaboratorsSubsystem - . interpretSparAccess where lh = view (options . settings . featureFlags . to npProject) e + legalHoldEnv = + let makeReq fpr url rb = runApp e (LHInternal.makeVerifiedRequest fpr url rb) + makeReqFresh fpr url rb = runApp e (LHInternal.makeVerifiedRequestFreshManager fpr url rb) + in LegalHoldEnv {makeVerifiedRequest = makeReq, makeVerifiedRequestFreshManager = makeReqFresh} -interpretTeamFeatureSpecialContext :: Env -> Sem (Input (Maybe [TeamId], FeatureDefaults LegalholdConfig) ': r) a -> Sem r a +interpretTeamFeatureSpecialContext :: Env -> Sem (Input (FeatureDefaults LegalholdConfig) ': r) a -> Sem r a interpretTeamFeatureSpecialContext e = runInputConst - ( e ^. options . settings . exposeInvitationURLsTeamAllowlist, - e ^. options . settings . featureFlags . to npProject - ) + (e ^. options . settings . featureFlags . to npProject) diff --git a/services/galley/src/Galley/Aws.hs b/services/galley/src/Galley/Aws.hs deleted file mode 100644 index 67963a7e908..00000000000 --- a/services/galley/src/Galley/Aws.hs +++ /dev/null @@ -1,194 +0,0 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE TemplateHaskell #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Galley.Aws - ( Env, - mkEnv, - awsEnv, - eventQueue, - QueueUrl (..), - Amazon, - execute, - enqueue, - - -- * Errors - Error (..), - ) -where - -import Amazonka qualified as AWS -import Amazonka.SQS qualified as SQS -import Amazonka.SQS.Lens qualified as SQS -import Control.Lens hiding ((.=)) -import Control.Monad.Catch -import Control.Monad.Trans.Resource -import Control.Retry (exponentialBackoff, limitRetries, retrying) -import Data.ByteString.Base64 qualified as B64 -import Data.ByteString.Builder (toLazyByteString) -import Data.ProtoLens.Encoding -import Data.Text.Encoding (decodeLatin1) -import Data.UUID (toText) -import Data.UUID.V4 -import Galley.Options -import Imports -import Network.HTTP.Client - ( HttpException (..), - HttpExceptionContent (..), - Manager, - ) -import Network.TLS qualified as TLS -import Proto.TeamEvents qualified as E -import System.Logger qualified as Logger -import System.Logger.Class -import Util.Options hiding (endpoint) - -newtype QueueUrl = QueueUrl Text - deriving (Show) - -data Error where - GeneralError :: (Show e, AWS.AsError e) => e -> Error - -deriving instance Show Error - -deriving instance Typeable Error - -instance Exception Error - -data Env = Env - { _awsEnv :: !AWS.Env, - _logger :: !Logger, - _eventQueue :: !QueueUrl - } - -makeLenses ''Env - -newtype Amazon a = Amazon - { unAmazon :: ReaderT Env (ResourceT IO) a - } - deriving - ( Functor, - Applicative, - Monad, - MonadIO, - MonadThrow, - MonadCatch, - MonadMask, - MonadReader Env, - MonadResource, - MonadUnliftIO - ) - -instance MonadLogger Amazon where - log l m = view logger >>= \g -> Logger.log g l m - -mkEnv :: Logger -> Manager -> JournalOpts -> IO Env -mkEnv lgr mgr opts = do - let g = Logger.clone (Just "aws.galley") lgr - e <- mkAwsEnv g - q <- getQueueUrl e (opts ^. queueName) - pure (Env e g q) - where - sqs e = AWS.setEndpoint (e ^. awsSecure) (e ^. awsHost) (e ^. awsPort) SQS.defaultService - mkAwsEnv g = do - baseEnv <- - AWS.newEnv AWS.discover - <&> AWS.configureService (sqs (opts ^. endpoint)) - pure $ - baseEnv - { AWS.logger = awsLogger g, - AWS.retryCheck = retryCheck, - AWS.manager = mgr - } - awsLogger g l = Logger.log g (mapLevel l) . Logger.msg . toLazyByteString - mapLevel AWS.Info = Logger.Info - -- Debug output from amazonka can be very useful for tracing requests - -- but is very verbose (and multiline which we don't handle well) - -- distracting from our own debug logs, so we map amazonka's 'Debug' - -- level to our 'Trace' level. - mapLevel AWS.Debug = Logger.Trace - mapLevel AWS.Trace = Logger.Trace - -- n.b. Errors are either returned or thrown. In both cases they will - -- already be logged if left unhandled. We don't want errors to be - -- logged inside amazonka already, before we even had a chance to handle - -- them, which results in distracting noise. For debugging purposes, - -- they are still revealed on debug level. - mapLevel AWS.Error = Logger.Debug - -- TODO: Remove custom retryCheck? Should be fixed since tls 1.3.9? - -- account occasional TLS handshake failures. - -- See: https://github.com/vincenthz/hs-tls/issues/124 - -- See: https://github.com/brendanhay/amazonka/issues/269 - retryCheck _ InvalidUrlException {} = False - retryCheck n (HttpExceptionRequest _ ex) = case ex of - _ | n >= 3 -> False - NoResponseDataReceived -> True - ConnectionTimeout -> True - ConnectionClosed -> True - ConnectionFailure _ -> True - InternalException x -> case fromException x of - Just TLS.HandshakeFailed {} -> True - _ -> False - _ -> False - getQueueUrl :: AWS.Env -> Text -> IO QueueUrl - getQueueUrl e q = do - x <- - runResourceT $ - AWS.trying AWS._Error $ - AWS.send e (SQS.newGetQueueUrl q) - either - (throwM . GeneralError) - (pure . QueueUrl . view SQS.getQueueUrlResponse_queueUrl) - x - -execute :: (MonadIO m) => Env -> Amazon a -> m a -execute e m = liftIO $ runResourceT (runReaderT (unAmazon m) e) - -enqueue :: E.TeamEvent -> Amazon () -enqueue e = do - QueueUrl url <- view eventQueue - rnd <- liftIO nextRandom - amaznkaEnv <- view awsEnv - res <- retrying (limitRetries 5 <> exponentialBackoff 1000000) (const canRetry) $ const (sendCatch amaznkaEnv (req url rnd)) - either (throwM . GeneralError) (const (pure ())) res - where - event = decodeLatin1 $ B64.encode $ encodeMessage e - req url dedup = - SQS.newSendMessage url event - & SQS.sendMessage_messageGroupId ?~ "team.events" - & SQS.sendMessage_messageDeduplicationId ?~ toText dedup - --------------------------------------------------------------------------------- --- Utilities - -sendCatch :: - ( AWS.AWSRequest r, - Typeable r, - Typeable (AWS.AWSResponse r) - ) => - AWS.Env -> - r -> - Amazon (Either AWS.Error (AWS.AWSResponse r)) -sendCatch e = AWS.trying AWS._Error . AWS.send e - -canRetry :: (MonadIO m) => Either AWS.Error a -> m Bool -canRetry (Right _) = pure False -canRetry (Left e) = case e of - AWS.TransportError (HttpExceptionRequest _ ResponseTimeout) -> pure True - AWS.ServiceError se | se ^. AWS.serviceError_code == AWS.ErrorCode "RequestThrottled" -> pure True - _ -> pure False diff --git a/services/galley/src/Galley/Cassandra/LegalHold.hs b/services/galley/src/Galley/Cassandra/LegalHold.hs deleted file mode 100644 index 5a7c30acfa0..00000000000 --- a/services/galley/src/Galley/Cassandra/LegalHold.hs +++ /dev/null @@ -1,197 +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 Galley.Cassandra.LegalHold - ( interpretLegalHoldStoreToCassandra, - isTeamLegalholdWhitelisted, - - -- * Used by tests - selectPendingPrekeys, - validateServiceKey, - ) -where - -import Brig.Types.Instances () -import Brig.Types.Team.LegalHold -import Cassandra -import Control.Exception.Enclosed (handleAny) -import Control.Lens (unsnoc) -import Data.ByteString.Conversion.To -import Data.ByteString.Lazy.Char8 qualified as LC8 -import Data.Id -import Data.LegalHold -import Data.Misc -import Galley.Cassandra.Queries qualified as Q -import Galley.Cassandra.Store -import Galley.Cassandra.Util -import Galley.Effects.LegalHoldStore (LegalHoldStore (..)) -import Galley.Env -import Galley.External.LegalHoldService.Internal -import Galley.Monad -import Galley.Types.Teams -import Imports hiding (unsnoc) -import OpenSSL.EVP.Digest qualified as SSL -import OpenSSL.EVP.PKey qualified as SSL -import OpenSSL.PEM qualified as SSL -import OpenSSL.RSA qualified as SSL -import Polysemy -import Polysemy.Input -import Polysemy.TinyLog -import Ssl.Util qualified as SSL -import Wire.API.Provider.Service -import Wire.API.Team.Feature -import Wire.API.User.Client.Prekey -import Wire.ConversationStore.Cassandra.Instances () - -interpretLegalHoldStoreToCassandra :: - ( Member (Embed IO) r, - Member (Input ClientState) r, - Member (Input Env) r, - Member TinyLog r - ) => - FeatureDefaults LegalholdConfig -> - Sem (LegalHoldStore ': r) a -> - Sem r a -interpretLegalHoldStoreToCassandra lh = interpret $ \case - CreateSettings s -> do - logEffect "LegalHoldStore.CreateSettings" - embedClient $ createSettings s - GetSettings tid -> do - logEffect "LegalHoldStore.GetSettings" - embedClient $ getSettings tid - RemoveSettings tid -> do - logEffect "LegalHoldStore.RemoveSettings" - embedClient $ removeSettings tid - InsertPendingPrekeys uid pkeys -> do - logEffect "LegalHoldStore.InsertPendingPrekeys" - embedClient $ insertPendingPrekeys uid pkeys - SelectPendingPrekeys uid -> do - logEffect "LegalHoldStore.SelectPendingPrekeys" - embedClient $ selectPendingPrekeys uid - DropPendingPrekeys uid -> do - logEffect "LegalHoldStore.DropPendingPrekeys" - embedClient $ dropPendingPrekeys uid - SetUserLegalHoldStatus tid uid st -> do - logEffect "LegalHoldStore.SetUserLegalHoldStatus" - embedClient $ setUserLegalHoldStatus tid uid st - SetTeamLegalholdWhitelisted tid -> do - logEffect "LegalHoldStore.SetTeamLegalholdWhitelisted" - embedClient $ setTeamLegalholdWhitelisted tid - UnsetTeamLegalholdWhitelisted tid -> do - logEffect "LegalHoldStore.UnsetTeamLegalholdWhitelisted" - embedClient $ unsetTeamLegalholdWhitelisted tid - IsTeamLegalholdWhitelisted tid -> do - logEffect "LegalHoldStore.IsTeamLegalholdWhitelisted" - embedClient $ isTeamLegalholdWhitelisted lh tid - -- FUTUREWORK: should this action be part of a separate effect? - MakeVerifiedRequestFreshManager fpr url r -> do - logEffect "LegalHoldStore.MakeVerifiedRequestFreshManager" - embedApp $ makeVerifiedRequestFreshManager fpr url r - MakeVerifiedRequest fpr url r -> do - logEffect "LegalHoldStore.MakeVerifiedRequest" - embedApp $ makeVerifiedRequest fpr url r - ValidateServiceKey sk -> do - logEffect "LegalHoldStore.ValidateServiceKey" - embed @IO $ validateServiceKey sk - --- | Returns 'False' if legal hold is not enabled for this team --- The Caller is responsible for checking whether legal hold is enabled for this team -createSettings :: (MonadClient m) => LegalHoldService -> m () -createSettings (LegalHoldService tid url fpr tok key) = do - retry x1 $ write Q.insertLegalHoldSettings (params LocalQuorum (url, fpr, tok, key, tid)) - --- | Returns 'Nothing' if no settings are saved --- The Caller is responsible for checking whether legal hold is enabled for this team -getSettings :: (MonadClient m) => TeamId -> m (Maybe LegalHoldService) -getSettings tid = - fmap toLegalHoldService <$> do - retry x1 $ query1 Q.selectLegalHoldSettings (params LocalQuorum (Identity tid)) - where - toLegalHoldService (httpsUrl, fingerprint, tok, key) = LegalHoldService tid httpsUrl fingerprint tok key - -removeSettings :: (MonadClient m) => TeamId -> m () -removeSettings tid = retry x5 (write Q.removeLegalHoldSettings (params LocalQuorum (Identity tid))) - -insertPendingPrekeys :: (MonadClient m) => UserId -> [Prekey] -> m () -insertPendingPrekeys uid keys = retry x5 . batch $ - forM_ keys $ - \key -> - addPrepQuery Q.insertPendingPrekeys (toTuple key) - where - toTuple (Prekey keyId key) = (uid, keyId, key) - -selectPendingPrekeys :: (MonadClient m) => UserId -> m (Maybe ([Prekey], LastPrekey)) -selectPendingPrekeys uid = - pickLastKey . fmap fromTuple - <$> retry x1 (query Q.selectPendingPrekeys (params LocalQuorum (Identity uid))) - where - fromTuple (keyId, key) = Prekey keyId key - pickLastKey allPrekeys = - case unsnoc allPrekeys of - Nothing -> Nothing - Just (keys, lst) -> pure (keys, lastPrekey . prekeyKey $ lst) - -dropPendingPrekeys :: (MonadClient m) => UserId -> m () -dropPendingPrekeys uid = retry x5 (write Q.dropPendingPrekeys (params LocalQuorum (Identity uid))) - -setUserLegalHoldStatus :: (MonadClient m) => TeamId -> UserId -> UserLegalHoldStatus -> m () -setUserLegalHoldStatus tid uid status = - retry x5 (write Q.updateUserLegalHoldStatus (params LocalQuorum (status, tid, uid))) - -setTeamLegalholdWhitelisted :: (MonadClient m) => TeamId -> m () -setTeamLegalholdWhitelisted tid = - retry x5 (write Q.insertLegalHoldWhitelistedTeam (params LocalQuorum (Identity tid))) - -unsetTeamLegalholdWhitelisted :: (MonadClient m) => TeamId -> m () -unsetTeamLegalholdWhitelisted tid = - retry x5 (write Q.removeLegalHoldWhitelistedTeam (params LocalQuorum (Identity tid))) - -isTeamLegalholdWhitelisted :: FeatureDefaults LegalholdConfig -> TeamId -> Client Bool -isTeamLegalholdWhitelisted FeatureLegalHoldDisabledPermanently _ = pure False -isTeamLegalholdWhitelisted FeatureLegalHoldDisabledByDefault _ = pure False -isTeamLegalholdWhitelisted FeatureLegalHoldWhitelistTeamsAndImplicitConsent tid = - isJust <$> (runIdentity <$$> retry x5 (query1 Q.selectLegalHoldWhitelistedTeam (params LocalQuorum (Identity tid)))) - --- | Copied unchanged from "Brig.Provider.API". Interpret a service certificate and extract --- key and fingerprint. (This only has to be in 'MonadIO' because the FFI in OpenSSL works --- like that.) --- --- FUTUREWORK: It would be nice to move (part of) this to ssl-util, but it has types from --- brig-types and types-common. -validateServiceKey :: (MonadIO m) => ServiceKeyPEM -> m (Maybe (ServiceKey, Fingerprint Rsa)) -validateServiceKey pem = - liftIO $ - readPublicKey >>= \pk -> - case SSL.toPublicKey =<< pk of - Nothing -> pure Nothing - Just pk' -> do - Just sha <- SSL.getDigestByName "SHA256" - let size = SSL.rsaSize (pk' :: SSL.RSAPubKey) - if size < minRsaKeySize - then pure Nothing - else do - fpr <- Fingerprint <$> SSL.rsaFingerprint sha pk' - let bits = fromIntegral size * 8 - let key = ServiceKey RsaServiceKey bits pem - pure (Just (key, fpr)) - where - readPublicKey = - handleAny - (const $ pure Nothing) - (SSL.readPublicKey (LC8.unpack (toByteString pem)) <&> Just) - minRsaKeySize :: Int - minRsaKeySize = 256 -- Bytes (= 2048 bits) diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index 9b078d222bb..3f9ca75a5a7 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -16,7 +16,6 @@ -- with this program. If not, see . -- | Tables that are used in this module: --- - billing_team_member -- - clients -- - conversation_codes -- - custom_backend @@ -24,9 +23,7 @@ -- - legalhold_service -- - legalhold_whitelisted -- - team --- - team_admin -- - team_member --- - user_team -- update using: `rg -i -P '(?:update|from|into)\s+([A-Za-z0-9_]+)' -or '$1' --no-line-number services/galley/src/Galley/Cassandra/Queries.hs | sort | uniq` module Galley.Cassandra.Queries ( selectCustomBackend, @@ -48,48 +45,14 @@ module Galley.Cassandra.Queries dropPendingPrekeys, selectPendingPrekeys, updateUserLegalHoldStatus, - selectLegalHoldWhitelistedTeam, insertLegalHoldWhitelistedTeam, removeLegalHoldWhitelistedTeam, - insertTeam, - listBillingTeamMembers, - listTeamAdmins, - selectTeamName, - selectUserTeamsFrom, - selectUserTeams, - selectTeamMember, - insertTeamMember, - insertUserTeam, - insertBillingTeamMember, - insertTeamAdmin, - updatePermissions, - deleteBillingTeamMember, - deleteTeamAdmin, - deleteTeamMember, - deleteUserTeam, - selectTeam, - selectUserTeamsIn, - selectTeamMembers, - selectOneUserTeam, - selectTeamBindingWritetime, - selectTeamBinding, - markTeamDeleted, - deleteTeam, - updateTeamStatus, - updateTeamName, - updateTeamIcon, - updateTeamIconKey, - updateTeamSplashScreen, - selectTeamMembersFrom, - selectTeamMembers', ) where import Cassandra as C hiding (Value) -import Cassandra.Util (Writetime) import Data.Domain (Domain) import Data.Id -import Data.Json.Util import Data.LegalHold import Data.Misc import Data.Text.Lazy qualified as LT @@ -100,159 +63,9 @@ import Wire.API.Conversation.Code import Wire.API.Password (Password) 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 import Wire.API.User.Client.Prekey --- Teams -------------------------------------------------------------------- - -selectTeam :: PrepQuery R (Identity TeamId) (UserId, Text, Icon, Maybe Text, Bool, Maybe TeamStatus, Maybe (Writetime TeamStatus), Maybe TeamBinding, Maybe Icon) -selectTeam = "select creator, name, icon, icon_key, deleted, status, writetime(status), binding, splash_screen from team where team = ?" - -selectTeamName :: PrepQuery R (Identity TeamId) (Identity Text) -selectTeamName = "select name from team where team = ?" - -selectTeamBinding :: PrepQuery R (Identity TeamId) (Identity (Maybe TeamBinding)) -selectTeamBinding = "select binding from team where team = ?" - -selectTeamBindingWritetime :: PrepQuery R (Identity TeamId) (Identity (Maybe Int64)) -selectTeamBindingWritetime = "select writetime(binding) from team where team = ?" - -selectTeamMember :: - PrepQuery - R - (TeamId, UserId) - ( Permissions, - Maybe UserId, - Maybe UTCTimeMillis, - Maybe UserLegalHoldStatus - ) -selectTeamMember = "select perms, invited_by, invited_at, legalhold_status from team_member where team = ? and user = ?" - -selectTeamMembersBase :: (IsString a) => [String] -> a -selectTeamMembersBase conds = fromString $ selectFrom <> " where team = ?" <> whereClause <> " order by user" - where - selectFrom = "select user, perms, invited_by, invited_at, legalhold_status from team_member" - whereClause = concatMap (" and " <>) conds - --- | This query fetches **all** members of a team, it should always be paginated -selectTeamMembers :: - PrepQuery - R - (Identity TeamId) - ( UserId, - Permissions, - Maybe UserId, - Maybe UTCTimeMillis, - Maybe UserLegalHoldStatus - ) -selectTeamMembers = selectTeamMembersBase [] - -selectTeamMembersFrom :: - PrepQuery - R - (TeamId, UserId) - ( UserId, - Permissions, - Maybe UserId, - Maybe UTCTimeMillis, - Maybe UserLegalHoldStatus - ) -selectTeamMembersFrom = selectTeamMembersBase ["user > ?"] - -selectTeamMembers' :: - PrepQuery - R - (TeamId, [UserId]) - ( UserId, - Permissions, - Writetime Permissions, - Maybe UserId, - Maybe UTCTimeMillis, - Maybe UserLegalHoldStatus - ) -selectTeamMembers' = - [r| - select user, perms, writetime(perms), invited_by, invited_at, legalhold_status - from team_member - where team = ? and user in ? order by user - |] - -selectUserTeams :: PrepQuery R (Identity UserId) (Identity TeamId) -selectUserTeams = "select team from user_team where user = ? order by team" - -selectOneUserTeam :: PrepQuery R (Identity UserId) (Identity TeamId) -selectOneUserTeam = "select team from user_team where user = ? limit 1" - -selectUserTeamsIn :: PrepQuery R (UserId, [TeamId]) (Identity TeamId) -selectUserTeamsIn = "select team from user_team where user = ? and team in ? order by team" - -selectUserTeamsFrom :: PrepQuery R (UserId, TeamId) (Identity TeamId) -selectUserTeamsFrom = "select team from user_team where user = ? and team > ? order by team" - -insertTeam :: PrepQuery W (TeamId, UserId, Text, Icon, Maybe Text, TeamStatus, TeamBinding) () -insertTeam = "insert into team (team, creator, name, icon, icon_key, deleted, status, binding) values (?, ?, ?, ?, ?, false, ?, ?)" - -insertTeamMember :: PrepQuery W (TeamId, UserId, Permissions, Maybe UserId, Maybe UTCTimeMillis) () -insertTeamMember = "insert into team_member (team, user, perms, invited_by, invited_at) values (?, ?, ?, ?, ?)" - -deleteTeamMember :: PrepQuery W (TeamId, UserId) () -deleteTeamMember = "delete from team_member where team = ? and user = ?" - -insertBillingTeamMember :: PrepQuery W (TeamId, UserId) () -insertBillingTeamMember = "insert into billing_team_member (team, user) values (?, ?)" - -deleteBillingTeamMember :: PrepQuery W (TeamId, UserId) () -deleteBillingTeamMember = "delete from billing_team_member where team = ? and user = ?" - -listBillingTeamMembers :: PrepQuery R (Identity TeamId) (Identity UserId) -listBillingTeamMembers = "select user from billing_team_member where team = ?" - -insertTeamAdmin :: PrepQuery W (TeamId, UserId) () -insertTeamAdmin = "insert into team_admin (team, user) values (?, ?)" - -deleteTeamAdmin :: PrepQuery W (TeamId, UserId) () -deleteTeamAdmin = "delete from team_admin where team = ? and user = ?" - -listTeamAdmins :: PrepQuery R (Identity TeamId) (Identity UserId) -listTeamAdmins = "select user from team_admin where team = ?" - --- | This is not an upsert, but we can't add `IF EXISTS` here, or cassandra will yell `Invalid --- "Batch with conditions cannot span multiple tables"` at us. So we make sure in the --- application logic to only call this if the user exists (in the handler, not entirely --- race-condition-proof, unfortunately). -updatePermissions :: PrepQuery W (Permissions, TeamId, UserId) () -updatePermissions = "update team_member set perms = ? where team = ? and user = ?" - -insertUserTeam :: PrepQuery W (UserId, TeamId) () -insertUserTeam = "insert into user_team (user, team) values (?, ?)" - -deleteUserTeam :: PrepQuery W (UserId, TeamId) () -deleteUserTeam = "delete from user_team where user = ? and team = ?" - -markTeamDeleted :: PrepQuery W (TeamStatus, TeamId) () -markTeamDeleted = {- `IF EXISTS`, but that requires benchmarking -} "update team set status = ? where team = ?" - -deleteTeam :: PrepQuery W (TeamStatus, TeamId) () -deleteTeam = {- `IF EXISTS`, but that requires benchmarking -} "update team using timestamp 32503680000000000 set name = 'default', icon = 'default', status = ? where team = ? " - -updateTeamName :: PrepQuery W (Text, TeamId) () -updateTeamName = {- `IF EXISTS`, but that requires benchmarking -} "update team set name = ? where team = ?" - -updateTeamIcon :: PrepQuery W (Text, TeamId) () -updateTeamIcon = {- `IF EXISTS`, but that requires benchmarking -} "update team set icon = ? where team = ?" - -updateTeamIconKey :: PrepQuery W (Text, TeamId) () -updateTeamIconKey = {- `IF EXISTS`, but that requires benchmarking -} "update team set icon_key = ? where team = ?" - -updateTeamStatus :: PrepQuery W (TeamStatus, TeamId) () -updateTeamStatus = {- `IF EXISTS`, but that requires benchmarking -} "update team set status = ? where team = ?" - -updateTeamSplashScreen :: PrepQuery W (Text, TeamId) () -updateTeamSplashScreen = {- `IF EXISTS`, but that requires benchmarking -} "update team set splash_screen = ? where team = ?" - -- Conversations accessible by code ----------------------------------------- insertCode :: PrepQuery W (Key, Value, ConvId, Scope, Maybe Password, Int32) () @@ -336,12 +149,6 @@ updateUserLegalHoldStatus = where team = ? and user = ? |] -selectLegalHoldWhitelistedTeam :: PrepQuery R (Identity TeamId) (Identity TeamId) -selectLegalHoldWhitelistedTeam = - [r| - select team from legalhold_whitelisted where team = ? - |] - insertLegalHoldWhitelistedTeam :: PrepQuery W (Identity TeamId) () insertLegalHoldWhitelistedTeam = [r| diff --git a/services/galley/src/Galley/Cassandra/Team.hs b/services/galley/src/Galley/Cassandra/Team.hs index 7a7296ff070..40513789326 100644 --- a/services/galley/src/Galley/Cassandra/Team.hs +++ b/services/galley/src/Galley/Cassandra/Team.hs @@ -16,8 +16,7 @@ -- with this program. If not, see . module Galley.Cassandra.Team - ( interpretTeamStoreToCassandra, - interpretTeamMemberStoreToCassandra, + ( interpretTeamMemberStoreToCassandra, interpretTeamListToCassandra, interpretInternalTeamListToCassandra, interpretTeamMemberStoreToCassandraWithPaging, @@ -25,142 +24,28 @@ module Galley.Cassandra.Team where import Cassandra -import Cassandra.Util import Control.Exception (ErrorCall (ErrorCall)) import Control.Lens hiding ((<|)) import Control.Monad.Catch (throwM) import Control.Monad.Extra (ifM) -import Data.ByteString.Conversion (toByteString') -import Data.Id as Id -import Data.Json.Util (UTCTimeMillis (..), toUTCTimeMillis) +import Data.Id +import Data.Json.Util (UTCTimeMillis (..)) import Data.LegalHold (UserLegalHoldStatus (..), defUserLegalHoldStatus) -import Data.Map.Strict qualified as Map import Data.Range -import Data.Set qualified as Set -import Data.Text.Encoding -import Data.UUID.V4 (nextRandom) -import Galley.Aws qualified as Aws -import Galley.Cassandra.LegalHold (isTeamLegalholdWhitelisted) -import Galley.Cassandra.Queries qualified as Cql import Galley.Cassandra.Store import Galley.Cassandra.Util import Galley.Effects.TeamMemberStore -import Galley.Effects.TeamStore (TeamStore (..)) -import Galley.Env -import Galley.Monad -import Galley.Options -import Galley.Types.Teams +import Galley.Types.Teams (FeatureDefaults (..)) import Imports hiding (Set, max) import Polysemy import Polysemy.Input import Polysemy.TinyLog -import UnliftIO qualified -import Wire.API.Routes.Internal.Galley.TeamsIntra -import Wire.API.Team import Wire.API.Team.Feature import Wire.API.Team.Member -import Wire.API.Team.Member.Info (TeamMemberInfo (TeamMemberInfo)) -import Wire.API.Team.Member.Info qualified as Info -import Wire.API.Team.Permission (Perm (SetBilling), Permissions, self) -import Wire.ConversationStore (ConversationStore) -import Wire.ConversationStore qualified as E +import Wire.API.Team.Permission (Permissions) import Wire.ListItems import Wire.Sem.Paging.Cassandra - -interpretTeamStoreToCassandra :: - ( Member (Embed IO) r, - Member (Input Env) r, - Member (Input ClientState) r, - Member TinyLog r, - Member ConversationStore r - ) => - FeatureDefaults LegalholdConfig -> - Sem (TeamStore ': r) a -> - Sem r a -interpretTeamStoreToCassandra lh = interpret $ \case - CreateTeamMember tid mem -> do - logEffect "TeamStore.CreateTeamMember" - embedClient (addTeamMember tid mem) - SetTeamMemberPermissions perm0 tid uid perm1 -> do - logEffect "TeamStore.SetTeamMemberPermissions" - embedClient (updateTeamMember perm0 tid uid perm1) - CreateTeam t uid n i k b -> do - logEffect "TeamStore.CreateTeam" - createTeam t uid n i k b - DeleteTeamMember tid uid -> do - logEffect "TeamStore.DeleteTeamMember" - embedClient (removeTeamMember tid uid) - GetBillingTeamMembers tid -> do - logEffect "TeamStore.GetBillingTeamMembers" - embedClient (listBillingTeamMembers tid) - GetTeamAdmins tid -> do - logEffect "TeamStore.GetTeamAdmins" - embedClient (listTeamAdmins tid) - GetTeam tid -> do - logEffect "TeamStore.GetTeam" - embedClient (team tid) - GetTeamName tid -> do - logEffect "TeamStore.GetTeamName" - embedClient (getTeamName tid) - SelectTeams uid tids -> do - logEffect "TeamStore.SelectTeams" - embedClient (teamIdsOf uid tids) - GetTeamMember tid uid -> do - logEffect "TeamStore.GetTeamMember" - embedClient (teamMember lh tid uid) - GetTeamMembersWithLimit tid n -> do - logEffect "TeamStore.GetTeamMembersWithLimit" - embedClient (teamMembersWithLimit lh tid n) - GetTeamMembers tid -> do - logEffect "TeamStore.GetTeamMembers" - embedClient (teamMembersCollectedWithPagination lh tid) - SelectTeamMembers tid uids -> do - logEffect "TeamStore.SelectTeamMembers" - embedClient (teamMembersLimited lh tid uids) - SelectTeamMemberInfos tid uids -> do - logEffect "TeamStore.SelectTeamMemberInfos" - embedClient (teamMemberInfos tid uids) - GetUserTeams uid -> do - logEffect "TeamStore.GetUserTeams" - embedClient (userTeams uid) - GetUsersTeams uids -> do - logEffect "TeamStore.GetUsersTeams" - embedClient (usersTeams uids) - GetOneUserTeam uid -> do - logEffect "TeamStore.GetOneUserTeam" - embedClient (oneUserTeam uid) - GetTeamsBindings tid -> do - logEffect "TeamStore.GetTeamsBindings" - embedClient (getTeamsBindings tid) - GetTeamBinding tid -> do - logEffect "TeamStore.GetTeamBinding" - embedClient (getTeamBinding tid) - GetTeamCreationTime tid -> do - logEffect "TeamStore.GetTeamCreationTime" - embedClient (teamCreationTime tid) - DeleteTeam tid -> do - logEffect "TeamStore.DeleteTeam" - deleteTeam tid - SetTeamData tid upd -> do - logEffect "TeamStore.SetTeamData" - embedClient (updateTeam tid upd) - SetTeamStatus tid st -> do - logEffect "TeamStore.SetTeamStatus" - embedClient (updateTeamStatus tid st) - FanoutLimit -> do - logEffect "TeamStore.FanoutLimit" - embedApp (currentFanoutLimit <$> view options) - GetLegalHoldFlag -> do - logEffect "TeamStore.GetLegalHoldFlag" - view (options . settings . featureFlags . to npProject) <$> input - EnqueueTeamEvent e -> do - logEffect "TeamStore.EnqueueTeamEvent" - menv <- inputs (view aEnv) - for_ menv $ \env -> - embed @IO (Aws.execute env (Aws.enqueue e)) - SelectTeamMembersPaginated tid uids mps lim -> do - logEffect "TeamStore.SelectTeamMembersPaginated" - embedClient (selectSomeTeamMembersPaginated lh tid uids mps lim) +import Wire.TeamStore.Cassandra.Queries qualified as Cql interpretTeamListToCassandra :: ( Member (Embed IO) r, @@ -220,41 +105,6 @@ interpretTeamMemberStoreToCassandraWithPaging lh = interpret $ \case logEffect "TeamMemberStore.ListTeamMembers" embedClient $ teamMembersPageFrom lh tid mps lim -createTeam :: - ( Member (Input ClientState) r, - Member (Embed IO) r - ) => - Maybe TeamId -> - UserId -> - Range 1 256 Text -> - Icon -> - Maybe (Range 1 256 Text) -> - TeamBinding -> - Sem r Team -createTeam t uid (fromRange -> n) i k b = do - tid <- embed @IO $ maybe (Id <$> liftIO nextRandom) pure t - - embedClient $ retry x5 $ write Cql.insertTeam (params LocalQuorum (tid, uid, n, i, fromRange <$> k, initialStatus b, b)) - pure (newTeam tid uid n i b & teamIconKey .~ (fromRange <$> k)) - where - initialStatus Binding = PendingActive -- Team becomes Active after User account activation - initialStatus NonBinding = Active - -listBillingTeamMembers :: TeamId -> Client [UserId] -listBillingTeamMembers tid = - fmap runIdentity - <$> retry x1 (query Cql.listBillingTeamMembers (params LocalQuorum (Identity tid))) - -listTeamAdmins :: TeamId -> Client [UserId] -listTeamAdmins tid = - fmap runIdentity - <$> retry x1 (query Cql.listTeamAdmins (params LocalQuorum (Identity tid))) - -getTeamName :: TeamId -> Client (Maybe Text) -getTeamName tid = - fmap runIdentity - <$> retry x1 (query1 Cql.selectTeamName (params LocalQuorum (Identity tid))) - teamIdsFrom :: UserId -> Maybe TeamId -> Range 1 100 Int32 -> Client (ResultSet TeamId) teamIdsFrom usr range (fromRange -> max) = mkResultSet . fmap runIdentity . strip <$> case range of @@ -269,228 +119,6 @@ teamIdsForPagination usr range (fromRange -> max) = Just c -> paginate Cql.selectUserTeamsFrom (paramsP LocalQuorum (usr, c) max) Nothing -> paginate Cql.selectUserTeams (paramsP LocalQuorum (Identity usr) max) -teamMember :: FeatureDefaults LegalholdConfig -> TeamId -> UserId -> Client (Maybe TeamMember) -teamMember lh t u = - newTeamMember'' u =<< retry x1 (query1 Cql.selectTeamMember (params LocalQuorum (t, u))) - where - newTeamMember'' :: - UserId -> - Maybe (Permissions, Maybe UserId, Maybe UTCTimeMillis, Maybe UserLegalHoldStatus) -> - Client (Maybe TeamMember) - newTeamMember'' _ Nothing = pure Nothing - newTeamMember'' uid (Just (perms, minvu, minvt, mulhStatus)) = - Just <$> newTeamMember' lh t (uid, perms, minvu, minvt, mulhStatus) - -addTeamMember :: TeamId -> TeamMember -> Client () -addTeamMember t m = - retry x5 . batch $ do - setType BatchLogged - setConsistency LocalQuorum - addPrepQuery - Cql.insertTeamMember - ( t, - m ^. userId, - m ^. permissions, - m ^? invitation . _Just . _1, - m ^? invitation . _Just . _2 - ) - addPrepQuery Cql.insertUserTeam (m ^. userId, t) - - when (m `hasPermission` SetBilling) $ - addPrepQuery Cql.insertBillingTeamMember (t, m ^. userId) - - when (isAdminOrOwner (m ^. permissions)) $ - addPrepQuery Cql.insertTeamAdmin (t, m ^. userId) - -updateTeamMember :: - -- | Old permissions, used for maintaining 'billing_team_member' and 'team_admin' tables - Permissions -> - TeamId -> - UserId -> - -- | New permissions - Permissions -> - Client () -updateTeamMember oldPerms tid uid newPerms = do - retry x5 . batch $ do - setType BatchLogged - setConsistency LocalQuorum - addPrepQuery Cql.updatePermissions (newPerms, tid, uid) - - -- update billing_team_member table - let permDiff = Set.difference `on` self - acquiredPerms = newPerms `permDiff` oldPerms - lostPerms = oldPerms `permDiff` newPerms - - when (SetBilling `Set.member` acquiredPerms) $ - addPrepQuery Cql.insertBillingTeamMember (tid, uid) - when (SetBilling `Set.member` lostPerms) $ - addPrepQuery Cql.deleteBillingTeamMember (tid, uid) - - -- update team_admin table - let wasAdmin = isAdminOrOwner oldPerms - isAdmin = isAdminOrOwner newPerms - - when (isAdmin && not wasAdmin) $ - addPrepQuery Cql.insertTeamAdmin (tid, uid) - - when (not isAdmin && wasAdmin) $ - addPrepQuery Cql.deleteTeamAdmin (tid, uid) - -removeTeamMember :: TeamId -> UserId -> Client () -removeTeamMember t m = - retry x5 . batch $ do - setType BatchLogged - setConsistency LocalQuorum - addPrepQuery Cql.deleteTeamMember (t, m) - addPrepQuery Cql.deleteUserTeam (m, t) - addPrepQuery Cql.deleteBillingTeamMember (t, m) - addPrepQuery Cql.deleteTeamAdmin (t, m) - -team :: TeamId -> Client (Maybe TeamData) -team tid = - fmap toTeam <$> retry x1 (query1 Cql.selectTeam (params LocalQuorum (Identity tid))) - where - toTeam (u, n, i, k, d, s, st, b, ss) = - let t = newTeam tid u n i (fromMaybe NonBinding b) & teamIconKey .~ k & teamSplashScreen .~ fromMaybe DefaultIcon ss - status = if d then PendingDelete else fromMaybe Active s - in TeamData t status (writetimeToUTC <$> st) - -teamIdsOf :: UserId -> [TeamId] -> Client [TeamId] -teamIdsOf usr tids = - map runIdentity <$> retry x1 (query Cql.selectUserTeamsIn (params LocalQuorum (usr, toList tids))) - -teamMembersWithLimit :: - FeatureDefaults LegalholdConfig -> - TeamId -> - Range 1 HardTruncationLimit Int32 -> - Client TeamMemberList -teamMembersWithLimit lh t (fromRange -> limit) = do - -- NOTE: We use +1 as size and then trim it due to the semantics of C* when getting a page with the exact same size - pageTuple <- retry x1 (paginate Cql.selectTeamMembers (paramsP LocalQuorum (Identity t) (limit + 1))) - ms <- mapM (newTeamMember' lh t) . take (fromIntegral limit) $ result pageTuple - pure $ - if hasMore pageTuple - then newTeamMemberList ms ListTruncated - else newTeamMemberList ms ListComplete - --- NOTE: Use this function with care... should only be required when deleting a team! --- Maybe should be left explicitly for the caller? -teamMembersCollectedWithPagination :: FeatureDefaults LegalholdConfig -> TeamId -> Client [TeamMember] -teamMembersCollectedWithPagination lh tid = do - mems <- teamMembersForPagination tid Nothing (unsafeRange 2000) - collectTeamMembersPaginated [] mems - where - collectTeamMembersPaginated acc mems = do - tMembers <- mapM (newTeamMember' lh tid) (result mems) - if hasMore mems - then collectTeamMembersPaginated (tMembers ++ acc) =<< nextPage mems - else pure (tMembers ++ acc) - --- Lookup only specific team members: this is particularly useful for large teams when --- needed to look up only a small subset of members (typically 2, user to perform the action --- and the target user) -teamMembersLimited :: FeatureDefaults LegalholdConfig -> TeamId -> [UserId] -> Client [TeamMember] -teamMembersLimited lh t u = - mapM (\(uid, perms, _, minvu, minvt, mlh) -> newTeamMember' lh t (uid, perms, minvu, minvt, mlh)) - =<< retry x1 (query Cql.selectTeamMembers' (params LocalQuorum (t, u))) - -teamMemberInfos :: TeamId -> [UserId] -> Client [TeamMemberInfo] -teamMemberInfos t u = - mkTeamMemberInfo - <$$> retry x1 (query Cql.selectTeamMembers' (params LocalQuorum (t, u))) - where - mkTeamMemberInfo :: (UserId, Permissions, Writetime Permissions, Maybe UserId, Maybe UTCTimeMillis, Maybe UserLegalHoldStatus) -> TeamMemberInfo - mkTeamMemberInfo (uid, perms, permsWT, _, _, _) = - TeamMemberInfo - { Info.userId = uid, - Info.permissions = perms, - Info.permissionsWriteTime = toUTCTimeMillis $ writetimeToUTC permsWT - } - -userTeams :: UserId -> Client [TeamId] -userTeams u = - map runIdentity - <$> retry x1 (query Cql.selectUserTeams (params LocalQuorum (Identity u))) - -usersTeams :: [UserId] -> Client (Map UserId TeamId) -usersTeams uids = do - pairs :: [(UserId, TeamId)] <- - catMaybes - <$> UnliftIO.pooledMapConcurrentlyN 8 (\uid -> (uid,) <$$> oneUserTeam uid) uids - pure $ foldl' (\m (k, v) -> Map.insert k v m) Map.empty pairs - -oneUserTeam :: UserId -> Client (Maybe TeamId) -oneUserTeam u = - fmap runIdentity - <$> retry x1 (query1 Cql.selectOneUserTeam (params LocalQuorum (Identity u))) - -teamCreationTime :: TeamId -> Client (Maybe TeamCreationTime) -teamCreationTime t = - checkCreation . fmap runIdentity - <$> retry x1 (query1 Cql.selectTeamBindingWritetime (params LocalQuorum (Identity t))) - where - checkCreation (Just (Just ts)) = Just $ TeamCreationTime ts - checkCreation _ = Nothing - -getTeamBinding :: TeamId -> Client (Maybe TeamBinding) -getTeamBinding t = - fmap (fromMaybe NonBinding . runIdentity) - <$> retry x1 (query1 Cql.selectTeamBinding (params LocalQuorum (Identity t))) - -getTeamsBindings :: [TeamId] -> Client [TeamBinding] -getTeamsBindings = - fmap catMaybes - . UnliftIO.pooledMapConcurrentlyN 8 getTeamBinding - -deleteTeam :: - ( Member (Input ClientState) r, - Member (Embed IO) r, - Member ConversationStore r - ) => - TeamId -> - Sem r () -deleteTeam tid = do - embedClient (markTeamDeletedAndRemoveTeamMembers tid) - E.deleteTeamConversations tid - embedClient (retry x5 $ write Cql.deleteTeam (params LocalQuorum (Deleted, tid))) - -markTeamDeletedAndRemoveTeamMembers :: TeamId -> Client () -markTeamDeletedAndRemoveTeamMembers tid = do - -- TODO: delete service_whitelist records that mention this team - retry x5 $ write Cql.markTeamDeleted (params LocalQuorum (PendingDelete, tid)) - mems <- teamMembersForPagination tid Nothing (unsafeRange 2000) - removeTeamMembers mems - where - removeTeamMembers :: - Page - ( UserId, - Permissions, - Maybe UserId, - Maybe UTCTimeMillis, - Maybe UserLegalHoldStatus - ) -> - Client () - removeTeamMembers mems = do - mapM_ (removeTeamMember tid . view _1) (result mems) - unless (null $ result mems) $ - removeTeamMembers =<< liftClient (nextPage mems) - -updateTeamStatus :: TeamId -> TeamStatus -> Client () -updateTeamStatus t s = retry x5 $ write Cql.updateTeamStatus (params LocalQuorum (s, t)) - -updateTeam :: TeamId -> TeamUpdateData -> Client () -updateTeam tid u = retry x5 . batch $ do - setType BatchLogged - setConsistency LocalQuorum - for_ (u ^. nameUpdate) $ \n -> - addPrepQuery Cql.updateTeamName (fromRange n, tid) - for_ (u ^. iconUpdate) $ \i -> - addPrepQuery Cql.updateTeamIcon (decodeUtf8 . toByteString' $ i, tid) - for_ (u ^. iconKeyUpdate) $ \k -> - addPrepQuery Cql.updateTeamIconKey (fromRange k, tid) - for_ (u ^. splashScreenUpdate) $ \ss -> - addPrepQuery Cql.updateTeamSplashScreen (decodeUtf8 . toByteString' $ ss, tid) - -- | Construct 'TeamMember' from database tuple. -- If FeatureLegalHoldWhitelistTeamsAndImplicitConsent is enabled set UserLegalHoldDisabled -- if team is whitelisted. @@ -526,6 +154,12 @@ newTeamMember' lh tid (uid, perms, minvu, minvt, fromMaybe defUserLegalHoldStatu mk Nothing Nothing = pure $ mkTeamMember uid perms Nothing lhStatus mk _ _ = throwM $ ErrorCall "TeamMember with incomplete metadata." +isTeamLegalholdWhitelisted :: FeatureDefaults LegalholdConfig -> TeamId -> Client Bool +isTeamLegalholdWhitelisted FeatureLegalHoldDisabledPermanently _ = pure False +isTeamLegalholdWhitelisted FeatureLegalHoldDisabledByDefault _ = pure False +isTeamLegalholdWhitelisted FeatureLegalHoldWhitelistTeamsAndImplicitConsent tid = + isJust <$> (runIdentity <$$> retry x5 (query1 Cql.selectLegalHoldWhitelistedTeam (params LocalQuorum (Identity tid)))) + type RawTeamMember = (UserId, Permissions, Maybe UserId, Maybe UTCTimeMillis, Maybe UserLegalHoldStatus) -- This function has a bit of a difficult type to work with because we don't @@ -548,18 +182,3 @@ teamMembersPageFrom lh tid pagingState (fromRange -> max) = do page <- paginateWithState Cql.selectTeamMembers (paramsPagingState LocalQuorum (Identity tid) max pagingState) members <- mapM (newTeamMember' lh tid) (pwsResults page) pure $ PageWithState members (pwsState page) - -selectSomeTeamMembersPaginated :: - FeatureDefaults LegalholdConfig -> - TeamId -> - [UserId] -> - Maybe PagingState -> - Range 1 HardTruncationLimit Int32 -> - Client (PageWithState TeamMember) -selectSomeTeamMembersPaginated lh tid uids pagingState (fromRange -> max) = do - page <- paginateWithState Cql.selectTeamMembers' (paramsPagingState LocalQuorum (tid, uids) max pagingState) - members <- mapM mkTm (pwsResults page) - pure $ PageWithState members (pwsState page) - where - mkTm (uid, perms, _, minvu, minvt, fromMaybe defUserLegalHoldStatus -> lhStatus) = - newTeamMember' lh tid (uid, perms, minvu, minvt, Just lhStatus) diff --git a/services/galley/src/Galley/Effects.hs b/services/galley/src/Galley/Effects.hs index cd4134e6f5b..4b20074f9a9 100644 --- a/services/galley/src/Galley/Effects.hs +++ b/services/galley/src/Galley/Effects.hs @@ -21,8 +21,8 @@ module Galley.Effects -- * Effects to access the Intra API BrigAPIAccess, - FederatorAccess, - SparAccess, + FederationAPIAccess, + SparAPIAccess, -- * External services ExternalAccess, @@ -65,59 +65,61 @@ import Data.Qualified import Galley.Effects.ClientStore import Galley.Effects.CodeStore import Galley.Effects.CustomBackendStore -import Galley.Effects.FederatorAccess -import Galley.Effects.LegalHoldStore -import Galley.Effects.ProposalStore import Galley.Effects.Queue import Galley.Effects.SearchVisibilityStore -import Galley.Effects.SparAccess import Galley.Effects.TeamFeatureStore import Galley.Effects.TeamMemberStore import Galley.Effects.TeamNotificationStore -import Galley.Effects.TeamStore import Galley.Env import Galley.Options import Galley.Types.Teams -import Imports import Polysemy import Polysemy.Error import Polysemy.Input import Wire.API.Error import Wire.API.Error.Galley +import Wire.API.Federation.Client import Wire.API.Team.Feature import Wire.BackendNotificationQueueAccess import Wire.BrigAPIAccess import Wire.ConversationStore (ConversationStore, MLSCommitLockStore) import Wire.ConversationSubsystem import Wire.ExternalAccess +import Wire.FederationAPIAccess import Wire.FireAndForget import Wire.GundeckAPIAccess import Wire.HashPassword +import Wire.LegalHoldStore +import Wire.LegalHoldStore.Env (LegalHoldEnv) import Wire.ListItems import Wire.NotificationSubsystem +import Wire.ProposalStore import Wire.RateLimit import Wire.Rpc import Wire.Sem.Now import Wire.Sem.Paging.Cassandra import Wire.Sem.Random import Wire.ServiceStore +import Wire.SparAPIAccess import Wire.TeamCollaboratorsStore (TeamCollaboratorsStore) import Wire.TeamCollaboratorsSubsystem (TeamCollaboratorsSubsystem) +import Wire.TeamJournal (TeamJournal) +import Wire.TeamStore import Wire.TeamSubsystem (TeamSubsystem) import Wire.UserGroupStore -- All the possible high-level effects. type GalleyEffects1 = - '[ SparAccess, - TeamCollaboratorsSubsystem, + '[ TeamCollaboratorsSubsystem, ConversationSubsystem, + TeamSubsystem, + SparAPIAccess, NotificationSubsystem, ExternalAccess, BrigAPIAccess, - TeamSubsystem, GundeckAPIAccess, Rpc, - FederatorAccess, + FederationAPIAccess FederatorClient, BackendNotificationQueueAccess, FireAndForget, TeamCollaboratorsStore, @@ -128,12 +130,14 @@ type GalleyEffects1 = HashPassword, Random, CustomBackendStore, - LegalHoldStore, SearchVisibilityStore, + TeamStore, + TeamJournal, + LegalHoldStore, + Input LegalHoldEnv, UserGroupStore, ServiceStore, TeamNotificationStore, - TeamStore, ConversationStore, MLSCommitLockStore, TeamFeatureStore, @@ -141,8 +145,9 @@ type GalleyEffects1 = TeamMemberStore CassandraPaging, ListItems LegacyPaging TeamId, ListItems InternalPaging TeamId, + Input FanoutLimit, Input AllTeamFeatures, - Input (Maybe [TeamId], FeatureDefaults LegalholdConfig), + Input (FeatureDefaults LegalholdConfig), Input (Local ()), Input Opts, Now, diff --git a/services/galley/src/Galley/Effects/FederatorAccess.hs b/services/galley/src/Galley/Effects/FederatorAccess.hs deleted file mode 100644 index 73ab4ca9844..00000000000 --- a/services/galley/src/Galley/Effects/FederatorAccess.hs +++ /dev/null @@ -1,72 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Galley.Effects.FederatorAccess - ( -- * Federator access effect - FederatorAccess (..), - runFederated, - runFederatedEither, - runFederatedConcurrently, - runFederatedConcurrentlyEither, - runFederatedConcurrentlyBucketsEither, - isFederationConfigured, - ) -where - -import Data.Qualified -import Imports -import Polysemy -import Wire.API.Federation.Client -import Wire.API.Federation.Component -import Wire.API.Federation.Error - -data FederatorAccess m a where - RunFederated :: - (KnownComponent c) => - Remote x -> - FederatorClient c a -> - FederatorAccess m a - RunFederatedEither :: - (KnownComponent c) => - Remote x -> - FederatorClient c a -> - FederatorAccess m (Either FederationError a) - RunFederatedConcurrently :: - (KnownComponent c, Foldable f, Functor f) => - f (Remote x) -> - (Remote [x] -> FederatorClient c a) -> - FederatorAccess m [Remote a] - RunFederatedConcurrentlyEither :: - forall (c :: Component) f a m x. - (KnownComponent c, Foldable f, Functor f) => - f (Remote x) -> - (Remote [x] -> FederatorClient c a) -> - FederatorAccess m [Either (Remote [x], FederationError) (Remote a)] - -- | An action similar to 'RunFederatedConcurrentlyEither', but whose input is - -- already in buckets. The buckets are paired with arbitrary data that affect - -- the payload of the request for each remote backend. - RunFederatedConcurrentlyBucketsEither :: - forall (c :: Component) f a m x. - (KnownComponent c, Foldable f) => - f (Remote x) -> - (Remote x -> FederatorClient c a) -> - FederatorAccess m [Either (Remote x, FederationError) (Remote a)] - IsFederationConfigured :: FederatorAccess m Bool - -makeSem ''FederatorAccess diff --git a/services/galley/src/Galley/Env.hs b/services/galley/src/Galley/Env.hs index 4a399af5345..f9b8400d930 100644 --- a/services/galley/src/Galley/Env.hs +++ b/services/galley/src/Galley/Env.hs @@ -26,7 +26,6 @@ import Data.Id import Data.Misc (HttpsUrl) import Data.Range import Data.Time.Clock.DiffTime (millisecondsToDiffTime) -import Galley.Aws qualified as Aws import Galley.Options import Galley.Options qualified as O import Galley.Queue qualified as Q @@ -39,6 +38,7 @@ import System.Logger import Util.Options import Wire.API.MLS.Keys import Wire.API.Team.Member +import Wire.AWS qualified as Aws import Wire.ExternalAccess.External import Wire.NotificationSubsystem.Interpreter import Wire.RateLimit.Interpreter (RateLimitEnv) @@ -46,6 +46,8 @@ import Wire.RateLimit.Interpreter (RateLimitEnv) data DeleteItem = TeamItem TeamId UserId (Maybe ConnId) deriving (Eq, Ord, Show) +type FanoutLimit = Range 1 HardTruncationLimit Int32 + -- | Main application environment. data Env = Env { _reqId :: RequestId, @@ -72,7 +74,7 @@ reqIdMsg :: RequestId -> Msg -> Msg reqIdMsg = ("request" .=) . unRequestId {-# INLINE reqIdMsg #-} -currentFanoutLimit :: Opts -> Range 1 HardTruncationLimit Int32 +currentFanoutLimit :: Opts -> FanoutLimit currentFanoutLimit o = do let optFanoutLimit = fromIntegral . fromRange $ fromMaybe defaultFanoutLimit (o ^. (O.settings . maxFanoutSize)) let maxSize = fromIntegral (o ^. (O.settings . maxTeamSize)) diff --git a/services/galley/src/Galley/External/LegalHoldService.hs b/services/galley/src/Galley/External/LegalHoldService.hs index af7adc0a637..0cb6e483038 100644 --- a/services/galley/src/Galley/External/LegalHoldService.hs +++ b/services/galley/src/Galley/External/LegalHoldService.hs @@ -29,7 +29,6 @@ where import Bilge qualified import Bilge.Response -import Brig.Types.Team.LegalHold import Control.Monad.Catch (MonadThrow (throwM)) import Data.Aeson import Data.ByteString.Char8 qualified as BS8 @@ -39,7 +38,6 @@ import Data.Id import Data.Misc import Data.Qualified (Local, QualifiedWithTag (tUntagged), tUnqualified) import Data.Set qualified as Set -import Galley.Effects.LegalHoldStore as LegalHoldData import Imports import Network.HTTP.Client qualified as Http import Network.HTTP.Types @@ -49,7 +47,9 @@ import System.Logger.Class qualified as Log import Wire.API.Error (ErrorS, throwS) import Wire.API.Error.Galley import Wire.API.Team.LegalHold.External +import Wire.API.Team.LegalHold.Internal import Wire.BrigAPIAccess +import Wire.LegalHoldStore as LegalHoldData ---------------------------------------------------------------------- -- api diff --git a/services/galley/src/Galley/External/LegalHoldService/Internal.hs b/services/galley/src/Galley/External/LegalHoldService/Internal.hs index 77f77b05198..eac3a0d0100 100644 --- a/services/galley/src/Galley/External/LegalHoldService/Internal.hs +++ b/services/galley/src/Galley/External/LegalHoldService/Internal.hs @@ -59,7 +59,7 @@ makeVerifiedRequestWithManager mgr verifyFingerprints fpr (HttpsUrl url) reqBuil . Bilge.secure . prependPath (uriPath url) errHandler e = do - Log.info . Log.msg $ "error making request to legalhold service: " <> show e + Log.info . Log.msg $ "error making request to legalhold service: " <> displayException e throwM (legalHoldServiceUnavailable e) prependPath :: ByteString -> Http.Request -> Http.Request prependPath pth req = req {Http.path = pth Http.path req} diff --git a/services/galley/src/Galley/Intra/Federator.hs b/services/galley/src/Galley/Intra/Federator.hs deleted file mode 100644 index 6c35754d292..00000000000 --- a/services/galley/src/Galley/Intra/Federator.hs +++ /dev/null @@ -1,121 +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 Galley.Intra.Federator (interpretFederatorAccess) where - -import Control.Lens -import Data.Bifunctor -import Data.Qualified -import Galley.Cassandra.Util -import Galley.Effects.FederatorAccess (FederatorAccess (..)) -import Galley.Env -import Galley.Env qualified as E -import Galley.Monad -import Galley.Options -import Imports -import Polysemy -import Polysemy.Input -import Polysemy.TinyLog -import UnliftIO -import Wire.API.Federation.Client -import Wire.API.Federation.Error - -interpretFederatorAccess :: - ( Member (Embed IO) r, - Member (Input Env) r, - Member TinyLog r - ) => - Sem (FederatorAccess ': r) a -> - Sem r a -interpretFederatorAccess = interpret $ \case - RunFederated dom rpc -> do - logEffect "FederatorAccess.RunFederated" - embedApp $ runFederated dom rpc - RunFederatedEither dom rpc -> do - logEffect "FederatorAccess.RunFederatedEither" - embedApp $ runFederatedEither dom rpc - RunFederatedConcurrently rs f -> do - logEffect "FederatorAccess.RunFederatedConcurrently" - embedApp $ runFederatedConcurrently rs f - RunFederatedConcurrentlyEither rs f -> do - logEffect "FederatorAccess.RunFederatedConcurrentlyEither" - embedApp $ runFederatedConcurrentlyEither rs f - RunFederatedConcurrentlyBucketsEither rs f -> do - logEffect "FederatorAccess.RunFederatedConcurrentlyBucketsEither" - embedApp $ runFederatedConcurrentlyBucketsEither rs f - IsFederationConfigured -> do - logEffect "FederatorAccess.IsFederationConfigured" - embedApp $ isJust <$> view E.federator - -runFederatedEither :: - Remote x -> - FederatorClient c a -> - App (Either FederationError a) -runFederatedEither (tDomain -> remoteDomain) rpc = do - ownDomain <- view (options . settings . federationDomain) - mfedEndpoint <- view E.federator - mgr <- view http2Manager - rid <- view reqId - case mfedEndpoint of - Nothing -> pure (Left FederationNotConfigured) - Just fedEndpoint -> do - let ce = - FederatorClientEnv - { ceOriginDomain = ownDomain, - ceTargetDomain = remoteDomain, - ceFederator = fedEndpoint, - ceHttp2Manager = mgr, - ceOriginRequestId = rid - } - liftIO . fmap (first FederationCallFailure) $ runFederatorClient ce rpc - -runFederated :: - Remote x -> - FederatorClient c a -> - App a -runFederated dom rpc = - runFederatedEither dom rpc - >>= either (throwIO . federationErrorToWai) pure - -runFederatedConcurrently :: - ( Foldable f, - Functor f - ) => - f (Remote a) -> - (Remote [a] -> FederatorClient c b) -> - App [Remote b] -runFederatedConcurrently xs rpc = - pooledForConcurrentlyN 8 (bucketRemote xs) $ \r -> - qualifyAs r <$> runFederated r (rpc r) - -runFederatedConcurrentlyEither :: - (Foldable f, Functor f) => - f (Remote a) -> - (Remote [a] -> FederatorClient c b) -> - App [Either (Remote [a], FederationError) (Remote b)] -runFederatedConcurrentlyEither xs rpc = - pooledForConcurrentlyN 8 (bucketRemote xs) $ \r -> - bimap (r,) (qualifyAs r) <$> runFederatedEither r (rpc r) - -runFederatedConcurrentlyBucketsEither :: - (Foldable f) => - f (Remote x) -> - (Remote x -> FederatorClient c b) -> - App [Either (Remote x, FederationError) (Remote b)] -runFederatedConcurrentlyBucketsEither xs rpc = - pooledForConcurrentlyN 8 (toList xs) $ \r -> - bimap (r,) (qualifyAs r) <$> runFederatedEither r (rpc r) diff --git a/services/galley/src/Galley/Intra/Spar.hs b/services/galley/src/Galley/Intra/Spar.hs deleted file mode 100644 index 3fede63dc16..00000000000 --- a/services/galley/src/Galley/Intra/Spar.hs +++ /dev/null @@ -1,48 +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 Galley.Intra.Spar - ( deleteTeam, - lookupScimUserInfo, - ) -where - -import Bilge -import Data.ByteString.Conversion -import Data.Id -import Galley.Intra.Util -import Galley.Monad -import Imports -import Network.HTTP.Types.Method -import Wire.API.User (ScimUserInfo) - --- | Notify Spar that a team is being deleted. -deleteTeam :: TeamId -> App () -deleteTeam tid = do - void . call Spar $ - method DELETE - . paths ["i", "teams", toByteString' tid] - . expect2xx - --- | Get the SCIM user info for a user. -lookupScimUserInfo :: UserId -> App ScimUserInfo -lookupScimUserInfo uid = do - response <- - call Spar $ - method POST - . paths ["i", "scim", "userinfo", toByteString' uid] - responseJsonError response diff --git a/services/galley/src/Galley/Keys.hs b/services/galley/src/Galley/Keys.hs index 5cbfa540b21..e11faa1454a 100644 --- a/services/galley/src/Galley/Keys.hs +++ b/services/galley/src/Galley/Keys.hs @@ -38,6 +38,7 @@ import Data.PEM import Data.Proxy import Data.X509 import Imports +import Network.Wai.Utilities.Exception import Wire.API.MLS.CipherSuite import Wire.API.MLS.Keys @@ -119,7 +120,7 @@ decodeEcdsaKeyPair bytes = do pem <- expectOne "private key" pems let content = pemContent pem -- parse outer pkcs8 container as BER - asn1 <- first displayException (decodeASN1' BER content) + asn1 <- first displayExceptionNoBacktrace (decodeASN1' BER content) (oid, key) <- case asn1 of [ Start Sequence, IntVal _version, @@ -139,7 +140,7 @@ decodeEcdsaKeyPair bytes = do ) $ guard (oid == curveOID @c) -- parse key bytestring as BER again, this should be in the format of rfc5915 - asn1' <- first displayException (decodeASN1' BER key) + asn1' <- first displayExceptionNoBacktrace (decodeASN1' BER key) (privBS, pubBS) <- case asn1' of [ Start Sequence, IntVal _version, @@ -151,10 +152,10 @@ decodeEcdsaKeyPair bytes = do ] -> pure (priv, pub) _ -> Left "invalid ECDSA key format: expected rfc5915 private key format" priv <- - first displayException . eitherCryptoError $ + first displayExceptionNoBacktrace . eitherCryptoError $ ECDSA.decodePrivate curve privBS pub <- - first displayException . eitherCryptoError $ + first displayExceptionNoBacktrace . eitherCryptoError $ ECDSA.decodePublic curve pubBS pure (priv, pub) @@ -165,7 +166,7 @@ decodeEd25519PrivateKey bytes = do pems <- pemParseLBS bytes pem <- expectOne "private key" pems let content = pemContent pem - asn1 <- first displayException (decodeASN1' BER content) + asn1 <- first displayExceptionNoBacktrace (decodeASN1' BER content) (priv, remainder) <- fromASN1 asn1 expectEmpty remainder case priv of diff --git a/services/galley/src/Galley/Run.hs b/services/galley/src/Galley/Run.hs index 9536da440e9..326d0bbfa83 100644 --- a/services/galley/src/Galley/Run.hs +++ b/services/galley/src/Galley/Run.hs @@ -41,7 +41,6 @@ import Galley.API.Internal import Galley.API.Public.Servant import Galley.App import Galley.App qualified as App -import Galley.Aws (awsEnv) import Galley.Cassandra import Galley.Env import Galley.Monad @@ -67,6 +66,7 @@ import Wire.API.Routes.API import Wire.API.Routes.Public.Galley import Wire.API.Routes.Version import Wire.API.Routes.Version.Wai +import Wire.AWS (awsEnv) import Wire.OpenTelemetry (withTracerC) import Wire.PostgresMigrations (runAllMigrations) @@ -102,7 +102,7 @@ mkApp opts = . servantPrometheusMiddleware (Proxy @CombinedAPI) . otelMiddleware . GZip.gunzip - . GZip.gzip GZip.def + . GZip.gzip GZip.defaultGzipSettings . catchErrors logger defaultRequestIdHeaderName Codensity \k -> k () `finally` do diff --git a/services/galley/src/Galley/Schema/Run.hs b/services/galley/src/Galley/Schema/Run.hs index b32b21b98b7..f9b07735f85 100644 --- a/services/galley/src/Galley/Schema/Run.hs +++ b/services/galley/src/Galley/Schema/Run.hs @@ -21,6 +21,7 @@ import Cassandra.MigrateSchema (migrateSchema) import Cassandra.Schema import Control.Exception (finally) import Galley.Schema.V100_OutOfSync qualified as V100_OutOfSync +import Galley.Schema.V101_ConversationLowerGCGracePeriod qualified as V101_ConversationLowerGCGracePeriod import Galley.Schema.V20 qualified as V20 import Galley.Schema.V21 qualified as V21 import Galley.Schema.V22 qualified as V22 @@ -202,7 +203,8 @@ migrations = V97_CellsConversation.migration, V98_ChannelAddPermission.migration, V99_ConversationAddParent.migration, - V100_OutOfSync.migration + V100_OutOfSync.migration, + V101_ConversationLowerGCGracePeriod.migration -- FUTUREWORK: once #1726 has made its way to master/production, -- the 'message' field in connections table can be dropped. -- See also https://github.com/wireapp/wire-server/pull/1747/files diff --git a/services/galley/src/Galley/Schema/V101_ConversationLowerGCGracePeriod.hs b/services/galley/src/Galley/Schema/V101_ConversationLowerGCGracePeriod.hs new file mode 100644 index 00000000000..0ec66d5905e --- /dev/null +++ b/services/galley/src/Galley/Schema/V101_ConversationLowerGCGracePeriod.hs @@ -0,0 +1,48 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . +module Galley.Schema.V101_ConversationLowerGCGracePeriod + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + Migration 101 "set gc_grace_period for conversation related tables to 1 day" $ do + schema' + [r| ALTER TABLE conversation WITH gc_grace_seconds = 86400 |] + + schema' + [r| ALTER TABLE mls_group_member_client WITH gc_grace_seconds = 86400 |] + + schema' + [r| ALTER TABLE subconversation WITH gc_grace_seconds = 86400 |] + + schema' + [r| ALTER TABLE member WITH gc_grace_seconds = 86400 |] + + schema' + [r| ALTER TABLE user WITH gc_grace_seconds = 86400 |] + + schema' + [r| ALTER TABLE member_remote_user WITH gc_grace_seconds = 86400 |] + + schema' + [r| ALTER TABLE team_conv WITH gc_grace_seconds = 86400 |] diff --git a/services/galley/src/Galley/TeamSubsystem.hs b/services/galley/src/Galley/TeamSubsystem.hs deleted file mode 100644 index 35320c83cb4..00000000000 --- a/services/galley/src/Galley/TeamSubsystem.hs +++ /dev/null @@ -1,46 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2025 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Galley.TeamSubsystem where - -import Galley.Effects.TeamStore (TeamStore) -import Galley.Effects.TeamStore qualified as E -import Imports -import Polysemy -import Wire.API.Team.HardTruncationLimit -import Wire.API.Team.Member -import Wire.API.Team.Member.Info (TeamMemberInfoList (TeamMemberInfoList)) -import Wire.TeamSubsystem - --- This interpreter exists so galley code doesn't end up depending on --- GalleyAPIAccess, while it is possible to implement that, it'd add unnecesary --- HTTP calls for tiny things. --- --- When we actually implement TeamSubsystem this can move to wire-subsystem. --- Moving this to wire-subsystem before that would be too much work as the Store --- effects in galley are not as thin as we're doing them in wire-subsystems. --- They also depend on entire galley env. -interpretTeamSubsystem :: (Member TeamStore r) => InterpreterFor TeamSubsystem r -interpretTeamSubsystem = interpret $ \case - InternalGetTeamMember uid tid -> E.getTeamMember tid uid - InternalGetTeamMembers tid maxResults -> - E.getTeamMembersWithLimit tid $ fromMaybe hardTruncationLimitRange maxResults - InternalSelectTeamMemberInfos tid uids -> TeamMemberInfoList <$> E.selectTeamMemberInfos tid uids - InternalGetTeamAdmins tid -> do - admins <- E.getTeamAdmins tid - membs <- E.selectTeamMembers tid admins - pure $ newTeamMemberList membs ListComplete diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index fe8430d0018..313f11f0f55 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -1186,24 +1186,19 @@ postJoinCodeConvOk = do info <- decodeConvCodeEvent <$> postConvCode alice conv let cCode = info.code liftIO $ info.hasPassword @?= False - -- currently ConversationCode is used both as return type for POST ../code and as body for ../join - -- POST /code gives code,key,uri - -- POST /join expects code,key - -- TODO: Should there be two different types? - let payload = cCode {conversationUri = Nothing} -- unnecessary step, cCode can be posted as-is also. - incorrectCode = cCode {conversationCode = Code.Value (unsafeRange (Ascii.encodeBase64Url "incorrect-code"))} + let incorrectCode = cCode {conversationCode = Code.Value (unsafeRange (Ascii.encodeBase64Url "incorrect-code"))} -- with ActivatedAccess, bob can join, but not eve WS.bracketR2 c alice bob $ \(wsA, wsB) -> do -- incorrect code/key does not work postJoinCodeConv bob incorrectCode !!! const 404 === statusCode -- correct code works - postJoinCodeConv bob payload !!! const 200 === statusCode + postJoinCodeConv bob cCode !!! const 200 === statusCode -- non-admin cannot create invite link postConvCode bob conv !!! const 403 === statusCode -- test no-op - postJoinCodeConv bob payload !!! const 204 === statusCode + postJoinCodeConv bob cCode !!! const 204 === statusCode -- eve cannot join - postJoinCodeConv eve payload !!! const 403 === statusCode + postJoinCodeConv eve cCode !!! const 403 === statusCode void . liftIO $ WS.assertMatchN (5 # Second) [wsA, wsB] $ wsAssertMemberJoinWithRole qconv qbob [qbob] roleNameWireMember @@ -1211,13 +1206,13 @@ postJoinCodeConvOk = do Right accessRolesWithGuests <- liftIO $ genAccessRolesV2 [TeamMemberAccessRole, NonTeamMemberAccessRole, GuestAccessRole] [] let nonActivatedAccess = ConversationAccessData (Set.singleton CodeAccess) accessRolesWithGuests putQualifiedAccessUpdate alice qconv nonActivatedAccess !!! const 200 === statusCode - postJoinCodeConv eve payload !!! const 200 === statusCode + postJoinCodeConv eve cCode !!! const 200 === statusCode -- guest cannot create invite link postConvCode eve conv !!! const 403 === statusCode -- after removing CodeAccess, no further people can join let noCodeAccess = ConversationAccessData (Set.singleton InviteAccess) accessRoles putQualifiedAccessUpdate alice qconv noCodeAccess !!! const 200 === statusCode - postJoinCodeConv dave payload !!! const 404 === statusCode + postJoinCodeConv dave cCode !!! const 404 === statusCode -- @END @@ -1515,7 +1510,7 @@ getConvsOk2 = do Just actual -> do assertEqual "name mismatch" expected.metadata.cnvmName actual.cnvMetadata.cnvmName assertEqual "members.self" expected.members.self (Just actual.cnvMembers.cmSelf) - assertEqual "members.others" expected.members.others actual.cnvMembers.cmOthers + assertEqual "members.others" (sort expected.members.others) (sort actual.cnvMembers.cmOthers) getConvsFailMaxSizeV2 :: TestM () getConvsFailMaxSizeV2 = do @@ -2666,8 +2661,7 @@ leaveRemoteConvDenied = do guardComponent Galley mockReply $ LeaveConversationResponse - ( Left RemoveFromConversationErrorRemovalNotAllowed - ) + (Left RemoveFromConversationErrorRemovalNotAllowed) (resp, fedRequests) <- withTempMockFederator' mockResponses $ diff --git a/services/galley/test/integration/API/MLS/Mocks.hs b/services/galley/test/integration/API/MLS/Mocks.hs index dd1ae258895..68f9c7ab0e0 100644 --- a/services/galley/test/integration/API/MLS/Mocks.hs +++ b/services/galley/test/integration/API/MLS/Mocks.hs @@ -49,8 +49,7 @@ receiveCommitMock clients = "get-not-fully-connected-backends" ~> NonConnectedBackends mempty, "get-mls-clients" ~> Set.fromList - ( map (\c -> ClientInfo c.ciClient mempty True) clients - ) + (map (\c -> ClientInfo c.ciClient mempty True) clients) ] receiveCommitMockByDomain :: [ClientIdentity] -> Mock LByteString diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index 8d6a496995f..ef96d88d206 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -1165,8 +1165,7 @@ leaveCurrentConv cid qsub = case qUnqualified qsub of leaveSubConv (ciUser cid) (ciClient cid) (qsub $> cnv) subId !!! const 200 === statusCode ) - ( \rcid -> remoteLeaveCurrentConv rcid (qsub $> cnv) subId - ) + (\rcid -> remoteLeaveCurrentConv rcid (qsub $> cnv) subId) (cidQualifiedUser cid $> cid) State.modify $ \mls -> mls diff --git a/services/galley/test/integration/API/SQS.hs b/services/galley/test/integration/API/SQS.hs index ccf45732c90..9e8f35a6f1a 100644 --- a/services/galley/test/integration/API/SQS.hs +++ b/services/galley/test/integration/API/SQS.hs @@ -28,8 +28,7 @@ import Data.Id import Data.Set qualified as Set import Data.Text (pack) import Data.UUID qualified as UUID -import Galley.Aws qualified as Aws -import Galley.Options (JournalOpts) +import Galley.Options (JournalOpts, endpoint, queueName) import Imports import Network.HTTP.Client import Network.HTTP.Client.OpenSSL @@ -41,6 +40,7 @@ import System.Logger.Class qualified as L import Test.Tasty.HUnit import TestSetup import Util.Test.SQS qualified as SQS +import Wire.AWS qualified as Aws withTeamEventWatcher :: (HasCallStack) => (SQS.SQSWatcher TeamEvent -> TestM ()) -> TestM () withTeamEventWatcher action = do @@ -135,7 +135,7 @@ mkAWSEnv :: JournalOpts -> IO Aws.Env 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 + Aws.mkEnv l mgr (opts ^. endpoint) (opts ^. queueName) 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 9807e6756fe..6c096a04174 100644 --- a/services/galley/test/integration/API/Teams.hs +++ b/services/galley/test/integration/API/Teams.hs @@ -865,8 +865,7 @@ testDeleteBindingTeamSingleMember = do . zUser owner . zConn "conn" . json - ( newTeamMemberDeleteData (Just Util.defPassword) - ) + (newTeamMemberDeleteData (Just Util.defPassword)) ) !!! const 202 === statusCode diff --git a/services/galley/test/integration/API/Teams/LegalHold.hs b/services/galley/test/integration/API/Teams/LegalHold.hs index 72c2ee87686..f24abb8ada7 100644 --- a/services/galley/test/integration/API/Teams/LegalHold.hs +++ b/services/galley/test/integration/API/Teams/LegalHold.hs @@ -26,7 +26,6 @@ import API.Teams.LegalHold.Util import API.Util import Bilge hiding (accept, head, timeout, trace) import Bilge.Assert -import Brig.Types.Test.Arbitrary () import Control.Concurrent.Chan import Control.Lens hiding ((#)) import Data.Id @@ -34,7 +33,6 @@ import Data.LegalHold import Data.PEM import Data.Range import Data.Time.Clock qualified as Time -import Galley.Cassandra.LegalHold import Galley.Env qualified as Galley import Imports import Network.HTTP.Types.Status (status200, status404) @@ -53,6 +51,7 @@ import Wire.API.Team.Member import Wire.API.Team.Permission import Wire.API.Team.Role import Wire.API.User.Client +import Wire.LegalHoldStore.Cassandra tests :: IO TestSetup -> TestTree tests s = testGroup "Legalhold" [testsPublic s, testsInternal s] diff --git a/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs b/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs index 3f73ff3ffc3..94e15b9e542 100644 --- a/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs +++ b/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs @@ -30,7 +30,6 @@ import API.Util import Bilge hiding (accept, head, timeout, trace) import Bilge.Assert import Brig.Types.Intra (UserSet (..)) -import Brig.Types.Test.Arbitrary () import Control.Category ((>>>)) import Control.Concurrent.Chan import Control.Lens @@ -41,7 +40,6 @@ import Data.Map.Strict qualified as Map import Data.PEM import Data.Range import Data.Set qualified as Set -import Galley.Cassandra.LegalHold import Galley.Env qualified as Galley import Imports import Network.HTTP.Types.Status (status200, status404) @@ -62,6 +60,7 @@ import Wire.API.Team.Permission import Wire.API.Team.Role import Wire.API.User.Client import Wire.API.User.Client qualified as Client +import Wire.LegalHoldStore.Cassandra tests :: IO TestSetup -> TestTree tests s = diff --git a/services/galley/test/integration/API/Teams/LegalHold/Util.hs b/services/galley/test/integration/API/Teams/LegalHold/Util.hs index 0949ab9864b..39c18a415b4 100644 --- a/services/galley/test/integration/API/Teams/LegalHold/Util.hs +++ b/services/galley/test/integration/API/Teams/LegalHold/Util.hs @@ -27,7 +27,6 @@ import API.SQS import API.Util import Bilge hiding (accept, head, timeout, trace) import Bilge.Assert -import Brig.Types.Test.Arbitrary () import Control.Concurrent.Async qualified as Async import Control.Concurrent.Chan import Control.Concurrent.Timeout hiding (threadDelay) @@ -516,7 +515,7 @@ assertMatchChan c match = go [] match n refill buf `catchAll` \e -> case asyncExceptionFromException e of - Just x -> error $ show (x :: SomeAsyncException) + Just x -> error $ displayException (x :: SomeAsyncException) Nothing -> go (n : buf) Nothing -> do refill buf @@ -551,8 +550,7 @@ errWith wantStatus wantBody rsp = liftIO $ do assertEqual "" wantStatus (statusCode rsp) assertBool (show $ responseBody rsp) - ( maybe False wantBody (responseJsonMaybe rsp) - ) + (maybe False wantBody (responseJsonMaybe rsp)) ------------------------------------ diff --git a/services/galley/test/integration/Run.hs b/services/galley/test/integration/Run.hs index 54547697b7c..72eb745c1aa 100644 --- a/services/galley/test/integration/Run.hs +++ b/services/galley/test/integration/Run.hs @@ -34,7 +34,6 @@ import Data.Text (pack) import Data.Text.Encoding (encodeUtf8) import Data.Yaml (decodeFileEither) import Federation -import Galley.Aws qualified as Aws import Galley.Options hiding (endpoint) import Galley.Options qualified as O import Imports hiding (local) @@ -54,6 +53,7 @@ import Util.Options import Util.Options.Common import Util.Test import Util.Test.SQS qualified as SQS +import Wire.AWS qualified as Aws newtype ServiceConfigFile = ServiceConfigFile String deriving (Eq, Ord, Typeable) diff --git a/services/galley/test/integration/TestSetup.hs b/services/galley/test/integration/TestSetup.hs index b35dce1fe21..da26f53f0f4 100644 --- a/services/galley/test/integration/TestSetup.hs +++ b/services/galley/test/integration/TestSetup.hs @@ -55,7 +55,6 @@ import Data.ByteString.Conversion import Data.Domain import Data.Proxy import Data.Text qualified as Text -import Galley.Aws qualified as Aws import Galley.Options (Opts) import Imports import Network.HTTP.Client qualified as HTTP @@ -70,6 +69,7 @@ import Wire.API.Federation.API import Wire.API.Federation.Domain import Wire.API.Federation.Version import Wire.API.VersionInfo +import Wire.AWS qualified as Aws type GalleyR = Request -> Request diff --git a/services/gundeck/src/Gundeck/Aws.hs b/services/gundeck/src/Gundeck/Aws.hs index 71014205f40..c31f95d019b 100644 --- a/services/gundeck/src/Gundeck/Aws.hs +++ b/services/gundeck/src/Gundeck/Aws.hs @@ -65,6 +65,7 @@ import Amazonka.SQS.Lens qualified as SQS import Amazonka.SQS.Types import Control.Category ((>>>)) import Control.Error hiding (err, isRight) +import Control.Exception.Lens import Control.Lens hiding ((.=)) import Control.Monad.Catch import Control.Monad.Trans.Resource @@ -208,7 +209,7 @@ mkEnv lgr opts mgr = do getQueueUrl e q = do x <- runResourceT $ - AWS.trying AWS._Error $ + trying AWS._Error $ AWS.send e (SQS.newGetQueueUrl q) either (throwM . GeneralError) @@ -473,14 +474,14 @@ listen throttleMillis callback = do -- Utilities sendCatch :: - (AWSRequest r, Typeable r, Typeable (AWSResponse r)) => + (AWSRequest r) => AWS.Env -> r -> Amazon (Either AWS.Error (AWSResponse r)) -sendCatch env = AWS.trying AWS._Error . AWS.send env +sendCatch env = trying AWS._Error . AWS.send env send :: - (AWSRequest r, Typeable r, Typeable (AWSResponse r)) => + (AWSRequest r) => AWS.Env -> r -> Amazon (AWSResponse r) diff --git a/services/gundeck/src/Gundeck/Notification/Data.hs b/services/gundeck/src/Gundeck/Notification/Data.hs index 06f294e345e..c8d9ba20f7e 100644 --- a/services/gundeck/src/Gundeck/Notification/Data.hs +++ b/services/gundeck/src/Gundeck/Notification/Data.hs @@ -34,7 +34,7 @@ import Data.Id import Data.List.NonEmpty (NonEmpty) import Data.List.NonEmpty qualified as NonEmpty import Data.Range (Range, fromRange) -import Data.Sequence (Seq, ViewL ((:<))) +import Data.Sequence (Seq) import Data.Sequence qualified as Seq import Gundeck.Env import Gundeck.Options (NotificationTTL (..), internalPageSize, maxPayloadLoadSize, settings) @@ -179,18 +179,26 @@ payloadSize (_, mbPayload, _, mbPayloadRefSize, _) = (_, Just size) -> size _ -> 0 +data FetchPayloadsResult = FetchPayloadsResult + { notifications :: Seq QueuedNotification, + remainingBytes :: Int32, + truncatedByPayloadLimit :: Bool + } + -- | Fetches referenced payloads until maxTotalSize payload bytes are fetched from the database. -- At least the first row is fetched regardless of the payload size. -fetchPayloads :: (MonadClient m, MonadUnliftIO m) => Maybe ClientId -> Int32 -> [NotifRow] -> m (Seq QueuedNotification, Int32) -fetchPayloads c left rows = do - let (rows', left') = truncateNotifs [] (0 :: Int) left rows - s <- Seq.fromList . catMaybes <$> pooledMapConcurrentlyN 16 (fetchPayload c) rows' - pure (s, left') +fetchPayloads :: (MonadClient m, MonadUnliftIO m) => Maybe ClientId -> Int32 -> [NotifRow] -> m FetchPayloadsResult +fetchPayloads client remainingBytes rows = do + let isFirstRow = True + let (rows', remainingBytes', truncatedByPayloadLimit) = truncateNotifs [] isFirstRow remainingBytes rows + notifications <- Seq.fromList . catMaybes <$> pooledMapConcurrentlyN 16 (fetchPayload client) rows' + pure $ FetchPayloadsResult notifications remainingBytes' truncatedByPayloadLimit where - truncateNotifs acc _i l [] = (reverse acc, l) - truncateNotifs acc i l (row : rest) - | i > 0 && l <= 0 = (reverse acc, l) - | otherwise = truncateNotifs (row : acc) (i + 1) (l - payloadSize row) rest + truncateNotifs :: [NotifRow] -> Bool -> Int32 -> [NotifRow] -> ([NotifRow], Int32, Bool) + truncateNotifs acc _ remainingBytes' [] = (reverse acc, remainingBytes', False) + truncateNotifs acc isFirstRow remainingBytes' (row : rest) + | not isFirstRow && remainingBytes' <= 0 = (reverse acc, remainingBytes', not (null rest)) + | otherwise = truncateNotifs (row : acc) False (remainingBytes' - payloadSize row) rest -- | Tries to fetch @remaining@ many notifications. -- The returned 'Seq' might contain more notifications than @remaining@, (see @@ -198,16 +206,21 @@ fetchPayloads c left rows = do -- -- The boolean indicates whether more notifications can be fetched. collect :: (MonadReader Env m, MonadClient m, MonadUnliftIO m) => Maybe ClientId -> Seq QueuedNotification -> Bool -> Int -> Int32 -> m (Page NotifRow) -> m (Seq QueuedNotification, Bool) -collect c acc lastPageHasMore remaining remainingBytes getPage - | remaining <= 0 = pure (acc, lastPageHasMore) - | remainingBytes <= 0 = pure (acc, True) - | not lastPageHasMore = pure (acc, False) +collect c acc prevPageHasMore remaining remainingBytes getPage + -- we have fetched at least the requested size: terminating the recursion + | remaining <= 0 = pure (acc, prevPageHasMore) + -- we reached or exceeded the max payload: terminating the recursion + | remainingBytes <= 0 = pure (acc, prevPageHasMore) + -- there is no more data: terminating the recursion + | not prevPageHasMore = pure (acc, False) + -- in any other case we are going to get the next page | otherwise = do page <- getPage let rows = result page - (s, remaingBytes') <- fetchPayloads c remainingBytes rows - let remaining' = remaining - Seq.length s - collect c (acc <> s) (hasMore page) remaining' remaingBytes' (liftClient (nextPage page)) + fetchResult <- fetchPayloads c remainingBytes rows + let remaining' = remaining - Seq.length fetchResult.notifications + more' = hasMore page || fetchResult.truncatedByPayloadLimit + collect c (acc <> fetchResult.notifications) more' remaining' fetchResult.remainingBytes (liftClient (nextPage page)) mkResultPage :: Int -> Bool -> Seq QueuedNotification -> ResultPage mkResultPage size more ns = @@ -224,7 +237,8 @@ fetch u c Nothing (fromIntegral . fromRange -> size) = do -- We always need to look for one more than requested in order to correctly -- report whether there are more results. maxPayloadSize <- fromMaybe (5 * 1024 * 1024) <$> asks (^. options . settings . maxPayloadLoadSize) - (ns, more) <- collect c Seq.empty True (size + 1) maxPayloadSize page1 + let prevPageHasMore = True + (ns, more) <- collect c Seq.empty prevPageHasMore (size + 1) maxPayloadSize page1 -- Drop the extra element at the end if present pure $! mkResultPage size more ns where @@ -236,29 +250,31 @@ fetch u c Nothing (fromIntegral . fromRange -> size) = do \ORDER BY id ASC" fetch u c (Just since) (fromIntegral . fromRange -> size) = do pageSize <- fromMaybe 100 <$> asks (^. options . settings . internalPageSize) + sinceFound <- isJust <$> retry x1 (query1 cqlExists (params LocalQuorum (u, TimeUuid (toUUID since)))) let page1 = retry x1 $ - paginate cqlSince (paramsP LocalQuorum (u, TimeUuid (toUUID since)) pageSize) - -- We fetch 2 more rows than requested. The first is to accommodate the - -- notification corresponding to the `since` argument itself. The second is - -- to get an accurate `hasMore`, just like in the case above. - + paginate cqlAfterSince (paramsP LocalQuorum (u, TimeUuid (toUUID since)) pageSize) maxPayloadSize <- fromMaybe (5 * 1024 * 1024) <$> asks (^. options . settings . maxPayloadLoadSize) - (ns, more) <- collect c Seq.empty True (size + 2) maxPayloadSize page1 - -- Remove notification corresponding to the `since` argument, and record if it is found. - let (ns', sinceFound) = case Seq.viewl ns of - x :< xs | since == x ^. queuedNotificationId -> (xs, True) - _ -> (ns, False) + let prevPageHasMore = True + -- We always need to look for one more than requested in order to correctly + -- report whether there are more results. + (ns, more) <- collect c Seq.empty prevPageHasMore (size + 1) maxPayloadSize page1 pure $! - (mkResultPage size more ns') + (mkResultPage size more ns) { resultGap = not sinceFound } where - cqlSince :: PrepQuery R (UserId, TimeUuid) NotifRow - cqlSince = + cqlExists :: PrepQuery R (UserId, TimeUuid) (Identity TimeUuid) + cqlExists = + "SELECT id \ + \FROM notifications \ + \WHERE user = ? AND id = ?" + + cqlAfterSince :: PrepQuery R (UserId, TimeUuid) NotifRow + cqlAfterSince = "SELECT id, payload, payload_ref, payload_ref_size, clients \ \FROM notifications \ - \WHERE user = ? AND id >= ? \ + \WHERE user = ? AND id > ? \ \ORDER BY id ASC" deleteAll :: (MonadClient m) => UserId -> m () diff --git a/services/gundeck/src/Gundeck/Push/Native.hs b/services/gundeck/src/Gundeck/Push/Native.hs index bc6b414bbb4..a7ac2069ddb 100644 --- a/services/gundeck/src/Gundeck/Push/Native.hs +++ b/services/gundeck/src/Gundeck/Push/Native.hs @@ -310,5 +310,5 @@ logError a m exn = Log.err $ field "user" (toByteString (a ^. addrUser)) ~~ field "arn" (toText (a ^. addrEndpoint)) - ~~ field "error" (show exn) + ~~ field "error" (displayException exn) ~~ msg m diff --git a/services/gundeck/src/Gundeck/Push/Websocket.hs b/services/gundeck/src/Gundeck/Push/Websocket.hs index 43dba86e9ce..562bcb10730 100644 --- a/services/gundeck/src/Gundeck/Push/Websocket.hs +++ b/services/gundeck/src/Gundeck/Push/Websocket.hs @@ -125,7 +125,7 @@ logBadCannons (uri, (err, prcs)) = do ~~ Log.field "created_at" (ms $ createdAt prc) ~~ Log.field "cannon_uri" (show uri) ~~ Log.field "resource_target" (show $ resource prc) - ~~ Log.field "http_exception" (intercalate " | " . lines . show $ err) + ~~ Log.field "http_exception" (intercalate " | " . lines . displayException $ err) ~~ Log.msg (val "WebSocket presence unreachable: ") logPrcsGone :: (Log.MonadLogger m) => Presence -> m () @@ -327,7 +327,7 @@ push notif (toList -> tgts) originUser originConn conns = do <$> runWithDefaultRedis (Presence.listAll (view targetUser <$> tgts)) noPresences exn = do Log.err $ - Log.field "error" (show exn) + Log.field "error" (displayException exn) ~~ Log.msg (val "Failed to get presences.") pure [] filterByClient = map $ \(tgt, ps) -> diff --git a/services/gundeck/src/Gundeck/Redis.hs b/services/gundeck/src/Gundeck/Redis.hs index 5a8ba319caa..17e1f2e3171 100644 --- a/services/gundeck/src/Gundeck/Redis.hs +++ b/services/gundeck/src/Gundeck/Redis.hs @@ -85,8 +85,8 @@ connectRobust l retryStrategy connectLowLevel = do const $ Catch.Handler (\(e :: IOException) -> logEx (Log.err l) e "network error when connecting to Redis" >> pure True) ] . const -- ignore RetryStatus - logEx :: (Show e) => ((Msg -> Msg) -> IO ()) -> e -> ByteString -> IO () - logEx lLevel e description = lLevel $ Log.msg (Log.val description) . Log.field "error" (show e) + logEx :: (Exception e) => ((Msg -> Msg) -> IO ()) -> e -> ByteString -> IO () + logEx lLevel e description = lLevel $ Log.msg (Log.val description) . Log.field "error" (displayException e) -- | Run a 'Redis' action through a 'RobustConnection'. -- @@ -107,7 +107,7 @@ runRobust mvar action = retry $ do . const -- ignore RetryStatus logAndHandle (Handler handler) _ = Handler $ \e -> do - LogClass.err $ Log.msg (Log.val "Redis connection failed") . Log.field "error" (show e) + LogClass.err $ Log.msg (Log.val "Redis connection failed") . Log.field "error" (displayException e) handler e data PingException = PingException Reply deriving (Show) diff --git a/services/gundeck/src/Gundeck/Run.hs b/services/gundeck/src/Gundeck/Run.hs index f3f1ed140db..89e4c9f8ef2 100644 --- a/services/gundeck/src/Gundeck/Run.hs +++ b/services/gundeck/src/Gundeck/Run.hs @@ -156,7 +156,7 @@ run opts = withTracer \tracer -> do . requestIdMiddleware (env ^. applog) defaultRequestIdHeaderName . Metrics.servantPrometheusMiddleware (Proxy @(GundeckAPI :<|> InternalAPI)) . GZip.gunzip - . GZip.gzip GZip.def + . GZip.gzip GZip.defaultGzipSettings . catchErrors (env ^. applog) defaultRequestIdHeaderName mkApp :: Env -> Wai.Application diff --git a/services/gundeck/src/Gundeck/ThreadBudget/Internal.hs b/services/gundeck/src/Gundeck/ThreadBudget/Internal.hs index 89595b9d51b..44d7953c93d 100644 --- a/services/gundeck/src/Gundeck/ThreadBudget/Internal.hs +++ b/services/gundeck/src/Gundeck/ThreadBudget/Internal.hs @@ -308,5 +308,5 @@ safeForever :: safeForever action = forever $ action `catchAny` \exc -> do - LC.err $ "error" LC..= show exc LC.~~ LC.msg (LC.val "watchThreadBudgetState: crashed; retrying") + LC.err $ "error" LC..= displayException exc LC.~~ LC.msg (LC.val "watchThreadBudgetState: crashed; retrying") threadDelay 60000000 -- pause to keep worst-case noise in logs manageable diff --git a/services/gundeck/test/unit/MockGundeck.hs b/services/gundeck/test/unit/MockGundeck.hs index aaef7736187..cb7b4f5fa88 100644 --- a/services/gundeck/test/unit/MockGundeck.hs +++ b/services/gundeck/test/unit/MockGundeck.hs @@ -606,8 +606,8 @@ mockBulkPush notifs = do let delivered :: [(Notification, [Presence])] delivered = [ (nid, prcs) - | (nid, filter (`elem` deliveredprcs) -> prcs) <- notifs, - not $ null prcs -- (sic!) (this is what gundeck currently does) + | (nid, filter (`elem` deliveredprcs) -> prcs) <- notifs, + not $ null prcs -- (sic!) (this is what gundeck currently does) ] deliveredprcs :: [Presence] deliveredprcs = filter isreachable . mconcat . fmap fakePresences $ allRecipients env diff --git a/services/nginz/integration-test/conf/nginz/nginx.conf b/services/nginz/integration-test/conf/nginz/nginx.conf index efecea77fb2..4b9356f07c5 100644 --- a/services/nginz/integration-test/conf/nginz/nginx.conf +++ b/services/nginz/integration-test/conf/nginz/nginx.conf @@ -218,7 +218,7 @@ http { proxy_pass http://brig; } - location /register { + location ~* ^(/v[0-9]+)?/register { include common_response_no_zauth.conf; proxy_pass http://brig; } diff --git a/services/proxy/src/Proxy/API/Public.hs b/services/proxy/src/Proxy/API/Public.hs index c374a47f77e..5b7e01a4d77 100644 --- a/services/proxy/src/Proxy/API/Public.hs +++ b/services/proxy/src/Proxy/API/Public.hs @@ -134,7 +134,7 @@ proxy qparam keyname reroute path phost rq k = do onUpstreamError :: (Proxy () -> IO a) -> SomeException -> p -> (Response -> IO b) -> IO b onUpstreamError runInIO x _ next = do - void . runInIO $ Logger.warn (msg (val "gateway error") ~~ field "error" (show x)) + void . runInIO $ Logger.warn (msg (val "gateway error") ~~ field "error" (displayException x)) next (errorRs error502) waiProxyResponse :: Env -> Request -> ProxyDest -> WaiProxyResponse diff --git a/services/spar/spar.cabal b/services/spar/spar.cabal index 4117e731c14..6d26c0d0f07 100644 --- a/services/spar/spar.cabal +++ b/services/spar/spar.cabal @@ -13,6 +13,14 @@ license: AGPL-3 license-file: LICENSE build-type: Simple +-- This flags removes build-tool-depends when compiling things in the dev +-- environment. +-- https://github.com/NixOS/nixpkgs/issues/130556#issuecomment-2762237786 +flag nix-dev-env + description: In a Nix dev environment. + default: False + manual: True + library -- cabal-fmt: expand src exposed-modules: @@ -43,6 +51,7 @@ library Spar.Schema.V19 Spar.Schema.V2 Spar.Schema.V20 + Spar.Schema.V21 Spar.Schema.V3 Spar.Schema.V4 Spar.Schema.V5 @@ -339,7 +348,9 @@ executable spar-integration -with-rtsopts=-N -Wredundant-constraints -Wunused-packages -Wno-x-partial - build-tool-depends: hspec-discover:hspec-discover + if !flag(nix-dev-env) + build-tool-depends: hspec-discover:hspec-discover + build-depends: aeson , aeson-qq @@ -611,7 +622,9 @@ test-suite spec -with-rtsopts=-N -Wredundant-constraints -Wunused-packages -Wno-x-partial - build-tool-depends: hspec-discover:hspec-discover + if !flag(nix-dev-env) + build-tool-depends: hspec-discover:hspec-discover + build-depends: aeson , aeson-qq diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index 6224146fb27..f5f9de0d1e9 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -55,6 +55,7 @@ import Data.Domain import Data.HavePendingInvitations import Data.Id import Data.List.NonEmpty (NonEmpty) +import qualified Data.Map as Map import Data.Proxy import Data.Range import Data.Text.Encoding.Error @@ -109,6 +110,7 @@ import System.Logger (Msg) import qualified URI.ByteString as URI import Wire.API.Routes.Internal.Spar import Wire.API.Routes.Named +import Wire.API.Routes.Public (ZHostValue) import Wire.API.Routes.Public.Spar import Wire.API.Team.Member (HiddenPerm (CreateUpdateDeleteIdp, ReadIdp)) import Wire.API.User @@ -174,7 +176,7 @@ api :: ServerT SparAPI (Sem r) api opts = apiSSO opts - :<|> apiIDP + :<|> apiIDP opts :<|> apiScim :<|> apiINTERNAL @@ -219,14 +221,15 @@ apiIDP :: Member SAMLUserStore r, Member (Error SparError) r ) => + Opts -> ServerT APIIDP (Sem r) -apiIDP = +apiIDP opts = Named @"idp-get" idpGet -- get, json, captures idp id :<|> Named @"idp-get-raw" idpGetRaw -- get, raw xml, capture idp id :<|> Named @"idp-get-all" idpGetAll -- get, json - :<|> Named @"idp-create@v7" idpCreateV7 - :<|> Named @"idp-create" idpCreate -- post, created - :<|> Named @"idp-update" idpUpdate -- put, okay + :<|> Named @"idp-create@v7" (idpCreateV7 opts.saml) + :<|> Named @"idp-create" (idpCreate opts.saml) -- post, created + :<|> Named @"idp-update" (idpUpdate opts.saml) -- put, okay :<|> Named @"idp-delete" idpDelete -- delete, no content apiINTERNAL :: @@ -594,7 +597,7 @@ idpDelete mbzusr idpid (fromMaybe False -> purge) = withDebugLog "idpDelete" (co -- to be deleted in its old issuers list, but it's tricky to avoid race conditions, and -- there is little to be gained here: we only use old issuers to find users that have not -- been migrated yet, and if an old user points to a deleted idp, it just means that we - -- won't find any users to migrate. still, doesn't hurt mucht to look either. so we + -- won't find any users to migrate. still, doesn't hurt much to look either. so we -- leave old issuers dangling for now. updateReplacingIdP :: IdP -> Sem r () @@ -631,22 +634,46 @@ idpCreate :: Member IdPRawMetadataStore r, Member (Error SparError) r ) => + SAML.Config -> TeamId -> + Maybe ZHostValue -> IdPMetadataInfo -> Maybe SAML.IdPId -> Maybe WireIdPAPIVersion -> Maybe (Range 1 32 Text) -> Sem r IdP -idpCreate tid (IdPMetadataValue rawIdpMetadata idpmeta) mReplaces (fromMaybe defWireIdPAPIVersion -> apiversion) mHandle = withDebugLog "idpCreateXML" (Just . show . (^. SAML.idpId)) $ do +idpCreate samlConfig tid uncheckedMbHost (IdPMetadataValue rawIdpMetadata idpmeta) mReplaces (fromMaybe defWireIdPAPIVersion -> apiversion) mHandle = withDebugLog "idpCreateXML" (Just . show . (^. SAML.idpId)) $ do + let mbHost = filterMultiIngressZHost (samlConfig._cfgDomainConfigs) uncheckedMbHost GalleyAccess.assertSSOEnabled tid + guardMultiIngressDuplicateDomain tid mbHost idp <- maybe (IdPConfigStore.newHandle tid) (pure . IdPHandle . fromRange) mHandle - >>= validateNewIdP apiversion idpmeta tid mReplaces + >>= validateNewIdP apiversion idpmeta tid mReplaces mbHost IdPRawMetadataStore.store (idp ^. SAML.idpId) rawIdpMetadata IdPConfigStore.insertConfig idp forM_ mReplaces $ \replaces -> IdPConfigStore.setReplacedBy (Replaced replaces) (Replacing (idp ^. SAML.idpId)) pure idp + where + -- Ensure that the domain is not in use by an existing IDP + guardMultiIngressDuplicateDomain :: + ( Member (Error SparError) r, + Member IdPConfigStore r + ) => + TeamId -> + Maybe ZHostValue -> + Sem r () + guardMultiIngressDuplicateDomain _teamId Nothing = pure () + guardMultiIngressDuplicateDomain teamId (Just zHost) = do + idps <- IdPConfigStore.getConfigsByTeam teamId + let domains = idps ^.. traverse . SAML.idpExtraInfo . domain . _Just + when (zHost `elem` domains) $ + throwSparSem SparIdPDomainInUse + +-- | Only return a ZHost when multi-ingress is configured and the host value is a configured domain +filterMultiIngressZHost :: Either SAML.MultiIngressDomainConfig (Map Domain SAML.MultiIngressDomainConfig) -> Maybe ZHostValue -> Maybe ZHostValue +filterMultiIngressZHost (Right domainMap) (Just zHost) | (Domain zHost) `Map.member` domainMap = Just zHost +filterMultiIngressZHost _ _ = Nothing idpCreateV7 :: ( Member Random r, @@ -658,15 +685,16 @@ idpCreateV7 :: Member IdPRawMetadataStore r, Member (Error SparError) r ) => + SAML.Config -> TeamId -> IdPMetadataInfo -> Maybe SAML.IdPId -> Maybe WireIdPAPIVersion -> Maybe (Range 1 32 Text) -> Sem r IdP -idpCreateV7 tid idpmeta mReplaces mApiversion mHandle = do +idpCreateV7 samlConfig tid idpmeta mReplaces mApiversion mHandle = do assertNoScimOrNoIdP - idpCreate tid idpmeta mReplaces mApiversion mHandle + idpCreate samlConfig tid Nothing idpmeta mReplaces mApiversion mHandle where -- In teams with a scim access token, only one IdP is allowed. The reason is that scim user -- data contains no information about the idp issuer, only the user name, so no valid saml @@ -716,9 +744,10 @@ validateNewIdP :: SAML.IdPMetadata -> TeamId -> Maybe SAML.IdPId -> + Maybe ZHostValue -> IdPHandle -> m IdP -validateNewIdP apiversion _idpMetadata teamId mReplaces idHandle = withDebugLog "validateNewIdP" (Just . show . (^. SAML.idpId)) $ do +validateNewIdP apiversion _idpMetadata teamId mReplaces idpDomain idHandle = withDebugLog "validateNewIdP" (Just . show . (^. SAML.idpId)) $ do _idpId <- SAML.IdPId <$> Random.uuid oldIssuersList :: [SAML.Issuer] <- case mReplaces of Nothing -> pure [] @@ -726,7 +755,7 @@ validateNewIdP apiversion _idpMetadata teamId mReplaces idHandle = withDebugLog idp <- IdPConfigStore.getConfig replaces pure $ (idp ^. SAML.idpMetadata . SAML.edIssuer) : (idp ^. SAML.idpExtraInfo . oldIssuers) let requri = _idpMetadata ^. SAML.edRequestURI - _idpExtraInfo = WireIdP teamId (Just apiversion) oldIssuersList Nothing idHandle + _idpExtraInfo = WireIdP teamId (Just apiversion) oldIssuersList Nothing idHandle idpDomain enforceHttps requri mbIdp <- case apiversion of WireIdPAPIV1 -> IdPConfigStore.getIdPByIssuerV1Maybe (_idpMetadata ^. SAML.edIssuer) @@ -758,12 +787,16 @@ idpUpdate :: Member IdPRawMetadataStore r, Member (Error SparError) r ) => + SAML.Config -> Maybe UserId -> + Maybe ZHostValue -> IdPMetadataInfo -> SAML.IdPId -> Maybe (Range 1 32 Text) -> Sem r IdP -idpUpdate zusr (IdPMetadataValue raw xml) = idpUpdateXML zusr raw xml +idpUpdate samlConfig zusr uncheckedMbHost (IdPMetadataValue raw xml) = + let mbHost = filterMultiIngressZHost (samlConfig._cfgDomainConfigs) uncheckedMbHost + in idpUpdateXML zusr mbHost raw xml idpUpdateXML :: ( Member Random r, @@ -775,29 +808,51 @@ idpUpdateXML :: Member (Error SparError) r ) => Maybe UserId -> + Maybe ZHostValue -> Text -> SAML.IdPMetadata -> SAML.IdPId -> Maybe (Range 1 32 Text) -> Sem r IdP -idpUpdateXML zusr raw idpmeta idpid mHandle = withDebugLog "idpUpdateXML" (Just . show . (^. SAML.idpId)) $ do +idpUpdateXML zusr mDomain raw idpmeta idpid mHandle = withDebugLog "idpUpdateXML" (Just . show . (^. SAML.idpId)) $ do (teamid, idp) <- validateIdPUpdate zusr idpmeta idpid GalleyAccess.assertSSOEnabled teamid + guardMultiIngressDuplicateDomain teamid mDomain idpid IdPRawMetadataStore.store (idp ^. SAML.idpId) raw let idp' :: IdP = case mHandle of Just idpHandle -> idp & (SAML.idpExtraInfo . handle) .~ IdPHandle (fromRange idpHandle) Nothing -> idp + idp'' :: IdP = idp' & (SAML.idpExtraInfo . domain) .~ mDomain -- (if raw metadata is stored and then spar goes out, raw metadata won't match the -- structured idp config. since this will lead to a 5xx response, the client is expected to -- try again, which would clean up cassandra state.) - IdPConfigStore.insertConfig idp' + IdPConfigStore.insertConfig idp'' -- if the IdP issuer is updated, the old issuer must be removed explicitly. -- if this step is ommitted (due to a crash) resending the update request should fix the inconsistent state. - let mbteamid = case fromMaybe defWireIdPAPIVersion $ idp' ^. SAML.idpExtraInfo . apiVersion of + let mbteamid = case fromMaybe defWireIdPAPIVersion $ idp'' ^. SAML.idpExtraInfo . apiVersion of WireIdPAPIV1 -> Nothing WireIdPAPIV2 -> Just teamid - forM_ (idp' ^. SAML.idpExtraInfo . oldIssuers) (flip IdPConfigStore.deleteIssuer mbteamid) - pure idp' + forM_ (idp'' ^. SAML.idpExtraInfo . oldIssuers) (flip IdPConfigStore.deleteIssuer mbteamid) + pure idp'' + where + -- Ensure that the domain is not in use by an existing IDP + guardMultiIngressDuplicateDomain :: + ( Member (Error SparError) r, + Member IdPConfigStore r + ) => + TeamId -> + Maybe ZHostValue -> + SAML.IdPId -> + Sem r () + guardMultiIngressDuplicateDomain _teamId Nothing _ = pure () + guardMultiIngressDuplicateDomain teamId (Just zHost) idpId = do + idps <- IdPConfigStore.getConfigsByTeam teamId + let otherIdpsOnSameDomain = + any + (\idp -> (idp ^. SAML.idpExtraInfo . domain) == (Just zHost) && (idp ^. SAML.idpId) /= idpId) + idps + when otherIdpsOnSameDomain $ + throwSparSem SparIdPDomainInUse -- | Check that: idp id is valid; calling user is admin in that idp's home team; team id in -- new metainfo doesn't change; new issuer (if changed) is not in use anywhere else (except as @@ -834,7 +889,7 @@ validateIdPUpdate zusr _idpMetadata _idpId = withDebugLog "validateIdPUpdate" (J ( case fromMaybe defWireIdPAPIVersion $ previousIdP ^. SAML.idpExtraInfo . apiVersion of WireIdPAPIV1 -> IdPConfigStore.getIdPByIssuerV1Maybe newIssuer WireIdPAPIV2 -> IdPConfigStore.getIdPByIssuerV2Maybe newIssuer teamId - ) + ) <&> ( \case Just idpFound -> idpFound ^. SAML.idpId /= _idpId Nothing -> False diff --git a/services/spar/src/Spar/Error.hs b/services/spar/src/Spar/Error.hs index 36e0d823c18..48ff8866dcc 100644 --- a/services/spar/src/Spar/Error.hs +++ b/services/spar/src/Spar/Error.hs @@ -48,9 +48,6 @@ import qualified Bilge import Control.Monad.Except import Data.Aeson import qualified Data.ByteString.Lazy as LBS -import Data.Id -import qualified Data.Text as Text -import qualified Data.Text.Encoding as Text import qualified Data.Text.Lazy as LText import qualified Data.Text.Lazy.Encoding as LText import Data.Typeable (typeRep) @@ -119,6 +116,7 @@ data SparCustomError | SparSomeHttpError HttpError | -- | All errors returned from SCIM handlers are wrapped into 'SparScimError' SparScimError Scim.ScimError + | SparIdPDomainInUse deriving (Eq, Show) data SparProvisioningMoreThanOneIdP @@ -231,6 +229,7 @@ renderSparError (SAML.CustomError (IdpDbError AttemptToGetV2IssuerViaV1API)) = S renderSparError (SAML.CustomError (IdpDbError IdpNonUnique)) = StdError $ Wai.mkError status409 "idp-non-unique" "We have found multiple IdPs with the same issuer. Please contact customer support." renderSparError (SAML.CustomError (IdpDbError IdpWrongTeam)) = StdError $ Wai.mkError status409 "idp-wrong-team" "The IdP is not part of this team." renderSparError (SAML.CustomError (IdpDbError IdpNotFound)) = renderSparError (SAML.CustomError (SparIdPNotFound "")) +renderSparError (SAML.CustomError SparIdPDomainInUse) = StdError $ Wai.mkError status409 "idp-duplicate-domain-for-team" "This team already has an IdP configured for this domain." -- Errors related to provisioning renderSparError (SAML.CustomError (SparProvisioningMoreThanOneIdP msg)) = StdError $ Wai.mkError status400 "more-than-one-idp" do @@ -289,18 +288,4 @@ parseResponse serviceName resp = do mapScimSubsystemErrors :: (Member (Error SparError) r) => InterpreterFor (Error ScimSubsystemError) r mapScimSubsystemErrors = - Polysemy.Error.mapError $ - SAML.CustomError . SparScimError . \case - ScimSubsystemError err -> - err - ScimSubsystemInvalidGroupMemberId badId -> - Scim.badRequest Scim.InvalidValue (Just $ "Invalid group member ID: " <> badId) - ScimSubsystemGroupMembersNotFound badIds -> - Scim.badRequest Scim.InvalidValue (Just $ "These users are not in your team or not \"managed_by\" = \"scim\": " <> renderIds badIds) - ScimSubsystemInternal waiErr -> - Scim.serverError (Text.decodeUtf8 . LBS.toStrict $ encode waiErr) - ScimSubsystemInternalError _ -> - Scim.serverError "unexpected error" - where - renderIds :: [UserId] -> Text - renderIds = Text.intercalate ", " . fmap idToText + Polysemy.Error.mapError (SAML.CustomError . SparScimError . scimSubsystemErrorToScimError) diff --git a/services/spar/src/Spar/Intra/BrigApp.hs b/services/spar/src/Spar/Intra/BrigApp.hs index b14a3a60b26..08bc096ee87 100644 --- a/services/spar/src/Spar/Intra/BrigApp.hs +++ b/services/spar/src/Spar/Intra/BrigApp.hs @@ -23,7 +23,8 @@ module Spar.Intra.BrigApp ( veidToUserSSOId, urefToExternalId, - veidFromBrigUser, + oldVeidFromBrigUser, + newVeidFromBrigUser, veidFromUserSSOId, mkUserName, HavePendingInvitations (..), @@ -42,6 +43,7 @@ import Brig.Types.Intra import Control.Lens import Control.Monad.Except import Data.ByteString.Conversion +import Data.CaseInsensitive (original) import qualified Data.CaseInsensitive as CI import Data.Handle (Handle, parseHandle) import Data.HavePendingInvitations @@ -88,26 +90,39 @@ veidFromUserSSOId ssoId mEmail = case ssoId of -- If veid can be parsed as an email, we end up in the case above with email delivered separately. throwError "internal error: externalId is not an email and there is no SAML issuer" --- | If the brig user has a 'UserSSOId', transform that into a 'ValidScimId' (this is a --- total function as long as brig obeys the api). Otherwise, if the user has an email, we can --- construct a return value from that (and an optional saml issuer). +-- | Turns ssoid and email* fields back into a `ValidScimId`. +oldVeidFromBrigUser :: User -> Maybe ValidScimId +oldVeidFromBrigUser usr = + let mbEmail = userEmail usr <|> userEmailUnvalidated usr + in fromRight (error "impossible") $ (`veidFromUserSSOId` mbEmail) `mapM` userSSOId usr + +-- | Compute ValidScimId from updates. Take both the old user (just +-- like `oldVeidFromBrigUser`) and updated idp issuer and unvalidated +-- email into consideration. -- --- Note: the saml issuer is only needed in the case where a user has been invited via team --- settings and is now onboarded to saml/scim. If this case can safely be ruled out, it's ok --- to just set it to 'Nothing'. +-- If updated values are `Nothing`, the corresponding data from brig +-- user will be ignored (this is how you delete an idp association). -- --- `userSSOId usr` can be empty if the user has no SAML credentials and is brought under scim --- management for the first time. In that case, the externalId is taken to --- be the email address. -veidFromBrigUser :: (MonadError String m) => User -> Maybe SAML.Issuer -> Maybe EmailAddress -> m ValidScimId -veidFromBrigUser usr mIssuer mUnvalidatedEmail = case (userSSOId usr, userEmail usr, mIssuer) of - (Just ssoid, mValidatedEmail, _) -> do - -- `mEmail` is in synch with SCIM user schema. - let mEmail = mUnvalidatedEmail <|> mValidatedEmail - veidFromUserSSOId ssoid mEmail +-- `userSSOId usr` can be empty if the user has no SAML credentials +-- and is brought under scim management for the first time. In that +-- case, the externalId is taken to be the email address. +newVeidFromBrigUser :: (MonadError String m) => User -> Maybe SAML.Issuer -> m ValidScimId +newVeidFromBrigUser usr mIssuer = case (userSSOId usr, userEmail usr <|> userEmailUnvalidated usr, mIssuer) of + (Just ssoid, mbEmail, _) -> do + -- this makes sure email encoded in ssoid is in synch with SCIM user. + veidFromUserSSOId (updateSsoid ssoid) mbEmail (Nothing, Just email, Just issuer) -> pure $ ValidScimId (fromEmail email) (These email (SAML.UserRef issuer (fromRight' $ emailToSAMLNameID email))) (Nothing, Just email, Nothing) -> pure $ ValidScimId (fromEmail email) (This email) (Nothing, Nothing, _) -> throwError "user has neither ssoIdentity nor userEmail" + where + updateSsoid :: UserSSOId -> UserSSOId + updateSsoid ssoid = case (ssoid, mIssuer) of + (UserSSOId uref, Nothing) -> UserScimExternalId (uref ^. SAML.uidSubject . to SAML.nameIDToST . to original) + (dontchange@(UserScimExternalId _), Nothing) -> dontchange + (UserSSOId uref, Just issuer) -> UserSSOId (uref & SAML.uidTenant .~ issuer) + (UserScimExternalId eid, Just issuer) -> + let nameId :: SAML.NameID = SAML.emailNameID eid & fromRight (SAML.unspecifiedNameID eid) + in UserSSOId (SAML.UserRef issuer nameId) -- | Take a maybe text, construct a 'Name' from what we have in a scim user. If the text -- isn't present, use an email address or a saml subject (usually also an email address). If diff --git a/services/spar/src/Spar/Schema/Run.hs b/services/spar/src/Spar/Schema/Run.hs index 46bf1e5bd5f..2ddb825c3ce 100644 --- a/services/spar/src/Spar/Schema/Run.hs +++ b/services/spar/src/Spar/Schema/Run.hs @@ -35,6 +35,7 @@ import qualified Spar.Schema.V18 as V18 import qualified Spar.Schema.V19 as V19 import qualified Spar.Schema.V2 as V2 import qualified Spar.Schema.V20 as V20 +import qualified Spar.Schema.V21 as V21 import qualified Spar.Schema.V3 as V3 import qualified Spar.Schema.V4 as V4 import qualified Spar.Schema.V5 as V5 @@ -82,7 +83,8 @@ migrations = V17.migration, V18.migration, V19.migration, - V20.migration + V20.migration, + V21.migration -- TODO: Add a migration that removes unused fields -- (we don't want to risk running a migration which would -- effectively break the currently deployed spar service) diff --git a/services/galley/src/Galley/Effects/SparAccess.hs b/services/spar/src/Spar/Schema/V21.hs similarity index 67% rename from services/galley/src/Galley/Effects/SparAccess.hs rename to services/spar/src/Spar/Schema/V21.hs index f84e3ac87ec..aaff2080483 100644 --- a/services/galley/src/Galley/Effects/SparAccess.hs +++ b/services/spar/src/Spar/Schema/V21.hs @@ -1,8 +1,6 @@ -{-# LANGUAGE TemplateHaskell #-} - -- This file is part of the Wire Server implementation. -- --- Copyright (C) 2022 Wire Swiss GmbH +-- Copyright (C) 2025 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 @@ -17,14 +15,18 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Galley.Effects.SparAccess where - -import Data.Id -import Polysemy -import Wire.API.User (ScimUserInfo) +module Spar.Schema.V21 + ( migration, + ) +where -data SparAccess m a where - DeleteTeam :: TeamId -> SparAccess m () - LookupScimUserInfo :: UserId -> SparAccess m ScimUserInfo +import Cassandra.Schema +import Imports +import Text.RawString.QQ -makeSem ''SparAccess +migration :: Migration +migration = Migration 21 "Add domain column to idp table" $ do + schema' + [r| + ALTER TABLE idp ADD (domain text); + |] diff --git a/services/spar/src/Spar/Scim.hs b/services/spar/src/Spar/Scim.hs index 18060c4d64b..e4d020a85f7 100644 --- a/services/spar/src/Spar/Scim.hs +++ b/services/spar/src/Spar/Scim.hs @@ -67,6 +67,7 @@ import qualified Data.Text as T import qualified Data.Text.Encoding as T import Data.Text.Encoding.Error import Imports +import Network.Wai.Utilities.Exception import Polysemy import Polysemy.Error (Error, fromExceptionSem, runError, throw, try) import Polysemy.Input (Input) @@ -160,7 +161,7 @@ apiScim = -- We caught an exception that's not a Spar exception at all. It is wrapped into -- Scim.serverError. throw . SAML.CustomError . SparScimError $ - Scim.serverError (T.pack (displayException someException)) + Scim.serverError (T.pack (displayExceptionNoBacktrace someException)) Right (Left err@(SAML.CustomError (SparScimError _))) -> -- We caught a 'SparScimError' exception. It is left as-is. throw err diff --git a/services/spar/src/Spar/Scim/Auth.hs b/services/spar/src/Spar/Scim/Auth.hs index 09478cfa516..1f7b824ce10 100644 --- a/services/spar/src/Spar/Scim/Auth.hs +++ b/services/spar/src/Spar/Scim/Auth.hs @@ -181,8 +181,8 @@ createScimToken :: Sem r CreateScimTokenResponse createScimToken zusr Api.CreateScimToken {..} = do teamid <- guardScimTokenCreation zusr password verificationCode - mIdPId <- maybe (pure Nothing) (\idpid -> IdPConfigStore.getConfig idpid $> Just idpid) idp - createScimTokenUnchecked teamid name description mIdPId + let guardIdPExists = mapM_ IdPConfigStore.getConfig in guardIdPExists idp + createScimTokenUnchecked teamid name description idp guardScimTokenCreation :: forall r. diff --git a/services/spar/src/Spar/Scim/Group.hs b/services/spar/src/Spar/Scim/Group.hs index caa22bd5032..3ace67bcf7a 100644 --- a/services/spar/src/Spar/Scim/Group.hs +++ b/services/spar/src/Spar/Scim/Group.hs @@ -39,8 +39,11 @@ instance (AuthDB SparTag (Sem r), Member ScimSubsystem r) => SCG.GroupDB SparTag getGroups :: AuthInfo SparTag -> Maybe Filter -> + Maybe Int -> + Maybe Int -> ScimHandler (Sem r) (ListResponse (SCG.StoredGroup SparTag)) - getGroups ((.stiTeam) -> tid) mbFilter = lift $ scimGetUserGroups tid mbFilter + getGroups ((.stiTeam) -> tid) mbFilter mbStartIndex mbCount = + lift $ scimGetUserGroups tid mbFilter (fromIntegral <$> mbStartIndex) (fromIntegral <$> mbCount) -- \| Get a single group by ID. -- diff --git a/services/spar/src/Spar/Scim/Types.hs b/services/spar/src/Spar/Scim/Types.hs index 5877b7884a2..b2b6b360af7 100644 --- a/services/spar/src/Spar/Scim/Types.hs +++ b/services/spar/src/Spar/Scim/Types.hs @@ -30,9 +30,9 @@ -- * Request and response types for SCIM-related endpoints. module Spar.Scim.Types where -import Brig.Types.Test.Arbitrary (Arbitrary (..)) import Control.Lens (view) import Imports +import Test.QuickCheck (Arbitrary (..)) import Test.QuickCheck.Gen (elements) import qualified Web.Scim.Schema.Common as Scim import qualified Web.Scim.Schema.User as Scim.User diff --git a/services/spar/src/Spar/Scim/User.hs b/services/spar/src/Spar/Scim/User.hs index c1dcb6ddfe5..aaf4d56d418 100644 --- a/services/spar/src/Spar/Scim/User.hs +++ b/services/spar/src/Spar/Scim/User.hs @@ -851,7 +851,15 @@ deleteScimUser tokeninfo@ScimTokenInfo {stiTeam, stiIdP} uid = deleteUserInSpar account = do mIdpConfig <- mapM (lift . IdPConfigStore.getConfig) stiIdP - case Brig.veidFromBrigUser account ((^. SAML.idpMetadata . SAML.edIssuer) <$> mIdpConfig) account.userEmailUnvalidated of + -- delete user with idp associated *before* this update. + case Brig.oldVeidFromBrigUser account of + Nothing -> pure () + Just veid -> lift $ do + for_ (justThere veid.validScimIdAuthInfo) (SAMLUserStore.delete uid) + ScimExternalIdStore.delete stiTeam veid.validScimIdExternal + + -- delete user with idp associated to current scim token. + case Brig.newVeidFromBrigUser account ((^. SAML.idpMetadata . SAML.edIssuer) <$> mIdpConfig) of Left _ -> pure () Right veid -> lift $ do for_ (justThere veid.validScimIdAuthInfo) (SAMLUserStore.delete uid) @@ -1101,34 +1109,32 @@ getUserById :: MaybeT (Scim.ScimHandler (Sem r)) (Scim.StoredUser ST.SparTag) getUserById midp stiTeam uid = do brigUser <- MaybeT . lift $ BrigAccess.getAccount Brig.WithPendingInvitations uid - let mbveid = - Brig.veidFromBrigUser - brigUser - ((^. SAML.idpMetadata . SAML.edIssuer) <$> midp) - brigUser.userEmailUnvalidated - case mbveid of + let mbOldVeid = Brig.oldVeidFromBrigUser brigUser + mbNewVeid = Brig.newVeidFromBrigUser brigUser ((^. SAML.idpMetadata . SAML.edIssuer) <$> midp) + case mbNewVeid of Right veid | userTeam brigUser == Just stiTeam -> lift $ do storedUser :: Scim.StoredUser ST.SparTag <- synthesizeStoredUser brigUser veid -- if we get a user from brig that hasn't been touched by scim yet, we call this -- function to move it under scim control. assertExternalIdNotUsedElsewhere stiTeam veid uid + handleVeidChange brigUser mbOldVeid veid createValidScimUserSpar stiTeam uid storedUser veid - lift $ do - when (veidChanged brigUser veid) $ - BrigAccess.setSSOId uid (veidToUserSSOId veid) - when (managedByChanged brigUser) $ - BrigAccess.setManagedBy uid ManagedByScim pure storedUser _ -> Applicative.empty where - veidChanged :: User -> ST.ValidScimId -> Bool - veidChanged usr veid = case userIdentity usr of - Nothing -> True - Just (EmailIdentity _) -> True - Just (SSOIdentity ssoid _) -> Brig.veidToUserSSOId veid /= ssoid - - managedByChanged :: User -> Bool - managedByChanged usr = userManagedBy usr /= ManagedByScim + handleVeidChange :: User -> Maybe ValidScimId -> ValidScimId -> Scim.ScimHandler (Sem r) () + handleVeidChange brigUser mbOldVeid newVeid = do + -- set sso_id + when (mbOldVeid /= Just newVeid) do + lift $ BrigAccess.setSSOId uid (veidToUserSSOId newVeid) + -- set managed_by + when (userManagedBy brigUser /= ManagedByScim) do + lift $ BrigAccess.setManagedBy uid ManagedByScim + -- remove dangling entry from spar.user_v2 table (cassandra) + case mbOldVeid of + Just oldVeid | ST.veidUref newVeid /= ST.veidUref oldVeid -> do + lift $ SAMLUserStore.delete uid `mapM_` ST.veidUref oldVeid + _ -> pure () scimFindUserByHandle :: forall r. diff --git a/services/spar/src/Spar/Sem/IdPConfigStore/Cassandra.hs b/services/spar/src/Spar/Sem/IdPConfigStore/Cassandra.hs index bec55944aa9..ca771dbab78 100644 --- a/services/spar/src/Spar/Sem/IdPConfigStore/Cassandra.hs +++ b/services/spar/src/Spar/Sem/IdPConfigStore/Cassandra.hs @@ -38,6 +38,7 @@ import Spar.Data.Instances () import Spar.Error import Spar.Sem.IdPConfigStore (IdPConfigStore (..), Replaced (..), Replacing (..)) import URI.ByteString +import Wire.API.Routes.Public (ZHostValue) import Wire.API.User.IdentityProvider hiding (apiVersion, oldIssuers, replacedBy, team) import qualified Wire.API.User.IdentityProvider as IP import {- instance Cql SAML.IdPId -} Wire.DomainRegistrationStore.Cassandra () @@ -67,7 +68,7 @@ idPToCassandra = ClearReplacedBy r -> embed @m $ clearReplacedBy r DeleteIssuer i t -> embed @m $ deleteIssuer i t -type IdPConfigRow = (SAML.IdPId, SAML.Issuer, URI, SignedCertificate, [SignedCertificate], TeamId, Maybe WireIdPAPIVersion, [SAML.Issuer], Maybe SAML.IdPId, Maybe Text) +type IdPConfigRow = (SAML.IdPId, SAML.Issuer, URI, SignedCertificate, [SignedCertificate], TeamId, Maybe WireIdPAPIVersion, [SAML.Issuer], Maybe SAML.IdPId, Maybe Text, Maybe ZHostValue) insertIdPConfig :: forall m. @@ -91,7 +92,8 @@ insertIdPConfig idp = do idp ^. SAML.idpExtraInfo . IP.apiVersion, idp ^. SAML.idpExtraInfo . IP.oldIssuers, idp ^. SAML.idpExtraInfo . IP.replacedBy, - Just (unIdPHandle $ idp ^. SAML.idpExtraInfo . handle) + Just (unIdPHandle $ idp ^. SAML.idpExtraInfo . handle), + idp ^. SAML.idpExtraInfo . IP.domain ) addPrepQuery byIssuer @@ -119,7 +121,7 @@ insertIdPConfig idp = do getAllIdPsByIssuerUnsafe issuer >>= mapM_ (failIfNot thisVersion) ins :: PrepQuery W IdPConfigRow () - ins = "INSERT INTO idp (idp, issuer, request_uri, public_key, extra_public_keys, team, api_version, old_issuers, replaced_by, handle) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + ins = "INSERT INTO idp (idp, issuer, request_uri, public_key, extra_public_keys, team, api_version, old_issuers, replaced_by, handle, domain) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" -- FUTUREWORK: migrate `spar.issuer_idp` away, `spar.issuer_idp_v2` is enough. byIssuer :: PrepQuery W (SAML.Issuer, TeamId, SAML.IdPId) () @@ -177,18 +179,19 @@ getIdPConfig idpid = do apiVersion, oldIssuers, replacedBy, - mHandle + mHandle, + idpDomain ) = do let _edCertAuthnResponse = certsHead NL.:| certsTail _idpMetadata = SAML.IdPMetadata {..} - _idpExtraInfo = WireIdP teamId apiVersion oldIssuers replacedBy (mkHandle mHandle) + _idpExtraInfo = WireIdP teamId apiVersion oldIssuers replacedBy (mkHandle mHandle) idpDomain pure $ SAML.IdPConfig {..} mbidp <- traverse toIdp =<< retry x1 (query1 sel $ params LocalQuorum (Identity idpid)) maybe (throwError IdpNotFound) pure mbidp where sel :: PrepQuery R (Identity SAML.IdPId) IdPConfigRow - sel = "SELECT idp, issuer, request_uri, public_key, extra_public_keys, team, api_version, old_issuers, replaced_by, handle FROM idp WHERE idp = ?" + sel = "SELECT idp, issuer, request_uri, public_key, extra_public_keys, team, api_version, old_issuers, replaced_by, handle, domain FROM idp WHERE idp = ?" selTid :: PrepQuery R (Identity SAML.IdPId) (Identity (Maybe TeamId)) selTid = "SELECT team FROM idp WHERE idp = ?" diff --git a/services/spar/src/Spar/Sem/SAML2/Library.hs b/services/spar/src/Spar/Sem/SAML2/Library.hs index 78e8c08c3c9..7ca06008ffb 100644 --- a/services/spar/src/Spar/Sem/SAML2/Library.hs +++ b/services/spar/src/Spar/Sem/SAML2/Library.hs @@ -59,7 +59,7 @@ wrapMonadClientSPImpl action = . SAML.CustomError . SparCassandraError . LText.pack - . show @SomeException + . displayException @SomeException ) instance (Member (Final IO) r) => Catch.MonadThrow (SPImpl r) where diff --git a/services/spar/src/Spar/Sem/Utils.hs b/services/spar/src/Spar/Sem/Utils.hs index 381b2881717..0cac0ec9db4 100644 --- a/services/spar/src/Spar/Sem/Utils.hs +++ b/services/spar/src/Spar/Sem/Utils.hs @@ -64,7 +64,7 @@ interpretClientToIO ctx = interpret $ \case . SAML.CustomError . SparCassandraError . LText.pack - . show @SomeException + . displayException @SomeException pure $ action' `Catch.catch` \e -> handler' $ e <$ st ttlErrorToSparError :: (Member (Error SparError) r) => Sem (Error TTLError ': r) a -> Sem r a diff --git a/services/spar/test-integration/Test/Spar/APISpec.hs b/services/spar/test-integration/Test/Spar/APISpec.hs index 7ea165fcb95..6c1735c98b3 100644 --- a/services/spar/test-integration/Test/Spar/APISpec.hs +++ b/services/spar/test-integration/Test/Spar/APISpec.hs @@ -929,7 +929,7 @@ specCRUDIdentityProvider = do rawmeta <- call $ callIdpGetRaw (env ^. teSpar) (Just owner) (idp ^. idpId) liftIO $ do idp `shouldBe` idp' - let prefix = " do let updateOrReplaceIdps :: (UserId, IdP, SAML.IdPMetadata) -> TestSpar () @@ -1466,8 +1466,7 @@ specSsoSettings = do callGetDefaultSsoCode (env ^. teSpar) `shouldRespondWith` \resp -> (statusCode resp == 200) - && ( responseJsonEither resp == Right (ssoSettings (Just idpid1)) - ) + && (responseJsonEither resp == Right (ssoSettings (Just idpid1))) -- update to 2 callSetDefaultSsoCode (env ^. teSpar) idpid2 `shouldRespondWith` \resp -> @@ -1476,8 +1475,7 @@ specSsoSettings = do callGetDefaultSsoCode (env ^. teSpar) `shouldRespondWith` \resp -> (statusCode resp == 200) - && ( responseJsonEither resp == Right (ssoSettings (Just idpid2)) - ) + && (responseJsonEither resp == Right (ssoSettings (Just idpid2))) it "allows removing the default SSO code" $ do env <- ask (userid, _teamid) <- callCreateUserWithTeam @@ -1494,8 +1492,7 @@ specSsoSettings = do callGetDefaultSsoCode (env ^. teSpar) `shouldRespondWith` \resp -> (statusCode resp == 200) - && ( responseJsonEither resp == Right (ssoSettings Nothing) - ) + && (responseJsonEither resp == Right (ssoSettings Nothing)) it "removes the default SSO code if the IdP gets removed" $ do env <- ask (userid, _teamid) <- callCreateUserWithTeam @@ -1511,8 +1508,7 @@ specSsoSettings = do callGetDefaultSsoCode (env ^. teSpar) `shouldRespondWith` \resp -> (statusCode resp == 200) - && ( responseJsonEither resp == Right (ssoSettings Nothing) - ) + && (responseJsonEither resp == Right (ssoSettings Nothing)) where ssoSettings maybeCode = object diff --git a/services/spar/test-integration/Test/Spar/DataSpec.hs b/services/spar/test-integration/Test/Spar/DataSpec.hs index 49b5db4690b..ec4b589145c 100644 --- a/services/spar/test-integration/Test/Spar/DataSpec.hs +++ b/services/spar/test-integration/Test/Spar/DataSpec.hs @@ -201,14 +201,14 @@ spec = do it "getIdPConfigsByTeam works" $ do skipIdPAPIVersions [WireIdPAPIV1] teamid <- nextWireId - idp <- makeTestIdP <&> idpExtraInfo .~ WireIdP teamid Nothing [] Nothing (IdPHandle "IdP 1") + idp <- makeTestIdP <&> idpExtraInfo .~ WireIdP teamid Nothing [] Nothing (IdPHandle "IdP 1") Nothing () <- runSpar $ IdPEffect.insertConfig idp idps <- runSpar $ IdPEffect.getConfigsByTeam teamid liftIO $ idps `shouldBe` [idp] it "deleteIdPConfig works" $ do teamid <- nextWireId idpApiVersion <- asks (^. teWireIdPAPIVersion) - idp <- makeTestIdP <&> idpExtraInfo .~ WireIdP teamid (Just idpApiVersion) [] Nothing (IdPHandle "IdP 1") + idp <- makeTestIdP <&> idpExtraInfo .~ WireIdP teamid (Just idpApiVersion) [] Nothing (IdPHandle "IdP 1") Nothing () <- runSpar $ IdPEffect.insertConfig idp do midp <- runSpar $ IdPEffect.getConfig (idp ^. idpId) diff --git a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs index 965a854b60c..48a631f3a27 100644 --- a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs @@ -41,12 +41,13 @@ import qualified Data.Aeson as Aeson import Data.Aeson.Lens (key, _String) import Data.Aeson.QQ (aesonQQ) import Data.Aeson.Types (fromJSON, toJSON) +import Data.ByteString (toStrict) import Data.ByteString.Conversion import qualified Data.CaseInsensitive as CI import qualified Data.Csv as Csv import Data.Handle (Handle, fromHandle, parseHandle, parseHandleEither) import Data.HavePendingInvitations -import Data.Id (TeamId, UserId, randomId) +import Data.Id (TeamId, UserId, idToText, randomId) import Data.Ix (inRange) import Data.LanguageCodes (ISO639_1 (..)) import Data.Misc (HttpsUrl, mkHttpsUrl) @@ -202,6 +203,101 @@ specImportToScimFromSAML = (Just !uid') <- createViaSaml idp privCreds uref liftIO $ uid' `shouldBe` uid + -- create new scim token detached from saml + tok2 :: ScimToken <- do + registerScimToken teamid Nothing + + -- sync user + storedUserGot2 <- do + resp <- + aFewTimes (getUser_ (Just tok2) uid =<< view teSpar) ((== 200) . statusCode) + >= \(Just acc) -> liftIO $ do + userIdentity acc + `shouldBe` let emailText :: Text = decodeUtf8 $ toStrict $ toByteString email + in Just (SSOIdentity (UserScimExternalId emailText) (Just email)) + + -- the idp gets deleted now, but we'll see below that the user account survives. + call $ callIdpDelete (env ^. teSpar) (Just owner) (idp ^. SAML.idpId) + + -- password reset should work + let newPassword :: Text = "a8b7c1d8-d425-11f0-abbb-637eaf3793ee" + passwdReset env email + passResetToken <- stealPasswdResetToken env email + passwdResetComplete env email passResetToken newPassword + + -- ... after that, password login should work + login env (Aeson.object ["email" Aeson..= email, "password" Aeson..= newPassword]) + + -- after changing scim external id, login still works. + let patchOp = PatchOp.PatchOp [replaceAttrib "externalId" (idToText uid)] + where + replaceAttrib :: Text -> Text -> PatchOp.Operation + replaceAttrib name value = + PatchOp.Operation + PatchOp.Replace + (Just (PatchOp.NormalPath (Filter.topLevelAttrPath name))) + (Just (toJSON value)) + in do + patchUser_ (Just tok2) (Just uid) patchOp (env ^. teSpar) !!! const 200 === statusCode + login env (Aeson.object ["email" Aeson..= email, "password" Aeson..= ("a8b7c1d8-d425-11f0-abbb-637eaf3793ee" :: Text)]) + + passwdReset :: TestEnv -> EmailAddress -> (MonadReader TestEnv m, MonadIO m) => m () + passwdReset env email = + void . call . post $ + (versioned "v13") + . (env ^. teBrig) + . path "/password-reset" + . contentJson + . json (Aeson.object ["email" Aeson..= email]) + . expect2xx + + passwdResetComplete :: TestEnv -> EmailAddress -> Text -> Text -> (MonadReader TestEnv m, MonadIO m) => m () + passwdResetComplete env email passResetToken password = + void . call . post $ + (versioned "v13") + . (env ^. teBrig) + . path "/password-reset/complete" + . contentJson + . json + ( Aeson.object + [ "email" Aeson..= email, + "code" Aeson..= passResetToken, + "password" Aeson..= password + ] + ) + . expect2xx + + stealPasswdResetToken :: TestEnv -> EmailAddress -> (MonadReader TestEnv m, MonadIO m) => m Text + stealPasswdResetToken env (toStrict . toByteString -> email) = do + resp <- + call . get $ + (env ^. teBrig) + . path "/i/users/password-reset-code" + . contentJson + . queryItem "email" email + . expect2xx + maybe (error "could not find and/or parse passwd reset token") pure $ + responseBody resp ^? _Just . key "code" . _String + + login :: TestEnv -> Aeson.Value -> (MonadReader TestEnv m, MonadIO m) => m () + login env loginBody = + void . call . post $ + (versioned "v13") + . (env ^. teBrig) + . path "/login" + . contentJson + . queryItem "persist" "true" + . body (RequestBodyLBS (Aeson.encode loginBody)) + . expect2xx + specImportToScimFromInvitation :: SpecWith TestEnv specImportToScimFromInvitation = describe "Create with TM invitation; then re-provision with SCIM" $ do @@ -1230,7 +1326,8 @@ testFindSamlAutoProvisionedUserMigratedWithEmailInTeamWithSSO = do runSpar $ BrigAccess.setHandle uid handle pure usr let memberIdWithSSO = userId memberWithSSO - externalId = either error id $ veidToText =<< Intra.veidFromBrigUser memberWithSSO Nothing Nothing + idpIssuer = idp ^. SAML.idpMetadata . SAML.edIssuer + externalId = either error id $ veidToText =<< Intra.newVeidFromBrigUser memberWithSSO (Just idpIssuer) -- NOTE: once SCIM is enabled, SSO auto-provisioning is disabled tok <- registerScimToken teamid (Just (idp ^. SAML.idpId)) @@ -2134,15 +2231,17 @@ specDeleteUser = do !!! const 405 === statusCode describe "DELETE /Users/:id" $ do it "should delete user from brig, spar.scim_user_times, spar.user" $ do - (tok, _) <- registerIdPAndScimToken + (tok, (_, _, idp)) <- registerIdPAndScimToken user <- randomScimUser storedUser <- createUser tok user let uid :: UserId = scimUserId storedUser uref :: SAML.UserRef <- do mUsr <- runSpar $ BrigAccess.getAccount Intra.WithPendingInvitations uid - let err = error . ("brig user without UserRef: " <>) . show - case (\usr -> Intra.veidFromBrigUser usr Nothing Nothing) <$> mUsr of - bad@(Just (Right veid)) -> runValidScimIdEither pure (const $ err bad) veid + let cond usr = Intra.newVeidFromBrigUser usr (Just (idp ^. SAML.idpMetadata . SAML.edIssuer)) + good bad = runValidScimIdEither pure (const $ err bad) + err bad = error $ "brig user without UserRef: " <> show (bad, user) + case cond <$> mUsr of + bad@(Just (Right veid)) -> good bad veid bad -> err bad spar <- view teSpar deleteUser_ (Just tok) (Just uid) spar diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index c7b701ae801..f6c8e25d73c 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -545,7 +545,7 @@ nextWireId :: (MonadIO m) => m (Id a) nextWireId = Id <$> liftIO UUID.nextRandom nextWireIdP :: (MonadIO m) => WireIdPAPIVersion -> m WireIdP -nextWireIdP version = WireIdP <$> iid <*> pure (Just version) <*> pure [] <*> pure Nothing <*> idpHandle +nextWireIdP version = WireIdP <$> iid <*> pure (Just version) <*> pure [] <*> pure Nothing <*> idpHandle <*> pure Nothing where iid = Id <$> liftIO UUID.nextRandom idpHandle = iid <&> IdPHandle . pack . show diff --git a/services/spar/test-integration/Util/Scim.hs b/services/spar/test-integration/Util/Scim.hs index acfad1fe0a2..02fb3bae4e0 100644 --- a/services/spar/test-integration/Util/Scim.hs +++ b/services/spar/test-integration/Util/Scim.hs @@ -650,8 +650,8 @@ instance IsUser (WrappedScimUser SparTag) where maybeUserId = Nothing maybeHandle = Just (parseHandle . Scim.User.userName . fromWrappedScimUser) maybeName = Just (fmap Name . Scim.User.displayName . fromWrappedScimUser) - maybeTenant = Nothing - maybeSubject = Nothing + maybeTenant = Nothing -- we don't know from the scim schema. + maybeSubject = Nothing -- dito. maybeScimExternalId = Just $ Scim.User.externalId . fromWrappedScimUser maybeLocale = Just @@ -666,21 +666,12 @@ instance IsUser User where maybeUserId = Just userId maybeHandle = Just userHandle maybeName = Just (Just . userDisplayName) - maybeTenant = Just $ \usr -> - Intra.veidFromBrigUser usr Nothing Nothing - & either - (const Nothing) - (fmap SAML._uidTenant . veidUref) - maybeSubject = Just $ \usr -> - Intra.veidFromBrigUser usr Nothing Nothing - & either - (const Nothing) - (fmap SAML._uidSubject . veidUref) - maybeScimExternalId = Just $ \usr -> - Intra.veidFromBrigUser usr Nothing Nothing - & either - (const Nothing) - (runValidScimIdEither Intra.urefToExternalId (Just . fromEmail)) + maybeTenant = Just $ (fmap SAML._uidTenant . veidUref) <=< Intra.oldVeidFromBrigUser + maybeSubject = Just $ (fmap SAML._uidSubject . veidUref) <=< Intra.oldVeidFromBrigUser + maybeScimExternalId = + Just $ + Intra.oldVeidFromBrigUser + >=> (runValidScimIdEither Intra.urefToExternalId (Just . fromEmail)) maybeLocale = Just $ Just . userLocale -- | For all properties that are present in both @u1@ and @u2@, check that they match. diff --git a/services/wire-server-enterprise b/services/wire-server-enterprise index 5260f0d5038..8950f728178 160000 --- a/services/wire-server-enterprise +++ b/services/wire-server-enterprise @@ -1 +1 @@ -Subproject commit 5260f0d5038441351e878afaaaaa38830db87c18 +Subproject commit 8950f728178bc7d16c83303f2aa66a6321f3c29e diff --git a/tools/db/find-undead/src/Main.hs b/tools/db/find-undead/src/Main.hs index 5bc9506308e..ac4eaa00cfc 100644 --- a/tools/db/find-undead/src/Main.hs +++ b/tools/db/find-undead/src/Main.hs @@ -24,7 +24,7 @@ where import Cassandra as C import Cassandra.Settings as C -import Data.Text as Text +import Data.Text as Text hiding (show) import Database.Bloodhound qualified as ES import Imports import Network.HTTP.Client qualified as HTTP diff --git a/tools/db/mls-users/.ormolu b/tools/db/mls-users/.ormolu new file mode 120000 index 00000000000..ffc2ca9745e --- /dev/null +++ b/tools/db/mls-users/.ormolu @@ -0,0 +1 @@ +../../../.ormolu \ No newline at end of file diff --git a/tools/db/mls-users/README.md b/tools/db/mls-users/README.md new file mode 100644 index 00000000000..2da2fbeec6e --- /dev/null +++ b/tools/db/mls-users/README.md @@ -0,0 +1,3 @@ +# MLS users + +This program scans brig's users table and finds active users that don't support MLS. diff --git a/tools/db/mls-users/app/Main.hs b/tools/db/mls-users/app/Main.hs new file mode 100644 index 00000000000..18769e51240 --- /dev/null +++ b/tools/db/mls-users/app/Main.hs @@ -0,0 +1,23 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Main where + +import qualified MlsUsers.Lib as Lib + +main :: IO () +main = Lib.main diff --git a/tools/db/mls-users/default.nix b/tools/db/mls-users/default.nix new file mode 100644 index 00000000000..0f1c8695642 --- /dev/null +++ b/tools/db/mls-users/default.nix @@ -0,0 +1,52 @@ +# WARNING: GENERATED FILE, DO NOT EDIT. +# This file is generated by running hack/bin/generate-local-nix-packages.sh and +# must be regenerated whenever local packages are added or removed, or +# dependencies are added or removed. +{ mkDerivation +, aeson +, aeson-pretty +, base +, bytestring +, cassandra-util +, conduit +, containers +, cql +, extra +, gitignoreSource +, imports +, lens +, lib +, optparse-applicative +, time +, tinylog +, types-common +, wire-api +}: +mkDerivation { + pname = "mls-users"; + version = "1.0.0"; + src = gitignoreSource ./.; + isLibrary = true; + isExecutable = true; + libraryHaskellDepends = [ + aeson + aeson-pretty + bytestring + cassandra-util + conduit + containers + cql + extra + imports + lens + optparse-applicative + time + tinylog + types-common + wire-api + ]; + executableHaskellDepends = [ base ]; + description = "Find users without MLS support"; + license = lib.licenses.agpl3Only; + mainProgram = "mls-users"; +} diff --git a/tools/db/mls-users/mls-users.cabal b/tools/db/mls-users/mls-users.cabal new file mode 100644 index 00000000000..1ee86961787 --- /dev/null +++ b/tools/db/mls-users/mls-users.cabal @@ -0,0 +1,99 @@ +cabal-version: 3.0 +name: mls-users +version: 1.0.0 +synopsis: Find users without MLS support +category: Network +author: Wire Swiss GmbH +maintainer: Wire Swiss GmbH +copyright: (c) 2025 Wire Swiss GmbH +license: AGPL-3.0-only +build-type: Simple + +library + hs-source-dirs: src + default-language: Haskell2010 + exposed-modules: + MlsUsers.Lib + MlsUsers.Types + + ghc-options: + -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates + -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -funbox-strict-fields -threaded -with-rtsopts=-N + -Wredundant-constraints -Wunused-packages + + build-depends: + , aeson + , aeson-pretty + , bytestring + , cassandra-util + , conduit + , containers + , cql + , extra + , imports + , lens + , optparse-applicative + , time + , tinylog + , types-common + , wire-api + + default-extensions: + AllowAmbiguousTypes + BangPatterns + ConstraintKinds + DataKinds + DefaultSignatures + DeriveFunctor + DeriveGeneric + DeriveLift + DeriveTraversable + DerivingStrategies + DerivingVia + DuplicateRecordFields + EmptyCase + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + GeneralizedNewtypeDeriving + InstanceSigs + KindSignatures + LambdaCase + MultiParamTypeClasses + MultiWayIf + NamedFieldPuns + NoImplicitPrelude + OverloadedLabels + OverloadedRecordDot + OverloadedStrings + PackageImports + PatternSynonyms + PolyKinds + QuasiQuotes + RankNTypes + RecordWildCards + ScopedTypeVariables + StandaloneDeriving + TupleSections + TypeApplications + TypeFamilies + TypeFamilyDependencies + TypeOperators + UndecidableInstances + ViewPatterns + +executable mls-users + main-is: Main.hs + build-depends: + , base + , mls-users + + hs-source-dirs: app + default-language: Haskell2010 + ghc-options: + -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates + -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -funbox-strict-fields -threaded -with-rtsopts=-N + -Wredundant-constraints -Wunused-packages diff --git a/tools/db/mls-users/src/MlsUsers/Lib.hs b/tools/db/mls-users/src/MlsUsers/Lib.hs new file mode 100644 index 00000000000..9b23af9090d --- /dev/null +++ b/tools/db/mls-users/src/MlsUsers/Lib.hs @@ -0,0 +1,124 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2025 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 MlsUsers.Lib where + +import Cassandra as C hiding (All) +import Cassandra.Settings as C hiding (All) +import Conduit +import Control.Monad.Extra +import qualified Data.Conduit.Combinators as Conduit +import qualified Data.Conduit.List as ConduitL +import Data.Id +import qualified Data.Set as Set +import Data.Time.Clock +import qualified Database.CQL.Protocol as CQL +import Imports +import MlsUsers.Types +import Options.Applicative +import qualified System.Logger as Log +import System.Logger.Message ((.=), (~~)) +import Wire.API.User + +getUserResult :: Log.Logger -> ClientState -> UserRow -> IO Result +getUserResult logger brigClient ur = do + matches <- + andM + [ pure ur.activated, + pure $ ur.status == Just Active, + pure $ Set.notMember BaseProtocolMLSTag ur.supportedProtocols, + -- check that the user has at least one active client + do + now <- getCurrentTime + tms <- catMaybes <$> lookupClientsLastActiveTimestamps brigClient ur.userId + let active = any (\tm -> diffUTCTime now tm < 90 * nominalDay) tms + when active + $ Log.info logger + $ "user_record" + .= show ur + ~~ "last_active_timestamps" .= show tms + ~~ Log.msg (Log.val "active user found") + pure active + ] + + pure + Result + { totalUsers = 1, + activeNoMLS = if matches then 1 else 0 + } + +process :: Log.Logger -> Maybe Int -> ClientState -> IO Result +process logger limit brigClient = + runConduit + $ readUsers brigClient + .| Conduit.concat + .| (maybe (mapC id) takeC limit) + -- process users in chunks, yield a Result for each chunk + .| forever + ( ConduitL.isolate 10000 + .| (foldMapMC (getUserResult logger brigClient) >>= yield) + ) + .| Conduit.takeWhile ((> 0) . totalUsers) + -- join all results and log + .| ConduitL.scan (<>) mempty + `fuseUpstream` Conduit.mapM_ (\r -> Log.info logger $ "intermediate_result" .= show r) + +main :: IO () +main = do + opts <- execParser (info (helper <*> optsParser) desc) + logger <- initLogger + brigClient <- initCas opts.brigDb logger + result <- process logger opts.limit brigClient + Log.info logger $ "result" .= show result + where + initLogger = + Log.new + . Log.setLogLevel Log.Info + . Log.setOutput Log.StdOut + . Log.setFormat Nothing + . Log.setBufSize 0 + $ Log.defSettings + initCas settings l = + C.init + . C.setLogger (C.mkLogger l) + . C.setContacts settings.host [] + . C.setPortNumber (fromIntegral settings.port) + . C.setKeyspace settings.keyspace + . C.setProtocolVersion C.V4 + $ C.defSettings + desc = header "mls-users" <> progDesc "This program scans brig's users table and finds active users that don't support MLS" <> fullDesc + +-------------------------------------------------------------------------------- +-- queries + +lookupClientsLastActiveTimestamps :: ClientState -> UserId -> IO [Maybe UTCTime] +lookupClientsLastActiveTimestamps client u = do + runClient client $ runIdentity <$$> retry x1 (query selectClients (params One (Identity u))) + where + selectClients :: PrepQuery R (Identity UserId) (Identity (Maybe UTCTime)) + selectClients = "SELECT last_active from clients where user = ?" + +readUsers :: ClientState -> ConduitM () [UserRow] IO () +readUsers client = + transPipe (runClient client) (paginateC selectUsersAll (paramsP One () 1000) x5) + .| Conduit.map (fmap CQL.asRecord) + where + selectUsersAll :: C.PrepQuery C.R () (CQL.TupleType UserRow) + selectUsersAll = + "SELECT id, activated, status, supported_protocols FROM user" diff --git a/tools/db/mls-users/src/MlsUsers/Types.hs b/tools/db/mls-users/src/MlsUsers/Types.hs new file mode 100644 index 00000000000..49f219afd55 --- /dev/null +++ b/tools/db/mls-users/src/MlsUsers/Types.hs @@ -0,0 +1,123 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE TemplateHaskell #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2024 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 MlsUsers.Types where + +import qualified Cassandra as C +import Control.Lens +import qualified Data.Aeson as A +import qualified Data.Aeson.Encode.Pretty as A +import qualified Data.ByteString.Lazy.Char8 as LC8 +import Data.Id +import Data.Text.Strict.Lens +import Database.CQL.Protocol (Record (..), TupleType, recordInstance) +import Imports +import Options.Applicative +import Wire.API.User + +data UserRow = UserRow + { userId :: UserId, + activated :: Bool, + status :: Maybe AccountStatus, + supportedProtocols :: Set BaseProtocolTag + } + deriving (Generic) + +instance A.ToJSON UserRow + +recordInstance ''UserRow + +instance Show UserRow where + show = LC8.unpack . A.encodePretty + +data Result = Result + { totalUsers :: Int, + activeNoMLS :: Int + } + deriving (Generic) + +instance A.ToJSON Result + +instance Show Result where + show = LC8.unpack . A.encodePretty + +instance Semigroup Result where + r1 <> r2 = + Result + { totalUsers = r1.totalUsers + r2.totalUsers, + activeNoMLS = r1.activeNoMLS + r2.activeNoMLS + } + +instance Monoid Result where + mempty = Result {totalUsers = 0, activeNoMLS = 0} + +data CassandraSettings = CassandraSettings + { host :: String, + port :: Int, + keyspace :: C.Keyspace + } + +data Opts = Opts + { brigDb :: CassandraSettings, + limit :: Maybe Int + } + +optsParser :: Parser Opts +optsParser = + Opts + <$> brigCassandraParser + <*> optional + ( option + auto + ( long "limit" + <> short 'l' + <> metavar "INT" + <> help "Limit the number of users to process" + ) + ) + +brigCassandraParser :: Parser CassandraSettings +brigCassandraParser = + CassandraSettings + <$> strOption + ( long "brig-cassandra-host" + <> metavar "HOST" + <> help "Cassandra Host for brig" + <> value "localhost" + <> showDefault + ) + <*> option + auto + ( long "brig-cassandra-port" + <> metavar "PORT" + <> help "Cassandra Port for brig" + <> value 9042 + <> showDefault + ) + <*> ( C.Keyspace + . view packed + <$> strOption + ( long "brig-cassandra-keyspace" + <> metavar "STRING" + <> help "Cassandra Keyspace for brig" + <> value "brig_test" + <> showDefault + ) + ) diff --git a/tools/entreprise-provisioning/default.nix b/tools/entreprise-provisioning/default.nix index b2e1cd807af..ac8d0863103 100644 --- a/tools/entreprise-provisioning/default.nix +++ b/tools/entreprise-provisioning/default.nix @@ -23,6 +23,7 @@ , types-common , uuid , vector +, wai-utilities , wire-api }: mkDerivation { @@ -46,6 +47,7 @@ mkDerivation { types-common uuid vector + wai-utilities wire-api ]; testHaskellDepends = [ diff --git a/tools/entreprise-provisioning/entreprise-provisioning.cabal b/tools/entreprise-provisioning/entreprise-provisioning.cabal index fa89078a1b9..61f914ce513 100644 --- a/tools/entreprise-provisioning/entreprise-provisioning.cabal +++ b/tools/entreprise-provisioning/entreprise-provisioning.cabal @@ -37,6 +37,7 @@ executable entreprise-provisioning , types-common , uuid , vector + , wai-utilities , wire-api ghc-options: diff --git a/tools/entreprise-provisioning/src/API.hs b/tools/entreprise-provisioning/src/API.hs index 90bca4861bf..f17908a4148 100644 --- a/tools/entreprise-provisioning/src/API.hs +++ b/tools/entreprise-provisioning/src/API.hs @@ -34,6 +34,7 @@ import Data.Vector qualified as V import Imports import Network.HTTP.Client import Network.HTTP.Types.Status +import Network.Wai.Utilities.Exception (displayExceptionNoBacktrace) import Types import Wire.API.Conversation import Wire.API.Conversation.Role (roleNameWireAdmin) @@ -85,7 +86,7 @@ createChannel manager (ApiUrl apiUrl) (Token token) userId teamId channelName = result <- try $ httpLbs request manager case result of Left (e :: HttpException) -> - pure $ Left $ ErrorDetail 0 (object ["error" .= show e]) + pure $ Left $ ErrorDetail 0 (object ["error" .= displayExceptionNoBacktrace e]) Right resp -> let respStatus = statusCode (responseStatus resp) in case respStatus of @@ -130,7 +131,7 @@ associateChannelsToGroup manager (ApiUrl apiUrl) (Token token) userId groupId co result <- try $ httpLbs request manager case result of Left (e :: HttpException) -> - pure $ Left $ ErrorDetail 0 (object ["error" .= show e]) + pure $ Left $ ErrorDetail 0 (object ["error" .= displayExceptionNoBacktrace e]) Right resp -> case statusCode (responseStatus resp) of 200 -> pure $ Right () diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index 98fcb14c3cc..0ba12df2135 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -52,6 +52,7 @@ import Imports hiding (head) import Network.HTTP.Types import Network.Wai import Network.Wai.Utilities as Wai +import Network.Wai.Utilities.Exception (displayExceptionNoBacktrace) import Network.Wai.Utilities.Server import Network.Wai.Utilities.Server qualified as Server import Servant (NoContent (NoContent), ServerT, (:<|>) (..)) @@ -173,7 +174,9 @@ sitemap' = :<|> Named @"get-route-enforce-file-download-location" (mkFeatureGetRoute @EnforceFileDownloadLocationConfig) :<|> Named @"put-route-enforce-file-download-location" (mkFeaturePutRoute @EnforceFileDownloadLocationConfig) :<|> Named @"get-route-cells" (mkFeatureGetRoute @CellsConfig) - :<|> Named @"put-route-cells" (mkFeatureStatusPutRoute @CellsConfig) + :<|> Named @"put-route-cells" (mkFeaturePutRoute @CellsConfig) + :<|> Named @"get-route-cells-internal" (mkFeatureGetRoute @CellsInternalConfig) + :<|> Named @"put-route-cells-internal" (mkFeaturePutRoute @CellsInternalConfig) :<|> Named @"get-route-guest-links" (mkFeatureGetRoute @GuestLinksConfig) :<|> Named @"put-route-guest-links" (mkFeatureStatusPutRoute @GuestLinksConfig) :<|> Named @"get-route-self-deleting-messages" (mkFeatureGetRoute @SelfDeletingMessagesConfig) @@ -190,6 +193,10 @@ sitemap' = :<|> Named @"put-route-apps-config" (mkFeatureStatusPutRoute @AppsConfig) :<|> Named @"get-route-stealth-users-config" (mkFeatureGetRoute @StealthUsersConfig) :<|> Named @"put-route-stealth-users-config" (mkFeatureStatusPutRoute @StealthUsersConfig) + :<|> Named @"get-route-meetings-config" (mkFeatureGetRoute @MeetingsConfig) + :<|> Named @"put-route-meetings-config" (mkFeatureStatusPutRoute @MeetingsConfig) + :<|> Named @"get-route-meetings-premium-config" (mkFeatureGetRoute @MeetingsPremiumConfig) + :<|> Named @"put-route-meetings-premium-config" (mkFeatureStatusPutRoute @MeetingsPremiumConfig) :<|> Named @"get-team-invoice" getTeamInvoice :<|> Named @"get-team-billing-info" getTeamBillingInfo :<|> Named @"put-team-billing-info" updateTeamBillingInfo @@ -224,6 +231,8 @@ sitemap' = :<|> Named @"lock-unlock-route-consumable-notifications-config" (mkFeatureLockUnlockRoute @ConsumableNotificationsConfig) :<|> Named @"lock-unlock-route-chat-bubbles-config" (mkFeatureLockUnlockRoute @ChatBubblesConfig) :<|> Named @"lock-unlock-route-apps-config" (mkFeatureLockUnlockRoute @AppsConfig) + :<|> Named @"lock-unlock-route-meetings-config" (mkFeatureLockUnlockRoute @MeetingsConfig) + :<|> Named @"lock-unlock-route-meetings-premium-config" (mkFeatureLockUnlockRoute @MeetingsPremiumConfig) sitemapInternal :: Servant.Server SternAPIInternal sitemapInternal = @@ -446,16 +455,16 @@ getUserData uid mMaxConvs mMaxNotifs = do notfs <- ( Intra.getUserNotifications uid (fromMaybe 100 mMaxNotifs) <&> toJSON @[QueuedNotification] - ) + ) `catchE` (pure . String . T.pack . show) -- galeb consent <- (Intra.getUserConsentValue uid <&> toJSON @ConsentValue) - `catchE` (pure . String . T.pack . show) + `catchE` (pure . String . T.pack . displayExceptionNoBacktrace) consentLog <- (Intra.getUserConsentLog uid <&> toJSON @ConsentLog) - `catchE` (pure . String . T.pack . show) + `catchE` (pure . String . T.pack . displayExceptionNoBacktrace) let em = userEmail account marketo <- do let noEmail = MarketoResult $ KeyMap.singleton "results" emptyArray @@ -463,7 +472,7 @@ getUserData uid mMaxConvs mMaxNotifs = do (pure $ toJSON noEmail) ( \e -> (Intra.getMarketoResult e <&> toJSON) - `catchE` (pure . String . T.pack . show) + `catchE` (pure . String . T.pack . displayExceptionNoBacktrace) ) em pure . UserMetaInfo . KeyMap.fromList $ diff --git a/tools/stern/src/Stern/API/Routes.hs b/tools/stern/src/Stern/API/Routes.hs index 086bcc6e061..e50562ee9dc 100644 --- a/tools/stern/src/Stern/API/Routes.hs +++ b/tools/stern/src/Stern/API/Routes.hs @@ -308,7 +308,9 @@ type SternAPI = :> MkFeaturePutRoute EnforceFileDownloadLocationConfig ) :<|> Named "get-route-cells" (MkFeatureGetRoute CellsConfig) - :<|> Named "put-route-cells" (MkFeatureStatusPutRoute CellsConfig) + :<|> Named "put-route-cells" (MkFeaturePutRoute CellsConfig) + :<|> Named "get-route-cells-internal" (MkFeatureGetRoute CellsInternalConfig) + :<|> Named "put-route-cells-internal" (MkFeaturePutRoute CellsInternalConfig) :<|> Named "get-route-guest-links" (MkFeatureGetRoute GuestLinksConfig) :<|> Named "put-route-guest-links" (MkFeatureStatusPutRoute GuestLinksConfig) :<|> Named "get-route-self-deleting-messages" (MkFeatureGetRoute SelfDeletingMessagesConfig) @@ -325,6 +327,10 @@ type SternAPI = :<|> Named "put-route-apps-config" (MkFeatureStatusPutRoute AppsConfig) :<|> Named "get-route-stealth-users-config" (MkFeatureGetRoute StealthUsersConfig) :<|> Named "put-route-stealth-users-config" (MkFeatureStatusPutRoute StealthUsersConfig) + :<|> Named "get-route-meetings-config" (MkFeatureGetRoute MeetingsConfig) + :<|> Named "put-route-meetings-config" (MkFeatureStatusPutRoute MeetingsConfig) + :<|> Named "get-route-meetings-premium-config" (MkFeatureGetRoute MeetingsPremiumConfig) + :<|> Named "put-route-meetings-premium-config" (MkFeatureStatusPutRoute MeetingsPremiumConfig) :<|> Named "get-team-invoice" ( Summary "Get a specific invoice by Number" @@ -476,6 +482,8 @@ type SternAPI = :<|> Named "lock-unlock-route-consumable-notifications-config" (MkFeatureLockUnlockRoute ConsumableNotificationsConfig) :<|> Named "lock-unlock-route-chat-bubbles-config" (MkFeatureLockUnlockRoute ChatBubblesConfig) :<|> Named "lock-unlock-route-apps-config" (MkFeatureLockUnlockRoute AppsConfig) + :<|> Named "lock-unlock-route-meetings-config" (MkFeatureLockUnlockRoute MeetingsConfig) + :<|> Named "lock-unlock-route-meetings-premium-config" (MkFeatureLockUnlockRoute MeetingsPremiumConfig) ------------------------------------------------------------------------------- -- Swagger diff --git a/tools/stern/src/Stern/Intra.hs b/tools/stern/src/Stern/Intra.hs index b81c11fb597..6daee4beffe 100644 --- a/tools/stern/src/Stern/Intra.hs +++ b/tools/stern/src/Stern/Intra.hs @@ -81,7 +81,7 @@ import Data.Aeson hiding (Error) import Data.Aeson.KeyMap qualified as KeyMap import Data.Aeson.Types (emptyArray) import Data.ByteString.Char8 qualified as BS -import Data.ByteString.Conversion +import Data.ByteString.Conversion as BSC import Data.ByteString.UTF8 qualified as UTF8 import Data.Domain import Data.Handle (Handle) @@ -101,6 +101,7 @@ import Network.HTTP.Types (urlEncode) import Network.HTTP.Types.Method import Network.HTTP.Types.Status hiding (statusCode, statusMessage) import Network.Wai.Utilities (Error (..), mkError) +import Network.Wai.Utilities.Exception import Servant.API import Servant.Client qualified as SC import Servant.Server qualified as SS @@ -196,7 +197,7 @@ getUserConnections uid = do parseResponse (mkError status502 "bad-upstream") r batchSize = 100 :: Int -getUsersConnections :: List UserId -> Handler [ConnectionStatus] +getUsersConnections :: BSC.List UserId -> Handler [ConnectionStatus] getUsersConnections uids = do info $ msg "Getting user connections" b <- asks (.brig) @@ -654,7 +655,7 @@ catchRpcErrors action = ExceptT $ catch (Right <$> action) catchRPCException catchRPCException :: RPCException -> App (Either Error a) catchRPCException rpcE = do Log.err $ rpcExceptionMsg rpcE - pure . Left $ mkError status500 "io-error" (pack $ show rpcE) + pure . Left $ mkError status500 "io-error" (pack $ displayExceptionNoBacktrace rpcE) getTeamData :: TeamId -> Handler TeamData getTeamData tid = do @@ -1063,7 +1064,7 @@ runClientToHandler :: SC.ClientM a -> Handler a runClientToHandler client = do clientEnv <- asks (.brigServantClientEnv) res <- liftIO $ SC.runClientM client clientEnv - either (throwE . mkError status400 "servant-client-error" . LT.pack . displayException) pure res + either (throwE . mkError status400 "servant-client-error" . LT.pack . displayExceptionNoBacktrace) pure res domRegLock :: Domain -> SC.ClientM NoContent domRegUnlock :: Domain -> SC.ClientM NoContent diff --git a/tools/stern/test/integration/API.hs b/tools/stern/test/integration/API.hs index 19be283d2fb..7d80f83d9f4 100644 --- a/tools/stern/test/integration/API.hs +++ b/tools/stern/test/integration/API.hs @@ -33,6 +33,8 @@ import Data.ByteString.Conversion import Data.Default import Data.Handle import Data.Id +import Data.Json.Util (BigIntString (..)) +import Data.Misc (HttpsUrl) import Data.Range (unsafeRange) import Data.Schema import Data.Set qualified as Set @@ -104,6 +106,7 @@ tests s = test s "i/domain-registration" testDomainRegistration, test s "GET /teams/:tid/features/domainRegistration" $ testFeatureStatus @DomainRegistrationConfig, test s "PUT /teams/:tid/features/domainRegistration{,'?lockOrUnlock'}" $ testFeatureStatusWithLock @DomainRegistrationConfig, + test s "/teams/:tid/features/cells" testCellsConfigRoutes, test s "/teams/:tid/features/channels" $ testLockedFeatureConfig @ChannelsConfig, test s "PUT /teams/:tid/features/channels{,'?lockOrUnlock'}" $ testLockStatus @ChannelsConfig, test s "PUT /teams/:tid/features/digitalSignatures{,'?lockOrUnlock'}" $ testLockStatus @DigitalSignaturesConfig, @@ -116,11 +119,16 @@ tests s = test s "PUT /teams/:tid/features/sndFactorPasswordChallenge{,'?lockOrUnlock'}" $ testLockStatus @SndFactorPasswordChallengeConfig, test s "PUT /teams/:tid/features/limitedEventFanout{,'?lockOrUnlock'}" $ testLockStatus @LimitedEventFanoutConfig, test s "PUT /teams/:tid/features/cells{,'?lockOrUnlock'}" $ testLockStatus @CellsConfig, + test s "/teams/:tid/features/cellsInternal" testCellsInternalConfig, test s "PUT /teams/:tid/features/consumableNotifications{,'?lockOrUnlock'}" $ testLockStatus @ConsumableNotificationsConfig, test s "PUT /teams/:tid/features/chatBubbles{,'?lockOrUnlock'}" $ testLockStatus @ChatBubblesConfig, test s "/teams/:tid/features/chatBubbles" $ testFeatureStatus @ChatBubblesConfig, test s "PUT /teams/:tid/features/apps{,'?lockOrUnlock'}" $ testLockStatus @AppsConfig, - test s "/teams/:tid/features/apps" $ testFeatureStatus @AppsConfig + test s "/teams/:tid/features/apps" $ testFeatureStatus @AppsConfig, + test s "PUT /teams/:tid/features/meetings{,'?lockOrUnlock'}" $ testLockStatus @MeetingsConfig, + test s "/teams/:tid/features/meetings" $ testFeatureStatus @MeetingsConfig, + test s "PUT /teams/:tid/features/meetingsPremium{,'?lockOrUnlock'}" $ testLockStatus @MeetingsPremiumConfig, + test s "/teams/:tid/features/meetingsPremium" $ testFeatureStatus @MeetingsPremiumConfig -- The following endpoints can not be tested here because they require ibis: -- - `GET /teams/:tid/billing` -- - `GET /teams/:tid/invoice/:inr` @@ -327,6 +335,84 @@ testFeatureConfig = do cfg' <- getFeatureConfig @cfg tid liftIO $ cfg'.status @?= newStatus +testCellsConfigRoutes :: TestM () +testCellsConfigRoutes = do + (_, tid, _) <- createTeamWithNMembers 1 + cfg <- getFeatureConfig @CellsConfig tid + -- at the time of writing the galley.integration.yaml has the feature enabled and unlocked + liftIO $ cfg @?= def {status = FeatureStatusEnabled, lockStatus = LockStatusUnlocked} + + putFeatureStatusLock @CellsConfig tid LockStatusUnlocked Nothing !!! const 200 === statusCode + + let updatedConfig :: LockableFeature CellsConfig + updatedConfig = + LockableFeature + { status = FeatureStatusEnabled, + lockStatus = LockStatusUnlocked, + config = + CellsConfig + { channels = CellsProperty {enabled = False, default_ = Enforced}, + groups = CellsProperty {enabled = True, default_ = Disabled}, + one2one = CellsProperty {enabled = True, default_ = Enabled}, + users = CellsUsers {externals = False, guests = True}, + collabora = CellsCollaboraStatus {enabled = True}, + publicLinks = + CellsPublicLinks + { enableFiles = True, + enableFolders = False, + enforcePassword = True, + enforceExpirationMax = 86400, + enforceExpirationDefault = 3600 + }, + storage = + CellsConfigStorage + { perFileQuotaBytes = NumBytes (BigIntString 2000000000), + recycle = + CellsRecycle + { autoPurgeDays = 14, + disable = False, + allowSkip = True + } + }, + metadata = + CellsMetadata + { namespaces = + CellsNamespaces + { usermetaTags = + CellsUserMetaTags + { defaultValues = ["default-tag"], + allowFreeValues = False + } + } + } + } + } + + putFeatureConfig @CellsConfig tid updatedConfig !!! const 200 === statusCode + cfg' <- getFeatureConfig @CellsConfig tid + liftIO $ cfg' @?= updatedConfig + +testCellsInternalConfig :: TestM () +testCellsInternalConfig = do + (_, tid, _) <- createTeamWithNMembers 1 + cfg <- getFeatureConfig @CellsInternalConfig tid + let newBackend :: HttpsUrl + newBackend = fromMaybe (error "invalid url") . fromByteString $ "https://cells-internal.example.com" + newCfg = + cfg + { config = + cfg.config + { backend = CellsBackend newBackend, + collabora = CellsCollabora Cool, + storage = CellsStorage (NumBytes (BigIntString 2000000000000)) + } + } :: + LockableFeature CellsInternalConfig + + putFeatureConfig @CellsInternalConfig tid newCfg !!! const 200 === statusCode + cfg' <- getFeatureConfig @CellsInternalConfig tid + liftIO $ cfg' @?= newCfg + testGetFeatureConfig :: forall cfg. ( KnownSymbol (FeatureSymbol cfg), diff --git a/treefmt.toml b/treefmt.toml index 847bdbb793f..fd38436758e 100644 --- a/treefmt.toml +++ b/treefmt.toml @@ -1,9 +1,6 @@ [formatter.nix] command = "nixpkgs-fmt" includes = ["*.nix"] -excludes = [ - "nix/sources.nix" # managed by niv. -] [formatter.cabal-fmt] command = "cabal-fmt"